rigortype 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/15-type-protection-coverage.md +6 -4
- data/lib/rigor/analysis/baseline.rb +56 -96
- data/lib/rigor/analysis/buffer_binding.rb +13 -20
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
- data/lib/rigor/analysis/check_rules.rb +174 -274
- data/lib/rigor/analysis/dependency_recorder.rb +26 -38
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
- data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
- data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
- data/lib/rigor/analysis/diagnostic.rb +48 -75
- data/lib/rigor/analysis/erb_template_detector.rb +13 -18
- data/lib/rigor/analysis/fact_store.rb +12 -22
- data/lib/rigor/analysis/incremental.rb +47 -63
- data/lib/rigor/analysis/incremental_session.rb +74 -100
- data/lib/rigor/analysis/project_scan.rb +12 -23
- data/lib/rigor/analysis/result.rb +3 -5
- data/lib/rigor/analysis/rule_catalog.rb +67 -95
- data/lib/rigor/analysis/run_stats.rb +38 -61
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
- data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
- data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
- data/lib/rigor/analysis/runner.rb +220 -333
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
- data/lib/rigor/analysis/worker_session.rb +64 -102
- data/lib/rigor/ast/type_node.rb +6 -8
- data/lib/rigor/ast.rb +6 -10
- data/lib/rigor/bleeding_edge.rb +27 -35
- data/lib/rigor/builtins/hkt_builtins.rb +7 -14
- data/lib/rigor/builtins/imported_refinements.rb +106 -149
- data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
- data/lib/rigor/builtins/regex_refinement.rb +32 -43
- data/lib/rigor/builtins/static_return_refinements.rb +57 -81
- data/lib/rigor/cache/descriptor.rb +58 -94
- data/lib/rigor/cache/incremental_snapshot.rb +32 -48
- data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
- data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
- data/lib/rigor/cache/rbs_constant_table.rb +8 -13
- data/lib/rigor/cache/rbs_descriptor.rb +8 -13
- data/lib/rigor/cache/rbs_environment.rb +11 -19
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
- data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
- data/lib/rigor/cache/store.rb +96 -151
- data/lib/rigor/cli/annotate_command.rb +51 -88
- data/lib/rigor/cli/baseline_command.rb +10 -17
- data/lib/rigor/cli/check_command.rb +121 -199
- data/lib/rigor/cli/check_runner_factory.rb +8 -14
- data/lib/rigor/cli/ci_detector.rb +9 -14
- data/lib/rigor/cli/command.rb +8 -14
- data/lib/rigor/cli/coverage_command.rb +19 -26
- data/lib/rigor/cli/coverage_mutation.rb +14 -19
- data/lib/rigor/cli/coverage_report.rb +0 -2
- data/lib/rigor/cli/coverage_scan.rb +7 -11
- data/lib/rigor/cli/diagnostic_formats.rb +43 -62
- data/lib/rigor/cli/diff_command.rb +10 -20
- data/lib/rigor/cli/docs_command.rb +26 -40
- data/lib/rigor/cli/doctor_command.rb +8 -9
- data/lib/rigor/cli/explain_command.rb +4 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
- data/lib/rigor/cli/fused_protection_report.rb +6 -8
- data/lib/rigor/cli/lsp_command.rb +11 -19
- data/lib/rigor/cli/mcp_command.rb +4 -6
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
- data/lib/rigor/cli/mutation_protection_report.rb +6 -9
- data/lib/rigor/cli/options.rb +11 -19
- data/lib/rigor/cli/plugin_command.rb +18 -30
- data/lib/rigor/cli/plugins_command.rb +29 -51
- data/lib/rigor/cli/plugins_renderer.rb +12 -20
- data/lib/rigor/cli/prism_colorizer.rb +13 -19
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +27 -17
- data/lib/rigor/cli/renderable.rb +4 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
- data/lib/rigor/cli/sig_gen_command.rb +14 -26
- data/lib/rigor/cli/skill_command.rb +30 -47
- data/lib/rigor/cli/skill_describe.rb +40 -64
- data/lib/rigor/cli/trace_command.rb +9 -16
- data/lib/rigor/cli/trace_renderer.rb +35 -51
- data/lib/rigor/cli/triage_command.rb +6 -10
- data/lib/rigor/cli/triage_renderer.rb +1 -2
- data/lib/rigor/cli/type_of_command.rb +14 -23
- data/lib/rigor/cli/type_of_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_command.rb +6 -10
- data/lib/rigor/cli/type_scan_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_report.rb +2 -3
- data/lib/rigor/cli/upgrade_command.rb +2 -3
- data/lib/rigor/cli.rb +16 -28
- data/lib/rigor/config_audit.rb +35 -44
- data/lib/rigor/configuration/dependencies.rb +41 -70
- data/lib/rigor/configuration/severity_profile.rb +28 -42
- data/lib/rigor/configuration.rb +150 -241
- data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
- data/lib/rigor/environment/class_registry.rb +19 -26
- data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
- data/lib/rigor/environment/lockfile_resolver.rb +31 -47
- data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
- data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
- data/lib/rigor/environment/rbs_loader.rb +420 -518
- data/lib/rigor/environment/reflection.rb +28 -53
- data/lib/rigor/environment/reporters.rb +13 -24
- data/lib/rigor/environment.rb +176 -291
- data/lib/rigor/flow_contribution/conflict.rb +16 -27
- data/lib/rigor/flow_contribution/element.rb +7 -12
- data/lib/rigor/flow_contribution/fact.rb +33 -57
- data/lib/rigor/flow_contribution/merge_result.rb +6 -9
- data/lib/rigor/flow_contribution/merger.rb +32 -47
- data/lib/rigor/flow_contribution.rb +37 -55
- data/lib/rigor/inference/acceptance.rb +133 -219
- data/lib/rigor/inference/block_parameter_binder.rb +52 -82
- data/lib/rigor/inference/body_fixpoint.rb +22 -30
- data/lib/rigor/inference/budget_trace.rb +43 -63
- data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
- data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
- data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
- data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
- data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
- data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
- data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
- data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
- data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
- data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
- data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
- data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
- data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
- data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
- data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
- data/lib/rigor/inference/coverage_scanner.rb +13 -18
- data/lib/rigor/inference/def_return_typer.rb +8 -14
- data/lib/rigor/inference/dynamic_origin.rb +23 -20
- data/lib/rigor/inference/expression_typer.rb +774 -1152
- data/lib/rigor/inference/fallback.rb +8 -12
- data/lib/rigor/inference/fallback_tracer.rb +4 -10
- data/lib/rigor/inference/flow_tracer.rb +25 -36
- data/lib/rigor/inference/hkt_body.rb +45 -68
- data/lib/rigor/inference/hkt_body_parser.rb +33 -52
- data/lib/rigor/inference/hkt_reducer.rb +38 -59
- data/lib/rigor/inference/hkt_registry.rb +50 -76
- data/lib/rigor/inference/indexed_narrowing.rb +55 -81
- data/lib/rigor/inference/macro_block_self_type.rb +21 -34
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
- data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
- data/lib/rigor/inference/method_dispatcher.rb +378 -524
- data/lib/rigor/inference/method_parameter_binder.rb +81 -114
- data/lib/rigor/inference/multi_target_binder.rb +51 -68
- data/lib/rigor/inference/mutation_widening.rb +115 -158
- data/lib/rigor/inference/narrowing.rb +509 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
- data/lib/rigor/inference/precision_scanner.rb +26 -35
- data/lib/rigor/inference/project_patched_methods.rb +20 -32
- data/lib/rigor/inference/project_patched_scanner.rb +23 -37
- data/lib/rigor/inference/protection_scanner.rb +16 -18
- data/lib/rigor/inference/rbs_type_translator.rb +49 -66
- data/lib/rigor/inference/scope_indexer.rb +473 -817
- data/lib/rigor/inference/statement_evaluator.rb +706 -1111
- data/lib/rigor/inference/struct_fold_safety.rb +34 -48
- data/lib/rigor/inference/synthetic_method.rb +9 -16
- data/lib/rigor/inference/synthetic_method_index.rb +20 -35
- data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
- data/lib/rigor/language_server/buffer_resolution.rb +6 -9
- data/lib/rigor/language_server/buffer_table.rb +11 -18
- data/lib/rigor/language_server/completion_provider.rb +69 -116
- data/lib/rigor/language_server/debouncer.rb +16 -25
- data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
- data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
- data/lib/rigor/language_server/folding_range_provider.rb +11 -19
- data/lib/rigor/language_server/hover_provider.rb +8 -11
- data/lib/rigor/language_server/hover_renderer.rb +56 -93
- data/lib/rigor/language_server/loop.rb +14 -19
- data/lib/rigor/language_server/project_context.rb +46 -74
- data/lib/rigor/language_server/selection_range_provider.rb +9 -12
- data/lib/rigor/language_server/server.rb +55 -83
- data/lib/rigor/language_server/signature_help_provider.rb +29 -46
- data/lib/rigor/language_server/synchronized_writer.rb +4 -7
- data/lib/rigor/language_server/uri.rb +8 -13
- data/lib/rigor/language_server.rb +4 -6
- data/lib/rigor/mcp/loop.rb +2 -3
- data/lib/rigor/mcp/server.rb +4 -7
- data/lib/rigor/mcp.rb +3 -6
- data/lib/rigor/plugin/access_denied_error.rb +5 -8
- data/lib/rigor/plugin/additional_initializer.rb +21 -31
- data/lib/rigor/plugin/base.rb +335 -518
- data/lib/rigor/plugin/blueprint.rb +14 -23
- data/lib/rigor/plugin/box.rb +18 -29
- data/lib/rigor/plugin/fact_store.rb +16 -26
- data/lib/rigor/plugin/inflector.rb +37 -53
- data/lib/rigor/plugin/io_boundary.rb +33 -56
- data/lib/rigor/plugin/isolation.rb +42 -55
- data/lib/rigor/plugin/load_error.rb +10 -15
- data/lib/rigor/plugin/loader.rb +30 -49
- data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
- data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
- data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
- data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
- data/lib/rigor/plugin/macro.rb +10 -15
- data/lib/rigor/plugin/manifest.rb +85 -144
- data/lib/rigor/plugin/node_context.rb +14 -22
- data/lib/rigor/plugin/node_rule_walk.rb +49 -74
- data/lib/rigor/plugin/protocol_contract.rb +25 -39
- data/lib/rigor/plugin/registry.rb +132 -205
- data/lib/rigor/plugin/services.rb +21 -33
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
- data/lib/rigor/plugin/trust_policy.rb +24 -38
- data/lib/rigor/plugin/type_node_resolver.rb +15 -24
- data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
- data/lib/rigor/protection/mutation_scanner.rb +27 -35
- data/lib/rigor/protection/mutator.rb +50 -70
- data/lib/rigor/protection/test_suite_oracle.rb +20 -27
- data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
- data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
- data/lib/rigor/rbs_extended/reporter.rb +24 -40
- data/lib/rigor/rbs_extended.rb +107 -197
- data/lib/rigor/reflection.rb +68 -86
- data/lib/rigor/scope/discovery_index.rb +14 -19
- data/lib/rigor/scope.rb +255 -310
- data/lib/rigor/sig_gen/classification.rb +6 -10
- data/lib/rigor/sig_gen/generator.rb +197 -323
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +12 -17
- data/lib/rigor/sig_gen/observation_collector.rb +38 -70
- data/lib/rigor/sig_gen/observed_call.rb +13 -23
- data/lib/rigor/sig_gen/path_mapper.rb +17 -29
- data/lib/rigor/sig_gen/renderer.rb +7 -13
- data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
- data/lib/rigor/sig_gen/write_result.rb +8 -16
- data/lib/rigor/sig_gen/writer.rb +95 -174
- data/lib/rigor/sig_gen.rb +3 -6
- data/lib/rigor/signature_path_audit.rb +24 -30
- data/lib/rigor/source/constant_path.rb +10 -14
- data/lib/rigor/source/literals.rb +31 -45
- data/lib/rigor/source/node_locator.rb +9 -11
- data/lib/rigor/source/node_walker.rb +9 -13
- data/lib/rigor/source.rb +3 -4
- data/lib/rigor/testing.rb +16 -20
- data/lib/rigor/triage/catalogue.rb +38 -62
- data/lib/rigor/triage.rb +31 -52
- data/lib/rigor/trinary.rb +9 -13
- data/lib/rigor/type/acceptance_router.rb +4 -6
- data/lib/rigor/type/accepts_result.rb +10 -14
- data/lib/rigor/type/app.rb +19 -27
- data/lib/rigor/type/bot.rb +4 -6
- data/lib/rigor/type/bound_method.rb +10 -15
- data/lib/rigor/type/combinator.rb +165 -257
- data/lib/rigor/type/constant.rb +23 -34
- data/lib/rigor/type/data_class.rb +10 -15
- data/lib/rigor/type/data_instance.rb +14 -20
- data/lib/rigor/type/difference.rb +21 -32
- data/lib/rigor/type/dynamic.rb +3 -5
- data/lib/rigor/type/hash_shape.rb +32 -18
- data/lib/rigor/type/integer_range.rb +11 -16
- data/lib/rigor/type/intersection.rb +27 -42
- data/lib/rigor/type/nominal.rb +10 -15
- data/lib/rigor/type/plain_lattice.rb +9 -13
- data/lib/rigor/type/refined.rb +67 -114
- data/lib/rigor/type/singleton.rb +4 -6
- data/lib/rigor/type/struct_class.rb +11 -16
- data/lib/rigor/type/struct_instance.rb +15 -21
- data/lib/rigor/type/tuple.rb +14 -19
- data/lib/rigor/type/union.rb +30 -42
- data/lib/rigor/type_node/generic.rb +14 -26
- data/lib/rigor/type_node/identifier.rb +12 -19
- data/lib/rigor/type_node.rb +3 -12
- data/lib/rigor/value_semantics.rb +16 -21
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
- data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
- data/sig/rigor/scope.rbs +6 -0
- metadata +2 -1
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -12,17 +12,13 @@ require_relative "../source/node_walker"
|
|
|
12
12
|
|
|
13
13
|
module Rigor
|
|
14
14
|
module Plugin
|
|
15
|
-
# Base class every Rigor plugin subclasses. The plugin gem
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# overrides {#init} to wire up any state it needs from the
|
|
19
|
-
# injected service container.
|
|
15
|
+
# Base class every Rigor plugin subclasses. The plugin gem subclasses {Base}, declares its identity
|
|
16
|
+
# through {.manifest}, registers the subclass with {Rigor::Plugin.register}, and overrides {#init} to wire
|
|
17
|
+
# up any state it needs from the injected service container.
|
|
20
18
|
#
|
|
21
|
-
# This class implements all plugin protocol hooks: per-call
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# and producer/cache hooks. Cumulative implementation per the
|
|
25
|
-
# ADR-37 / ADR-52 slice chain.
|
|
19
|
+
# This class implements all plugin protocol hooks: per-call return-type contributions (`dynamic_return`),
|
|
20
|
+
# narrowing-fact contributions (`narrowing_facts`), AST node rules (`node_rule`), and producer/cache
|
|
21
|
+
# hooks. Cumulative implementation per the ADR-37 / ADR-52 slice chain.
|
|
26
22
|
#
|
|
27
23
|
# Example plugin:
|
|
28
24
|
#
|
|
@@ -42,10 +38,8 @@ module Rigor
|
|
|
42
38
|
# Rigor::Plugin.register(MyRailsPlugin)
|
|
43
39
|
class Base # rubocop:disable Metrics/ClassLength
|
|
44
40
|
class << self
|
|
45
|
-
# Declares the plugin's manifest. Called once at class
|
|
46
|
-
#
|
|
47
|
-
# the class so {Rigor::Plugin::Loader} reads it without
|
|
48
|
-
# constructing the plugin.
|
|
41
|
+
# Declares the plugin's manifest. Called once at class definition time — the resulting {Manifest} is
|
|
42
|
+
# cached on the class so {Rigor::Plugin::Loader} reads it without constructing the plugin.
|
|
49
43
|
def manifest(**fields)
|
|
50
44
|
if fields.empty?
|
|
51
45
|
raise ArgumentError, "plugin #{self} did not declare a manifest" unless defined?(@manifest) && @manifest
|
|
@@ -56,8 +50,7 @@ module Rigor
|
|
|
56
50
|
@manifest = Manifest.new(**fields)
|
|
57
51
|
end
|
|
58
52
|
|
|
59
|
-
# ADR-7 § "Slice 6-A" — DSL declaration of a cached
|
|
60
|
-
# producer. Plugin authors write
|
|
53
|
+
# ADR-7 § "Slice 6-A" — DSL declaration of a cached producer. Plugin authors write
|
|
61
54
|
#
|
|
62
55
|
# class MyPlugin < Rigor::Plugin::Base
|
|
63
56
|
# manifest(id: "rails", version: "0.1.0")
|
|
@@ -68,44 +61,31 @@ module Rigor
|
|
|
68
61
|
# end
|
|
69
62
|
# end
|
|
70
63
|
#
|
|
71
|
-
# The block runs through `instance_exec` so `self` inside
|
|
72
|
-
#
|
|
73
|
-
# `
|
|
74
|
-
#
|
|
75
|
-
# argument; the same params Hash mixes into the cache
|
|
76
|
-
# key per `Cache::Descriptor#cache_key_for`.
|
|
64
|
+
# The block runs through `instance_exec` so `self` inside the body is the plugin instance —
|
|
65
|
+
# `io_boundary`, `services`, `manifest`, `config` are all in scope. The block receives the call-site
|
|
66
|
+
# `params` Hash as its sole argument; the same params Hash mixes into the cache key per
|
|
67
|
+
# `Cache::Descriptor#cache_key_for`.
|
|
77
68
|
#
|
|
78
|
-
# `serialize:` / `deserialize:` apply to the producer's
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
# v0.0.9 callable surface; producers whose return values
|
|
83
|
-
# are not Marshal-clean must supply their own pair.
|
|
69
|
+
# `serialize:` / `deserialize:` apply to the producer's return VALUE (the cache layer wraps them
|
|
70
|
+
# around the record-and-validate entry pair itself). Default round-trip is `Marshal.dump` /
|
|
71
|
+
# `Marshal.load` per the v0.0.9 callable surface; producers whose return values are not Marshal-clean
|
|
72
|
+
# must supply their own pair.
|
|
84
73
|
#
|
|
85
|
-
# `watch:` (ADR-60 WD3) declares the glob coverage of a
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# when the producer block never read them individually
|
|
89
|
-
# (e.g. it globbed a directory itself). It is either
|
|
74
|
+
# `watch:` (ADR-60 WD3) declares the glob coverage of a discovery-style producer — the files whose
|
|
75
|
+
# addition / removal / edit must invalidate the cached value even when the producer block never read
|
|
76
|
+
# them individually (e.g. it globbed a directory itself). It is either
|
|
90
77
|
#
|
|
91
|
-
# - a static Array of `[roots, pattern, ...]` tuples
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# instance at `cache_for` invocation time (NEVER at
|
|
97
|
-
# class-definition time — search roots are typically
|
|
98
|
-
# computed in `#init` from config), returning the same
|
|
99
|
-
# tuple Array.
|
|
78
|
+
# - a static Array of `[roots, pattern, ...]` tuples (`roots` a String or Array of Strings; one or
|
|
79
|
+
# more glob-pattern suffixes per tuple — the same shape {#glob_descriptor} takes), or
|
|
80
|
+
# - a Proc, run through `instance_exec` on the plugin instance at `cache_for` invocation time (NEVER
|
|
81
|
+
# at class-definition time — search roots are typically computed in `#init` from config), returning
|
|
82
|
+
# the same tuple Array.
|
|
100
83
|
#
|
|
101
|
-
# The evaluated tuples become {Cache::Descriptor::GlobEntry}
|
|
102
|
-
#
|
|
103
|
-
# block runs; `Descriptor#fresh?` re-globs + re-digests on
|
|
104
|
-
# the next run.
|
|
84
|
+
# The evaluated tuples become {Cache::Descriptor::GlobEntry} rows in the dependency descriptor
|
|
85
|
+
# recorded after the block runs; `Descriptor#fresh?` re-globs + re-digests on the next run.
|
|
105
86
|
#
|
|
106
|
-
# Producer ids are auto-prefixed `plugin.<manifest.id>.`
|
|
107
|
-
#
|
|
108
|
-
# collide with built-in producers.
|
|
87
|
+
# Producer ids are auto-prefixed `plugin.<manifest.id>.` at the cache layer (slice 6-C) so plugin-side
|
|
88
|
+
# ids cannot collide with built-in producers.
|
|
109
89
|
def producer(id, watch: nil, serialize: nil, deserialize: nil, &block)
|
|
110
90
|
raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?
|
|
111
91
|
|
|
@@ -117,8 +97,8 @@ module Rigor
|
|
|
117
97
|
id.to_sym
|
|
118
98
|
end
|
|
119
99
|
|
|
120
|
-
# ADR-60 WD3 — `watch:` is nil (no glob coverage), a static
|
|
121
|
-
#
|
|
100
|
+
# ADR-60 WD3 — `watch:` is nil (no glob coverage), a static tuple Array, or a Proc evaluated per
|
|
101
|
+
# `cache_for` call.
|
|
122
102
|
def validate_producer_watch!(watch)
|
|
123
103
|
return if watch.nil? || watch.is_a?(Array) || watch.respond_to?(:call)
|
|
124
104
|
|
|
@@ -128,20 +108,16 @@ module Rigor
|
|
|
128
108
|
end
|
|
129
109
|
private :validate_producer_watch!
|
|
130
110
|
|
|
131
|
-
# Frozen snapshot of the producer table. Inherited
|
|
132
|
-
# producers
|
|
133
|
-
#
|
|
134
|
-
# producers, and the loader instantiates one
|
|
135
|
-
# subclass per registration.
|
|
111
|
+
# Frozen snapshot of the producer table. Inherited producers from a superclass are intentionally NOT
|
|
112
|
+
# surfaced — Plugin::Base subclasses do not chain producers, and the loader instantiates one subclass
|
|
113
|
+
# per registration.
|
|
136
114
|
def producers
|
|
137
115
|
(@producers || {}).dup.freeze
|
|
138
116
|
end
|
|
139
117
|
|
|
140
|
-
# ADR-37 slice 1 — declares a node-scoped diagnostic rule.
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# rules registered for its type, so a plugin author writes the
|
|
144
|
-
# check, never the traversal:
|
|
118
|
+
# ADR-37 slice 1 — declares a node-scoped diagnostic rule. The engine owns a single AST walk per file
|
|
119
|
+
# (see {#node_rule_diagnostics}) and dispatches each node to the rules registered for its type, so a
|
|
120
|
+
# plugin author writes the check, never the traversal:
|
|
145
121
|
#
|
|
146
122
|
# class MyPlugin < Rigor::Plugin::Base
|
|
147
123
|
# manifest(id: "demo", version: "0.1.0")
|
|
@@ -152,25 +128,19 @@ module Rigor
|
|
|
152
128
|
# end
|
|
153
129
|
# end
|
|
154
130
|
#
|
|
155
|
-
# `node_type` is a `Prism::Node` subclass; the rule fires for
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# {Rigor::Plugin::NodeContext} carrying the node's lexical
|
|
164
|
-
# ancestors (enclosing class / method / block DSL). Trailing
|
|
165
|
-
# arguments may be omitted from the block's parameter list. The
|
|
166
|
-
# block MUST return an Array of `Rigor::Analysis::Diagnostic`
|
|
167
|
-
# (an empty array to fire nothing); the runner stamps
|
|
131
|
+
# `node_type` is a `Prism::Node` subclass; the rule fires for every node where
|
|
132
|
+
# `node.is_a?(node_type)`. The block runs through `instance_exec` so `self` is the plugin instance —
|
|
133
|
+
# `config`, `services`, `io_boundary`, `diagnostic`, and the cross-plugin `services.fact_store` are
|
|
134
|
+
# all in scope. It receives `(node, scope, path, file_context, context)` — the fourth argument is the
|
|
135
|
+
# value built by {.node_file_context} for a two-pass plugin (`nil` otherwise); the fifth is a
|
|
136
|
+
# {Rigor::Plugin::NodeContext} carrying the node's lexical ancestors (enclosing class / method / block
|
|
137
|
+
# DSL). Trailing arguments may be omitted from the block's parameter list. The block MUST return an
|
|
138
|
+
# Array of `Rigor::Analysis::Diagnostic` (an empty array to fire nothing); the runner stamps
|
|
168
139
|
# `plugin.<id>` provenance.
|
|
169
140
|
#
|
|
170
|
-
# Multiple rules for the same `node_type` are allowed and run
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
# needs the plugin instance, not pure data.
|
|
141
|
+
# Multiple rules for the same `node_type` are allowed and run in declaration order. This is the
|
|
142
|
+
# {.producer}-style class DSL rather than a manifest field because a rule carries logic that needs the
|
|
143
|
+
# plugin instance, not pure data.
|
|
174
144
|
def node_rule(node_type, &block)
|
|
175
145
|
raise ArgumentError, "Plugin::Base.node_rule requires a block body" if block.nil?
|
|
176
146
|
unless node_type.is_a?(Class) && node_type <= Prism::Node
|
|
@@ -183,19 +153,16 @@ module Rigor
|
|
|
183
153
|
node_type
|
|
184
154
|
end
|
|
185
155
|
|
|
186
|
-
# Frozen snapshot of the declared node rules, in declaration
|
|
187
|
-
#
|
|
188
|
-
# the loader instantiates one subclass per registration.
|
|
156
|
+
# Frozen snapshot of the declared node rules, in declaration order. Not inherited from a superclass —
|
|
157
|
+
# like {.producers}, the loader instantiates one subclass per registration.
|
|
189
158
|
def node_rules
|
|
190
159
|
(@node_rules || []).dup.freeze
|
|
191
160
|
end
|
|
192
161
|
|
|
193
|
-
# ADR-37 slice 1c — declares a per-file context builder for a
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
# is
|
|
197
|
-
# and returns an arbitrary file-local value that is threaded to
|
|
198
|
-
# every {.node_rule} block as its fourth argument:
|
|
162
|
+
# ADR-37 slice 1c — declares a per-file context builder for a two-pass (collect-then-validate)
|
|
163
|
+
# plugin. The block runs once per analysed file (via `instance_exec`, so the plugin instance is
|
|
164
|
+
# `self`) BEFORE any node rule fires, receives `(root, scope)`, and returns an arbitrary file-local
|
|
165
|
+
# value that is threaded to every {.node_rule} block as its fourth argument:
|
|
199
166
|
#
|
|
200
167
|
# node_file_context do |root, _scope|
|
|
201
168
|
# collect_declared_states(root) # the "collect" pass
|
|
@@ -206,16 +173,14 @@ module Rigor
|
|
|
206
173
|
# validate(node, path, states) # the "validate" pass
|
|
207
174
|
# end
|
|
208
175
|
#
|
|
209
|
-
# This is what lets a same-file two-pass plugin drop its
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
# `services.fact_store` instead — a node rule reads the fact
|
|
215
|
-
# directly and needs no per-file context.
|
|
176
|
+
# This is what lets a same-file two-pass plugin drop its hand-rolled validate walk: the collect pass
|
|
177
|
+
# computes the closed namespace once (it MUST complete before validation because a reference may
|
|
178
|
+
# precede its declaration), and the engine owns the validate walk. A cross-file collect belongs in
|
|
179
|
+
# `#prepare` + `services.fact_store` instead — a node rule reads the fact directly and needs no
|
|
180
|
+
# per-file context.
|
|
216
181
|
#
|
|
217
|
-
# Only one builder per plugin; a second declaration replaces the
|
|
218
|
-
#
|
|
182
|
+
# Only one builder per plugin; a second declaration replaces the first. The block result is `nil`
|
|
183
|
+
# when none is declared.
|
|
219
184
|
def node_file_context(&block)
|
|
220
185
|
raise ArgumentError, "Plugin::Base.node_file_context requires a block body" if block.nil?
|
|
221
186
|
|
|
@@ -227,10 +192,9 @@ module Rigor
|
|
|
227
192
|
defined?(@node_file_context_block) ? @node_file_context_block : nil
|
|
228
193
|
end
|
|
229
194
|
|
|
230
|
-
# ADR-37 slice 2 / ADR-52 WD2 — declares a per-call-site
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
# deleted `flow_contribution_for` hook (ADR-52 WD3):
|
|
195
|
+
# ADR-37 slice 2 / ADR-52 WD2 — declares a per-call-site return-type contribution, gated by receiver
|
|
196
|
+
# class, method name, or both. The narrow successor to the `return_type` slot of the deleted
|
|
197
|
+
# `flow_contribution_for` hook (ADR-52 WD3):
|
|
234
198
|
#
|
|
235
199
|
# # receiver-gated only:
|
|
236
200
|
# dynamic_return receivers: ["ActiveRecord::Base"] do |call_node, scope|
|
|
@@ -250,78 +214,59 @@ module Rigor
|
|
|
250
214
|
# # the block reads the receiver's shape itself
|
|
251
215
|
# end
|
|
252
216
|
#
|
|
253
|
-
# `receivers:` is a non-empty Array of class names; the engine
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
# `Environment#class_ordering`). It MAY be omitted — then the rule
|
|
257
|
-
# is receiver-independent and fires on `methods:` alone.
|
|
217
|
+
# `receivers:` is a non-empty Array of class names; the engine calls the block only when the call's
|
|
218
|
+
# receiver type's class equals or inherits from one of them (via `Environment#class_ordering`). It
|
|
219
|
+
# MAY be omitted — then the rule is receiver-independent and fires on `methods:` alone.
|
|
258
220
|
#
|
|
259
|
-
# `methods:` is an Array of Symbol method names. When provided, the
|
|
260
|
-
#
|
|
261
|
-
#
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
# neither would fire on every dispatch, which is exactly the
|
|
265
|
-
# ungated cost the `flow_contribution_for` escape valve carries —
|
|
266
|
-
# `dynamic_return` declines to reintroduce it.
|
|
221
|
+
# `methods:` is an Array of Symbol method names. When provided, the block is skipped unless
|
|
222
|
+
# `call_node.name` is in the list — declarative and cheaper than an in-block guard (the engine
|
|
223
|
+
# compiles it into the registry's contribution table, ADR-52 WD1). It is REQUIRED when `receivers:`
|
|
224
|
+
# is omitted: a rule gated on neither would fire on every dispatch, which is exactly the ungated cost
|
|
225
|
+
# the `flow_contribution_for` escape valve carries — `dynamic_return` declines to reintroduce it.
|
|
267
226
|
#
|
|
268
|
-
# Method-name and type-shape refinement can still be done inside
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
# the rule is consulted — always after `#prepare`) for a receiver
|
|
274
|
-
# set the plugin only knows at run time:
|
|
227
|
+
# Method-name and type-shape refinement can still be done inside the block. The block runs through
|
|
228
|
+
# `instance_exec`, so `config` / `services` are in scope.
|
|
229
|
+
# ADR-52 slice 3 — `receivers:` may also be a **callable** (a `-> { ... }` resolved once per run,
|
|
230
|
+
# lazily, the first time the rule is consulted — always after `#prepare`) for a receiver set the
|
|
231
|
+
# plugin only knows at run time:
|
|
275
232
|
#
|
|
276
233
|
# dynamic_return receivers: -> { attachment_index.model_names } do |call_node, scope|
|
|
277
234
|
# # fires when the receiver class is one a `prepare`-time scan
|
|
278
235
|
# # found; the block does the precise per-call lookup
|
|
279
236
|
# end
|
|
280
237
|
#
|
|
281
|
-
# The callable runs through `instance_exec`, so it reads the
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
# time. The resolved set is a safe over-approximation of the
|
|
287
|
-
# block's own filter (it admits subclasses too), so the block
|
|
288
|
-
# stays the precise gate and diagnostics are unchanged.
|
|
238
|
+
# The callable runs through `instance_exec`, so it reads the plugin's own `#prepare`-built indexes.
|
|
239
|
+
# It MUST be idempotent and post-`#prepare`-safe — reference a lazily-built / memoised index (as
|
|
240
|
+
# activestorage's `attachment_index` and activerecord's `model_index` are), never a value captured at
|
|
241
|
+
# class-definition time. The resolved set is a safe over-approximation of the block's own filter (it
|
|
242
|
+
# admits subclasses too), so the block stays the precise gate and diagnostics are unchanged.
|
|
289
243
|
#
|
|
290
|
-
# ADR-52 slice 4 — `methods:` may ALSO be a callable, for a
|
|
291
|
-
#
|
|
292
|
-
# catalog's keys, a config-derived DSL method name):
|
|
244
|
+
# ADR-52 slice 4 — `methods:` may ALSO be a callable, for a method-name set the plugin only knows at
|
|
245
|
+
# run time (a Sorbet catalog's keys, a config-derived DSL method name):
|
|
293
246
|
#
|
|
294
247
|
# dynamic_return methods: -> { catalog.method_names } do |call_node, scope|
|
|
295
248
|
# ...
|
|
296
249
|
# end
|
|
297
250
|
#
|
|
298
|
-
# Same contract as a callable `receivers:` — `instance_exec`'d,
|
|
299
|
-
#
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
# a
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
# set that varies per analysed file (rigor-rspec's `let` names —
|
|
308
|
-
# the names depend on each file's `describe`/`let` structure, so
|
|
309
|
-
# one run-wide set cannot exist). The callable receives the file
|
|
310
|
-
# path, runs through `instance_exec`, and is memoised per
|
|
311
|
-
# `(rule, path)`:
|
|
251
|
+
# Same contract as a callable `receivers:` — `instance_exec`'d, resolved lazily after `#prepare`,
|
|
252
|
+
# memoised, idempotent. A callable method set cannot be compiled into the registry's name gate (it is
|
|
253
|
+
# unknown at registry-build time), so the plugin is consulted on every dispatch and the name filter
|
|
254
|
+
# runs in this instance path instead — the block still only fires for a listed name, so diagnostics
|
|
255
|
+
# are unchanged.
|
|
256
|
+
# ADR-52 slice 5a — `file_methods:` is the per-file specialisation of the run-time `methods:`
|
|
257
|
+
# callable, for a name set that varies per analysed file (rigor-rspec's `let` names — the names
|
|
258
|
+
# depend on each file's `describe`/`let` structure, so one run-wide set cannot exist). The callable
|
|
259
|
+
# receives the file path, runs through `instance_exec`, and is memoised per `(rule, path)`:
|
|
312
260
|
#
|
|
313
261
|
# dynamic_return file_methods: ->(path) { let_names_for(path) } do |call_node, scope|
|
|
314
262
|
# ...
|
|
315
263
|
# end
|
|
316
264
|
#
|
|
317
|
-
# Same idempotence contract as the other callables, plus: it MUST
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
# `
|
|
321
|
-
# the
|
|
322
|
-
# `file_methods:` replaces `methods:` (declaring both is
|
|
323
|
-
# rejected — they are the same gate at two scopes); it MAY
|
|
324
|
-
# combine with `receivers:`.
|
|
265
|
+
# Same idempotence contract as the other callables, plus: it MUST tolerate any path the engine
|
|
266
|
+
# analyses (return `[]` / nil for a file it has no names for — never raise). Like a callable
|
|
267
|
+
# `methods:`, it cannot compile into the registry name gate, so the plugin is consulted on every
|
|
268
|
+
# dispatch and filtered here. `file_methods:` replaces `methods:` (declaring both is rejected — they
|
|
269
|
+
# are the same gate at two scopes); it MAY combine with `receivers:`.
|
|
325
270
|
def dynamic_return(receivers: nil, methods: nil, file_methods: nil, &block)
|
|
326
271
|
raise ArgumentError, "Plugin::Base.dynamic_return requires a block body" if block.nil?
|
|
327
272
|
|
|
@@ -340,8 +285,8 @@ module Rigor
|
|
|
340
285
|
nil
|
|
341
286
|
end
|
|
342
287
|
|
|
343
|
-
# A class-name Array is frozen element-wise; a run-time callable
|
|
344
|
-
#
|
|
288
|
+
# A class-name Array is frozen element-wise; a run-time callable (ADR-52 slice 3) is stored verbatim
|
|
289
|
+
# and resolved per instance.
|
|
345
290
|
def normalize_dynamic_return_receivers(receivers)
|
|
346
291
|
return nil if receivers.nil?
|
|
347
292
|
return receivers if receivers.respond_to?(:call)
|
|
@@ -350,9 +295,8 @@ module Rigor
|
|
|
350
295
|
end
|
|
351
296
|
private :normalize_dynamic_return_receivers
|
|
352
297
|
|
|
353
|
-
# A method-name Array is symbol-normalised + frozen; a run-time
|
|
354
|
-
#
|
|
355
|
-
# instance.
|
|
298
|
+
# A method-name Array is symbol-normalised + frozen; a run-time callable (ADR-52 slice 4) is stored
|
|
299
|
+
# verbatim and resolved per instance.
|
|
356
300
|
def normalize_dynamic_return_methods(methods)
|
|
357
301
|
return nil if methods.nil?
|
|
358
302
|
return methods if methods.respond_to?(:call)
|
|
@@ -361,26 +305,21 @@ module Rigor
|
|
|
361
305
|
end
|
|
362
306
|
private :normalize_dynamic_return_methods
|
|
363
307
|
|
|
364
|
-
# Frozen snapshot of the declared dynamic-return rules. Memoised:
|
|
365
|
-
#
|
|
366
|
-
# `
|
|
367
|
-
#
|
|
368
|
-
#
|
|
369
|
-
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
# safe.
|
|
373
|
-
# rubocop:disable Naming/MemoizedInstanceVariableName -- the
|
|
374
|
-
# natural name `@dynamic_returns` is the canonical (mutable-at-
|
|
375
|
-
# definition) store this snapshots; the memo must be distinct.
|
|
308
|
+
# Frozen snapshot of the declared dynamic-return rules. Memoised: `@dynamic_returns` is built once at
|
|
309
|
+
# class-definition time (via `dynamic_return`) and never mutated during analysis, and every element
|
|
310
|
+
# is already frozen, so a fresh `dup.freeze` per call was pure waste — the engine calls this for
|
|
311
|
+
# every plugin on every dispatch (`collect_plugin_contributions`), making it a top allocation site on
|
|
312
|
+
# plugin-heavy projects. The cached frozen array is immutable, so sharing one instance across callers
|
|
313
|
+
# is safe.
|
|
314
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName -- the natural name `@dynamic_returns` is the
|
|
315
|
+
# canonical (mutable-at-definition) store this snapshots; the memo must be distinct.
|
|
376
316
|
def dynamic_returns
|
|
377
317
|
@dynamic_returns_snapshot ||= (@dynamic_returns || []).dup.freeze
|
|
378
318
|
end
|
|
379
319
|
# rubocop:enable Naming/MemoizedInstanceVariableName
|
|
380
320
|
|
|
381
|
-
# ADR-52 WD2 — a rule must gate on something. `receivers:` alone,
|
|
382
|
-
#
|
|
383
|
-
# fire on every dispatch).
|
|
321
|
+
# ADR-52 WD2 — a rule must gate on something. `receivers:` alone, `methods:` alone, or both are
|
|
322
|
+
# valid; neither is not (it would fire on every dispatch).
|
|
384
323
|
def validate_dynamic_return_gate!(receivers, methods, file_methods)
|
|
385
324
|
return unless receivers.nil? && file_methods.nil?
|
|
386
325
|
return if (methods.is_a?(Array) && !methods.empty?) || methods.respond_to?(:call)
|
|
@@ -390,9 +329,8 @@ module Rigor
|
|
|
390
329
|
"gated on none would fire on every dispatch (that is what flow_contribution_for is for)"
|
|
391
330
|
end
|
|
392
331
|
|
|
393
|
-
# ADR-52 slice 5a — `file_methods:` must be a callable, and is
|
|
394
|
-
#
|
|
395
|
-
# declaring both is a contradiction, not a composition).
|
|
332
|
+
# ADR-52 slice 5a — `file_methods:` must be a callable, and is mutually exclusive with `methods:`
|
|
333
|
+
# (one name gate, two scopes — declaring both is a contradiction, not a composition).
|
|
396
334
|
def validate_dynamic_return_file_methods!(file_methods, methods)
|
|
397
335
|
return if file_methods.nil?
|
|
398
336
|
|
|
@@ -409,8 +347,8 @@ module Rigor
|
|
|
409
347
|
end
|
|
410
348
|
|
|
411
349
|
def validate_dynamic_return_receivers!(receivers)
|
|
412
|
-
# ADR-52 slice 3 — a run-time callable is resolved per instance
|
|
413
|
-
#
|
|
350
|
+
# ADR-52 slice 3 — a run-time callable is resolved per instance after `#prepare`; its shape is
|
|
351
|
+
# checked at resolution time.
|
|
414
352
|
return if receivers.respond_to?(:call)
|
|
415
353
|
return if receivers.is_a?(Array) && !receivers.empty? && receivers.all? { |r| r.is_a?(String) && !r.empty? }
|
|
416
354
|
|
|
@@ -421,8 +359,8 @@ module Rigor
|
|
|
421
359
|
|
|
422
360
|
def validate_dynamic_return_methods!(methods)
|
|
423
361
|
return if methods.nil?
|
|
424
|
-
# ADR-52 slice 4 — a run-time callable resolves to the name set
|
|
425
|
-
#
|
|
362
|
+
# ADR-52 slice 4 — a run-time callable resolves to the name set per instance after `#prepare`; its
|
|
363
|
+
# shape is checked then.
|
|
426
364
|
return if methods.respond_to?(:call)
|
|
427
365
|
return if methods.is_a?(Array) && !methods.empty? &&
|
|
428
366
|
methods.all? { |m| m.is_a?(Symbol) || (m.is_a?(String) && !m.empty?) }
|
|
@@ -435,30 +373,24 @@ module Rigor
|
|
|
435
373
|
private :validate_dynamic_return_gate!, :validate_dynamic_return_receivers!,
|
|
436
374
|
:validate_dynamic_return_methods!, :validate_dynamic_return_file_methods!
|
|
437
375
|
|
|
438
|
-
# ADR-37 slice 2 — declares a predicate/assertion narrowing
|
|
439
|
-
#
|
|
440
|
-
# `post_return_facts` slot of the deleted `flow_contribution_for`
|
|
441
|
-
# hook (ADR-52 WD3):
|
|
376
|
+
# ADR-37 slice 2 — declares a predicate/assertion narrowing contribution, method-gated. The narrow
|
|
377
|
+
# successor to the `post_return_facts` slot of the deleted `flow_contribution_for` hook (ADR-52 WD3):
|
|
442
378
|
#
|
|
443
379
|
# narrowing_facts methods: [:assert_kind_of] do |call_node, scope|
|
|
444
380
|
# # return an Array of post-return facts, or nil
|
|
445
381
|
# end
|
|
446
382
|
#
|
|
447
|
-
# `methods:` is a non-empty Array of method names; the engine
|
|
448
|
-
#
|
|
449
|
-
# block returns the same `post_return_facts` the merger applies.
|
|
383
|
+
# `methods:` is a non-empty Array of method names; the engine calls the block only when
|
|
384
|
+
# `call_node.name` is one of them. The block returns the same `post_return_facts` the merger applies.
|
|
450
385
|
#
|
|
451
|
-
# This hook supplies post-return *narrowing facts* — the
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
#
|
|
455
|
-
#
|
|
456
|
-
# contribute RBS via the manifest `signature_paths:`.
|
|
457
|
-
# `dynamic_return` is the type slot, this is the fact slot.
|
|
386
|
+
# This hook supplies post-return *narrowing facts* — the predicate/assertion edges a call establishes
|
|
387
|
+
# about its arguments or receiver, e.g. `assert_kind_of(String, x)` ⇒ `x` is narrowed to `String` on
|
|
388
|
+
# the continuation. It does NOT give a call a *type*; for that use {dynamic_return} (per-call-site)
|
|
389
|
+
# or contribute RBS via the manifest `signature_paths:`. `dynamic_return` is the type slot, this is
|
|
390
|
+
# the fact slot.
|
|
458
391
|
#
|
|
459
|
-
# Renamed from `type_specifier` (ADR-80): the old name read as a
|
|
460
|
-
#
|
|
461
|
-
# facts. {.type_specifier} survives as a deprecating alias through
|
|
392
|
+
# Renamed from `type_specifier` (ADR-80): the old name read as a parallel to `dynamic_return` (a
|
|
393
|
+
# type) when it actually returns facts. {.type_specifier} survives as a deprecating alias through
|
|
462
394
|
# 0.2.x and is removed in 0.3.0.
|
|
463
395
|
def narrowing_facts(methods:, &block)
|
|
464
396
|
raise ArgumentError, "Plugin::Base.narrowing_facts requires a block body" if block.nil?
|
|
@@ -474,11 +406,10 @@ module Rigor
|
|
|
474
406
|
nil
|
|
475
407
|
end
|
|
476
408
|
|
|
477
|
-
# DEPRECATED (ADR-80) — renamed to {.narrowing_facts}. This hook
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
# `type_specifier methods: …` → `narrowing_facts methods: …`.
|
|
409
|
+
# DEPRECATED (ADR-80) — renamed to {.narrowing_facts}. This hook supplies post-return narrowing
|
|
410
|
+
# *facts*, not a type; the old name misleads by parallel with {.dynamic_return}. Retained as a
|
|
411
|
+
# warning-emitting alias through 0.2.x; REMOVED in 0.3.0. Migrate `type_specifier methods: …` →
|
|
412
|
+
# `narrowing_facts methods: …`.
|
|
482
413
|
def type_specifier(methods:, &)
|
|
483
414
|
unless @type_specifier_deprecation_warned
|
|
484
415
|
@type_specifier_deprecation_warned = true
|
|
@@ -488,9 +419,9 @@ module Rigor
|
|
|
488
419
|
narrowing_facts(methods:, &)
|
|
489
420
|
end
|
|
490
421
|
|
|
491
|
-
# Frozen snapshot of the declared type-specifier rules. Memoised
|
|
492
|
-
#
|
|
493
|
-
#
|
|
422
|
+
# Frozen snapshot of the declared type-specifier rules. Memoised for the same reason as
|
|
423
|
+
# {dynamic_returns} — consulted per plugin per dispatch, over an array fixed at class-definition
|
|
424
|
+
# time.
|
|
494
425
|
# rubocop:disable Naming/MemoizedInstanceVariableName -- see dynamic_returns
|
|
495
426
|
def type_specifiers
|
|
496
427
|
@type_specifiers_snapshot ||= (@type_specifiers || []).dup.freeze
|
|
@@ -503,43 +434,35 @@ module Rigor
|
|
|
503
434
|
def initialize(services:, config: {})
|
|
504
435
|
@services = services
|
|
505
436
|
@config = merge_config_defaults(config).freeze
|
|
506
|
-
# ADR-52 slice 3 — per-rule cache of resolved run-time
|
|
507
|
-
# `
|
|
508
|
-
#
|
|
509
|
-
# memo-on-first-dispatch is a Hash-content mutation, sound even on
|
|
510
|
-
# a self-freezing plugin.
|
|
437
|
+
# ADR-52 slice 3 — per-rule cache of resolved run-time `dynamic_return receivers:` callables. Created
|
|
438
|
+
# here (before any subclass `initialize` freezes the instance) so the lazy memo-on-first-dispatch is
|
|
439
|
+
# a Hash-content mutation, sound even on a self-freezing plugin.
|
|
511
440
|
@dynamic_return_runtime_cache = {}
|
|
512
|
-
# ADR-60 WD4 — nil-inclusive memo tables for the authoring
|
|
513
|
-
#
|
|
514
|
-
#
|
|
515
|
-
# for the same reason: a populate is a Hash-content mutation.
|
|
441
|
+
# ADR-60 WD4 — nil-inclusive memo tables for the authoring helpers ({#read_fact} / {#producer_value}
|
|
442
|
+
# / {#producer_error}). Allocated here, before any subclass `initialize` self-freeze, for the same
|
|
443
|
+
# reason: a populate is a Hash-content mutation.
|
|
516
444
|
@fact_cache = {}
|
|
517
445
|
@producer_value_cache = {}
|
|
518
446
|
@producer_errors = {}
|
|
519
447
|
end
|
|
520
448
|
|
|
521
|
-
# Override in subclasses to wire any state the plugin needs
|
|
522
|
-
#
|
|
523
|
-
#
|
|
524
|
-
# hooks do not have to define an explicit body.
|
|
449
|
+
# Override in subclasses to wire any state the plugin needs from the injected service container.
|
|
450
|
+
# Default is a no-op so plugins that only contribute through later-slice protocol hooks do not have to
|
|
451
|
+
# define an explicit body.
|
|
525
452
|
def init(services) # rubocop:disable Lint/UnusedMethodArgument
|
|
526
453
|
nil
|
|
527
454
|
end
|
|
528
455
|
|
|
529
|
-
# NOTE: (ADR-52 WD3): the legacy ungated per-call hook
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
#
|
|
534
|
-
|
|
535
|
-
#
|
|
536
|
-
|
|
537
|
-
#
|
|
538
|
-
#
|
|
539
|
-
# per `Analysis::Runner.run`, after `#init` has run on every
|
|
540
|
-
# plugin and before any `#diagnostics_for_file` call.
|
|
541
|
-
# Plugins use this hook to compute and publish facts other
|
|
542
|
-
# plugins consume:
|
|
456
|
+
# NOTE: (ADR-52 WD3): the legacy ungated per-call hook `flow_contribution_for` was DELETED here pre-1.0
|
|
457
|
+
# after its five production users migrated. Per-call return types are declared via the gated
|
|
458
|
+
# {.dynamic_return} DSL (static / run-time / per-file name sets, static / run-time receiver sets);
|
|
459
|
+
# post-return narrowing facts via {.narrowing_facts}. See the CHANGELOG migration note for the
|
|
460
|
+
# idiom-by-idiom mapping.
|
|
461
|
+
|
|
462
|
+
# ADR-9 slice 3 — per-run preparation hook. The runner invokes `#prepare(services)` on every loaded
|
|
463
|
+
# plugin once per `Analysis::Runner.run`, after `#init` has run on every plugin and before any
|
|
464
|
+
# `#diagnostics_for_file` call. Plugins use this hook to compute and publish facts other plugins
|
|
465
|
+
# consume:
|
|
543
466
|
#
|
|
544
467
|
# def prepare(services)
|
|
545
468
|
# services.fact_store.publish(
|
|
@@ -547,72 +470,56 @@ module Rigor
|
|
|
547
470
|
# )
|
|
548
471
|
# end
|
|
549
472
|
#
|
|
550
|
-
# Default no-op so plugins without facts to publish leave
|
|
551
|
-
# `#prepare`
|
|
552
|
-
#
|
|
553
|
-
# raises in `#prepare` has its facts considered un-published
|
|
554
|
-
# and downstream consumers see `nil` from `fact_store.read`.
|
|
473
|
+
# Default no-op so plugins without facts to publish leave `#prepare` unimplemented. Failures isolate as
|
|
474
|
+
# `:plugin_loader runtime-error` diagnostics; a plugin that raises in `#prepare` has its facts
|
|
475
|
+
# considered un-published and downstream consumers see `nil` from `fact_store.read`.
|
|
555
476
|
#
|
|
556
|
-
# Slice 3 calls plugins in registration order. ADR-9 slice 5
|
|
557
|
-
#
|
|
558
|
-
# always run before consumers.
|
|
477
|
+
# Slice 3 calls plugins in registration order. ADR-9 slice 5 introduces topological ordering by
|
|
478
|
+
# `consumes:` so producers always run before consumers.
|
|
559
479
|
def prepare(services) # rubocop:disable Lint/UnusedMethodArgument
|
|
560
480
|
nil
|
|
561
481
|
end
|
|
562
482
|
|
|
563
|
-
# ADR-7 § "Slice 5-A" — per-file diagnostic emission hook.
|
|
564
|
-
#
|
|
565
|
-
# `
|
|
566
|
-
#
|
|
567
|
-
#
|
|
568
|
-
# ADR-7 § "Slice 5-B"; plugin authors should construct
|
|
569
|
-
# diagnostics without setting `source_family` (any value
|
|
570
|
-
# they pass is overwritten).
|
|
483
|
+
# ADR-7 § "Slice 5-A" — per-file diagnostic emission hook. Override in plugin subclasses to return an
|
|
484
|
+
# array of `Rigor::Analysis::Diagnostic` rows for the analysed file. The runner stamps each returned
|
|
485
|
+
# diagnostic with `source_family: "plugin.<manifest.id>"` automatically per ADR-7 § "Slice 5-B"; plugin
|
|
486
|
+
# authors should construct diagnostics without setting `source_family` (any value they pass is
|
|
487
|
+
# overwritten).
|
|
571
488
|
#
|
|
572
|
-
# `path` is the analysed file path; `scope` is the entry
|
|
573
|
-
# `
|
|
574
|
-
#
|
|
575
|
-
#
|
|
576
|
-
# (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base
|
|
577
|
-
# class mentioned in ADR-2 was superseded by the block-based
|
|
578
|
-
# `.node_rule` DSL.
|
|
489
|
+
# `path` is the analysed file path; `scope` is the entry `Rigor::Scope` after `ScopeIndexer` ran; `root`
|
|
490
|
+
# is the parsed `Prism::Node` root. Plugin authors can traverse `root` themselves or declare rules via
|
|
491
|
+
# the `.node_rule` DSL (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base class mentioned in ADR-2
|
|
492
|
+
# was superseded by the block-based `.node_rule` DSL.
|
|
579
493
|
#
|
|
580
|
-
# Default returns `[]` so plugins that contribute through
|
|
581
|
-
#
|
|
582
|
-
# slice-6 cache producers) do not have to override.
|
|
494
|
+
# Default returns `[]` so plugins that contribute through other channels (e.g. slice-4 narrowing
|
|
495
|
+
# contributions, slice-6 cache producers) do not have to override.
|
|
583
496
|
def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
|
|
584
497
|
[]
|
|
585
498
|
end
|
|
586
499
|
|
|
587
|
-
# ADR-37 slice 1 — runs the plugin's declared {.node_rule}s over
|
|
588
|
-
#
|
|
589
|
-
#
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
# block is `instance_exec`'d on this plugin instance with
|
|
593
|
-
# `(node, scope, path)`, and the returned diagnostics are
|
|
594
|
-
# concatenated in (node, declaration) order.
|
|
500
|
+
# ADR-37 slice 1 — runs the plugin's declared {.node_rule}s over one file and returns their
|
|
501
|
+
# diagnostics. The engine owns the single AST walk here so plugin authors never hand-roll a traversal:
|
|
502
|
+
# every node reachable from `root` is offered to each rule whose `node_type` it satisfies
|
|
503
|
+
# (`node.is_a?`), the rule's block is `instance_exec`'d on this plugin instance with
|
|
504
|
+
# `(node, scope, path)`, and the returned diagnostics are concatenated in (node, declaration) order.
|
|
595
505
|
#
|
|
596
|
-
# Returns `[]` immediately when the plugin declares no node rules,
|
|
597
|
-
#
|
|
598
|
-
# the
|
|
599
|
-
#
|
|
600
|
-
# propagates to the runner's per-plugin isolation boundary.
|
|
506
|
+
# Returns `[]` immediately when the plugin declares no node rules, so it is a zero-cost no-op for every
|
|
507
|
+
# plugin that does not use the DSL. The runner calls it alongside `#diagnostics_for_file` and stamps
|
|
508
|
+
# `plugin.<id>` provenance on the result; a raise propagates to the runner's per-plugin isolation
|
|
509
|
+
# boundary.
|
|
601
510
|
def node_rule_diagnostics(path:, scope:, root:)
|
|
602
511
|
rules = self.class.node_rules
|
|
603
512
|
return [] if rules.empty? || root.nil?
|
|
604
513
|
|
|
605
|
-
# ADR-37 slice 1c — build the per-file context once (the
|
|
606
|
-
#
|
|
607
|
-
# two-pass plugin sees the closed namespace at every node.
|
|
514
|
+
# ADR-37 slice 1c — build the per-file context once (the "collect" pass) before the engine-owned
|
|
515
|
+
# validate walk, so a two-pass plugin sees the closed namespace at every node.
|
|
608
516
|
context_block = self.class.node_file_context_block
|
|
609
517
|
file_context = context_block ? instance_exec(root, scope, &context_block) : nil
|
|
610
518
|
|
|
611
519
|
diagnostics = []
|
|
612
520
|
Source::NodeWalker.each_with_ancestors(root) do |node, ancestors|
|
|
613
|
-
# One frozen NodeContext per node, shared across the rules
|
|
614
|
-
#
|
|
615
|
-
# nodes (the vast majority) allocate nothing.
|
|
521
|
+
# One frozen NodeContext per node, shared across the rules that match it (ADR-52 WD1) — built
|
|
522
|
+
# lazily so non-matching nodes (the vast majority) allocate nothing.
|
|
616
523
|
context = nil
|
|
617
524
|
rules.each do |rule|
|
|
618
525
|
next unless node.is_a?(rule[:node_type])
|
|
@@ -624,20 +531,17 @@ module Rigor
|
|
|
624
531
|
diagnostics
|
|
625
532
|
end
|
|
626
533
|
|
|
627
|
-
# ADR-37 slice 2 — the return type contributed by this plugin's
|
|
628
|
-
#
|
|
629
|
-
# from `
|
|
630
|
-
#
|
|
631
|
-
# First non-nil wins (declaration order). Failures isolate to nil.
|
|
534
|
+
# ADR-37 slice 2 — the return type contributed by this plugin's {.dynamic_return} rules for a call, or
|
|
535
|
+
# nil. The engine calls this from `MethodDispatcher`; a rule fires only when `receiver_type`'s class
|
|
536
|
+
# equals or inherits from one of its declared `receivers:`. First non-nil wins (declaration order).
|
|
537
|
+
# Failures isolate to nil.
|
|
632
538
|
def dynamic_return_type(call_node:, scope:, receiver_type:)
|
|
633
539
|
rules = self.class.dynamic_returns
|
|
634
540
|
return nil if rules.empty? || receiver_type.nil?
|
|
635
541
|
|
|
636
|
-
# `class_name` is nil for a receiver carrier with no nominal
|
|
637
|
-
#
|
|
638
|
-
#
|
|
639
|
-
# the method name alone and reads the receiver shape inside its
|
|
640
|
-
# own block.
|
|
542
|
+
# `class_name` is nil for a receiver carrier with no nominal class (a refinement dimension, an
|
|
543
|
+
# inferred shape) — fine for a receiver-less (methods-only) rule (ADR-52 WD2), which gates on the
|
|
544
|
+
# method name alone and reads the receiver shape inside its own block.
|
|
641
545
|
class_name = dynamic_return_receiver_class_name(receiver_type)
|
|
642
546
|
environment = scope&.environment
|
|
643
547
|
rules.each do |rule|
|
|
@@ -651,11 +555,9 @@ module Rigor
|
|
|
651
555
|
nil
|
|
652
556
|
end
|
|
653
557
|
|
|
654
|
-
# ADR-37 slice 2 — the post-return narrowing facts contributed by
|
|
655
|
-
#
|
|
656
|
-
#
|
|
657
|
-
# `call_node.name`
|
|
658
|
-
# is one of its declared `methods:`. Failures isolate to [].
|
|
558
|
+
# ADR-37 slice 2 — the post-return narrowing facts contributed by this plugin's {.narrowing_facts}
|
|
559
|
+
# rules for a call. The engine calls this from `StatementEvaluator`; a rule fires only when
|
|
560
|
+
# `call_node.name` is one of its declared `methods:`. Failures isolate to [].
|
|
659
561
|
def type_specifier_facts(call_node:, scope:)
|
|
660
562
|
rules = self.class.type_specifiers
|
|
661
563
|
return [] if rules.empty? || !call_node.respond_to?(:name)
|
|
@@ -673,23 +575,17 @@ module Rigor
|
|
|
673
575
|
[]
|
|
674
576
|
end
|
|
675
577
|
|
|
676
|
-
# Builds a `Rigor::Analysis::Diagnostic` positioned at a Prism
|
|
677
|
-
# `
|
|
678
|
-
#
|
|
679
|
-
#
|
|
680
|
-
# message/severity/rule rather than unpacking `node.location`.
|
|
578
|
+
# Builds a `Rigor::Analysis::Diagnostic` positioned at a Prism `node` for return from
|
|
579
|
+
# `#diagnostics_for_file`. Internalises the 1-based `line` / `start_column + 1` convention every plugin
|
|
580
|
+
# otherwise re-derives by hand, so authors pass the node and the message/severity/rule rather than
|
|
581
|
+
# unpacking `node.location`.
|
|
681
582
|
#
|
|
682
|
-
# `source_family` is intentionally NOT accepted — the runner
|
|
683
|
-
#
|
|
684
|
-
# (
|
|
685
|
-
#
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
# `node.message_loc` so a matcher / method-name diagnostic points
|
|
689
|
-
# at the name, not the receiver-spanning whole call. A `nil`
|
|
690
|
-
# `location:` falls back to `node.location`, so
|
|
691
|
-
# `location: node.message_loc` reproduces the common
|
|
692
|
-
# `message_loc || location` idiom.
|
|
583
|
+
# `source_family` is intentionally NOT accepted — the runner stamps `plugin.<manifest.id>` on every
|
|
584
|
+
# returned diagnostic (ADR-7 § "Slice 5-B"), so any value set here would be overwritten.
|
|
585
|
+
# Pass `location:` (a Prism location) to point the diagnostic at a sub-location of `node` rather than
|
|
586
|
+
# `node.location` — typically `node.message_loc` so a matcher / method-name diagnostic points at the
|
|
587
|
+
# name, not the receiver-spanning whole call. A `nil` `location:` falls back to `node.location`, so
|
|
588
|
+
# `location: node.message_loc` reproduces the common `message_loc || location` idiom.
|
|
693
589
|
def diagnostic(node, path:, message:, severity: :error, rule: nil, location: nil)
|
|
694
590
|
Analysis::Diagnostic.from_location(
|
|
695
591
|
location || node.location,
|
|
@@ -697,16 +593,13 @@ module Rigor
|
|
|
697
593
|
)
|
|
698
594
|
end
|
|
699
595
|
|
|
700
|
-
# ADR-60 WD4 — maps a plugin's own violation objects to
|
|
701
|
-
#
|
|
702
|
-
#
|
|
703
|
-
# node
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
707
|
-
# sub-location such as `node.message_loc`), `#severity` (defaults
|
|
708
|
-
# `:error`), and `#rule`. Returns an Array suitable for direct
|
|
709
|
-
# return from `#diagnostics_for_file` / a `node_rule` block.
|
|
596
|
+
# ADR-60 WD4 — maps a plugin's own violation objects to `Rigor::Analysis::Diagnostic`s through
|
|
597
|
+
# {#diagnostic}, absorbing the `violations.map { |v| diagnostic(node, …) }` block the node-rule plugins
|
|
598
|
+
# otherwise repeat. Each violation duck-types: `#message` (required); optional `#node` (the Prism node
|
|
599
|
+
# to position at — falls back to the `node:` argument, the common "all violations point at the same
|
|
600
|
+
# call" case), `#location` (a sub-location such as `node.message_loc`), `#severity` (defaults
|
|
601
|
+
# `:error`), and `#rule`. Returns an Array suitable for direct return from `#diagnostics_for_file` / a
|
|
602
|
+
# `node_rule` block.
|
|
710
603
|
def diagnostics_for(violations, path:, node: nil)
|
|
711
604
|
Array(violations).map do |violation|
|
|
712
605
|
target = (violation.node if violation.respond_to?(:node)) || node
|
|
@@ -721,13 +614,11 @@ module Rigor
|
|
|
721
614
|
end
|
|
722
615
|
end
|
|
723
616
|
|
|
724
|
-
# ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by
|
|
725
|
-
#
|
|
726
|
-
#
|
|
727
|
-
#
|
|
728
|
-
#
|
|
729
|
-
# read". `services.fact_store` is the only sanctioned cross-plugin
|
|
730
|
-
# channel; a fact no loaded producer published reads as nil.
|
|
617
|
+
# ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by another plugin's `#prepare` hook,
|
|
618
|
+
# memoised per `(plugin_id, name)` on this instance INCLUDING a nil result. The nil-inclusive memo
|
|
619
|
+
# retires the hand-rolled `@x_resolved` flag the discovery plugins carried to distinguish "fact not
|
|
620
|
+
# published" from "not yet read". `services.fact_store` is the only sanctioned cross-plugin channel; a
|
|
621
|
+
# fact no loaded producer published reads as nil.
|
|
731
622
|
def read_fact(plugin_id:, name:)
|
|
732
623
|
key = [plugin_id.to_s, name.to_sym].freeze
|
|
733
624
|
return @fact_cache[key] if @fact_cache.key?(key)
|
|
@@ -735,13 +626,11 @@ module Rigor
|
|
|
735
626
|
@fact_cache[key] = services.fact_store.read(plugin_id: plugin_id.to_s, name: name.to_sym)
|
|
736
627
|
end
|
|
737
628
|
|
|
738
|
-
# ADR-60 WD4 — runs a declared {.producer} through {#cache_for}
|
|
739
|
-
#
|
|
740
|
-
#
|
|
741
|
-
#
|
|
742
|
-
#
|
|
743
|
-
# rather than aborting the whole run. This is the `*_index_or_nil`
|
|
744
|
-
# shape the discovery plugins hand-rolled, named once.
|
|
629
|
+
# ADR-60 WD4 — runs a declared {.producer} through {#cache_for} and returns its value, memoised per
|
|
630
|
+
# `(id, params)` INCLUDING nil. A `StandardError` the producer raises (a malformed project file, an I/O
|
|
631
|
+
# failure) is rescued, recorded for {#producer_error}, and yields nil — so one bad project file
|
|
632
|
+
# degrades a plugin to silence rather than aborting the whole run. This is the `*_index_or_nil` shape
|
|
633
|
+
# the discovery plugins hand-rolled, named once.
|
|
745
634
|
def producer_value(id, params: {})
|
|
746
635
|
key = [id.to_sym, params].freeze
|
|
747
636
|
return @producer_value_cache[key] if @producer_value_cache.key?(key)
|
|
@@ -752,23 +641,19 @@ module Rigor
|
|
|
752
641
|
@producer_value_cache[key] = nil
|
|
753
642
|
end
|
|
754
643
|
|
|
755
|
-
# ADR-60 WD4 — the `StandardError` a prior {#producer_value} call
|
|
756
|
-
#
|
|
757
|
-
# Plugins surface it as a load-error diagnostic from
|
|
644
|
+
# ADR-60 WD4 — the `StandardError` a prior {#producer_value} call rescued for `id`, or nil when it
|
|
645
|
+
# succeeded or was never called. Plugins surface it as a load-error diagnostic from
|
|
758
646
|
# `#diagnostics_for_file`.
|
|
759
647
|
def producer_error(id)
|
|
760
648
|
@producer_errors[id.to_sym]
|
|
761
649
|
end
|
|
762
650
|
|
|
763
|
-
# Boilerplate-reduction helper (review §1.3): the "did you mean …?"
|
|
764
|
-
#
|
|
765
|
-
#
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
#
|
|
769
|
-
# **class** method so it is callable both from a plugin instance
|
|
770
|
-
# (`Rigor::Plugin::Base.suggest(...)`) and from an `Analyzer` module
|
|
771
|
-
# function that has no instance.
|
|
651
|
+
# Boilerplate-reduction helper (review §1.3): the "did you mean …?" suggestion every
|
|
652
|
+
# diagnostic-emitting plugin otherwise hand-rolls. Returns the closest of `candidates` to `name` via
|
|
653
|
+
# `DidYouMean::SpellChecker` (the same engine Ruby's own `NoMethodError` hints use), or `nil` when
|
|
654
|
+
# there is no good match / no candidates — replacing the per-plugin Levenshtein copies. A **class**
|
|
655
|
+
# method so it is callable both from a plugin instance (`Rigor::Plugin::Base.suggest(...)`) and from an
|
|
656
|
+
# `Analyzer` module function that has no instance.
|
|
772
657
|
def self.suggest(name, candidates)
|
|
773
658
|
dictionary = Array(candidates).map(&:to_s)
|
|
774
659
|
return nil if dictionary.empty?
|
|
@@ -776,23 +661,18 @@ module Rigor
|
|
|
776
661
|
DidYouMean::SpellChecker.new(dictionary: dictionary).correct(name.to_s).first
|
|
777
662
|
end
|
|
778
663
|
|
|
779
|
-
# Convenience accessor — `manifest` on the instance returns
|
|
780
|
-
# the class-level manifest declaration.
|
|
664
|
+
# Convenience accessor — `manifest` on the instance returns the class-level manifest declaration.
|
|
781
665
|
def manifest
|
|
782
666
|
self.class.manifest
|
|
783
667
|
end
|
|
784
668
|
|
|
785
|
-
# ADR-25 — absolute RBS signature directories this plugin
|
|
786
|
-
#
|
|
787
|
-
#
|
|
788
|
-
#
|
|
789
|
-
#
|
|
790
|
-
#
|
|
791
|
-
#
|
|
792
|
-
# the class is anonymous (an anonymous class cannot ship a
|
|
793
|
-
# gem). `Plugin::Loader` validates the resolved dirs exist at
|
|
794
|
-
# load time; `Environment.for_project` merges them into the
|
|
795
|
-
# signature-path set fed to `RbsLoader`.
|
|
669
|
+
# ADR-25 — absolute RBS signature directories this plugin contributes. Resolves each
|
|
670
|
+
# `manifest.signature_paths` entry (declared relative to the plugin gem root) against that root. The
|
|
671
|
+
# gem root is the directory above `lib/` in the file that defined the plugin class (falling back to
|
|
672
|
+
# that file's directory for a non-conventional layout). Returns `[]` when the manifest declares no
|
|
673
|
+
# `signature_paths:` or the class is anonymous (an anonymous class cannot ship a gem).
|
|
674
|
+
# `Plugin::Loader` validates the resolved dirs exist at load time; `Environment.for_project` merges
|
|
675
|
+
# them into the signature-path set fed to `RbsLoader`.
|
|
796
676
|
def signature_paths
|
|
797
677
|
relative = manifest.signature_paths
|
|
798
678
|
return [] if relative.empty?
|
|
@@ -808,60 +688,42 @@ module Rigor
|
|
|
808
688
|
relative.map { |rel| File.expand_path(rel, root) }
|
|
809
689
|
end
|
|
810
690
|
|
|
811
|
-
# ADR-28 — the path-scoped method-protocol contracts this
|
|
812
|
-
#
|
|
813
|
-
#
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
# substituting the convention `path_glob` with a user-supplied
|
|
817
|
-
# one) without the manifest having to be config-aware.
|
|
818
|
-
# `Plugin::Registry#protocol_contracts` aggregates the result
|
|
819
|
-
# across loaded plugins.
|
|
691
|
+
# ADR-28 — the path-scoped method-protocol contracts this plugin contributes. Defaults to the
|
|
692
|
+
# manifest-declared `protocol_contracts:`; the same indirection `#signature_paths` uses, so a plugin
|
|
693
|
+
# MAY override this to fold per-project config into the contract set (e.g. substituting the convention
|
|
694
|
+
# `path_glob` with a user-supplied one) without the manifest having to be config-aware.
|
|
695
|
+
# `Plugin::Registry#protocol_contracts` aggregates the result across loaded plugins.
|
|
820
696
|
def protocol_contracts
|
|
821
697
|
manifest.protocol_contracts
|
|
822
698
|
end
|
|
823
699
|
|
|
824
|
-
# ADR-7 § "Slice 6-A/6-B" — per-plugin {IoBoundary}.
|
|
825
|
-
#
|
|
826
|
-
# rows persist across producer invocations within the
|
|
827
|
-
# same plugin instance and feed cache invalidation
|
|
700
|
+
# ADR-7 § "Slice 6-A/6-B" — per-plugin {IoBoundary}. Memoised so the boundary's accumulated `FileEntry`
|
|
701
|
+
# rows persist across producer invocations within the same plugin instance and feed cache invalidation
|
|
828
702
|
# via `cache_for`.
|
|
829
703
|
def io_boundary
|
|
830
704
|
@io_boundary ||= services.io_boundary_for(manifest.id)
|
|
831
705
|
end
|
|
832
706
|
|
|
833
|
-
# ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that
|
|
834
|
-
#
|
|
835
|
-
# the
|
|
836
|
-
#
|
|
837
|
-
#
|
|
838
|
-
#
|
|
839
|
-
#
|
|
840
|
-
#
|
|
841
|
-
#
|
|
842
|
-
# {Cache::Descriptor::GlobEntry} rows. In-block reads are
|
|
843
|
-
# therefore always captured (the structural stale-cache
|
|
844
|
-
# hazard `fetch_or_compute`'s call-time snapshot carried);
|
|
845
|
-
# the next run re-validates the recorded dependencies by
|
|
846
|
-
# re-digest (`Descriptor#fresh?`) and recomputes when any
|
|
847
|
-
# changed. The producer id is auto-prefixed
|
|
848
|
-
# `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
|
|
707
|
+
# ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that performs a `Cache::Store#fetch_or_validate`
|
|
708
|
+
# round-trip for the named producer (the ADR-45 record-and-validate path). The entry is KEYED on the
|
|
709
|
+
# stable identity inputs — the plugin's `PluginEntry` template (id, version, config_hash) composed with
|
|
710
|
+
# the optional `descriptor:` extras — and stores, beside the value, a DEPENDENCY descriptor recorded
|
|
711
|
+
# AFTER the producer block ran: the {IoBoundary}'s post-compute read history plus the evaluated
|
|
712
|
+
# `watch:` {Cache::Descriptor::GlobEntry} rows. In-block reads are therefore always captured (the
|
|
713
|
+
# structural stale-cache hazard `fetch_or_compute`'s call-time snapshot carried); the next run
|
|
714
|
+
# re-validates the recorded dependencies by re-digest (`Descriptor#fresh?`) and recomputes when any
|
|
715
|
+
# changed. The producer id is auto-prefixed `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
|
|
849
716
|
# caches stay sandboxed from built-in producers.
|
|
850
717
|
#
|
|
851
|
-
# When `services.cache_store` is `nil` (e.g. CLI
|
|
852
|
-
#
|
|
853
|
-
#
|
|
854
|
-
# as the v0.0.9 cache surface for built-in producers.
|
|
718
|
+
# When `services.cache_store` is `nil` (e.g. CLI `--no-cache`), the callable bypasses the cache and
|
|
719
|
+
# runs the producer block every time — same semantics as the v0.0.9 cache surface for built-in
|
|
720
|
+
# producers.
|
|
855
721
|
#
|
|
856
|
-
# `descriptor:` (optional) supplies extra `Cache::Descriptor`
|
|
857
|
-
# rows for
|
|
858
|
-
# `
|
|
859
|
-
#
|
|
860
|
-
#
|
|
861
|
-
# divergent inputs visible rather than silently shadowing.
|
|
862
|
-
# A key change is a miss, so the invalidation effect of the
|
|
863
|
-
# legacy `glob_descriptor`-as-`descriptor:` idiom is
|
|
864
|
-
# preserved unchanged.
|
|
722
|
+
# `descriptor:` (optional) supplies extra `Cache::Descriptor` rows for IDENTITY inputs — gem-version
|
|
723
|
+
# `GemEntry` pins, `ConfigEntry` rows for external state — that compose into the cache KEY via
|
|
724
|
+
# `Cache::Descriptor.compose`; per-slot conflicts raise `Cache::Descriptor::Conflict` to make divergent
|
|
725
|
+
# inputs visible rather than silently shadowing. A key change is a miss, so the invalidation effect of
|
|
726
|
+
# the legacy `glob_descriptor`-as-`descriptor:` idiom is preserved unchanged.
|
|
865
727
|
def cache_for(producer_id, params: {}, descriptor: nil)
|
|
866
728
|
producer = self.class.producers[producer_id.to_sym]
|
|
867
729
|
unless producer
|
|
@@ -889,28 +751,20 @@ module Rigor
|
|
|
889
751
|
end
|
|
890
752
|
end
|
|
891
753
|
|
|
892
|
-
# Builds a `Cache::Descriptor` covering every file matched by
|
|
893
|
-
# `
|
|
894
|
-
#
|
|
895
|
-
#
|
|
896
|
-
# any addition (a newly-glob-matched file appears in the
|
|
897
|
-
# descriptor), or any removal (the previously-matched file
|
|
898
|
-
# drops out).
|
|
754
|
+
# Builds a `Cache::Descriptor` covering every file matched by `pattern` (a glob, e.g. `"**/*.rb"`)
|
|
755
|
+
# under any of `roots`. Each matching file contributes a `:digest`-comparator `FileEntry` so the cache
|
|
756
|
+
# invalidates on any content change, any addition (a newly-glob-matched file appears in the
|
|
757
|
+
# descriptor), or any removal (the previously-matched file drops out).
|
|
899
758
|
#
|
|
900
|
-
# ADR-60 WD3 made this **private**: the declared way for a
|
|
901
|
-
#
|
|
902
|
-
#
|
|
903
|
-
#
|
|
904
|
-
#
|
|
905
|
-
# only as the building block for the rare producer that needs
|
|
906
|
-
# `FileEntry` rows directly; plugin code calls `watch:`.
|
|
759
|
+
# ADR-60 WD3 made this **private**: the declared way for a discovery-style producer to cover its glob
|
|
760
|
+
# is `producer watch:` (one {Cache::Descriptor::GlobEntry} per glob in the record-and-validate
|
|
761
|
+
# dependency descriptor), not a hand-built descriptor composed into the cache *key*. The method
|
|
762
|
+
# survives only as the building block for the rare producer that needs `FileEntry` rows directly;
|
|
763
|
+
# plugin code calls `watch:`.
|
|
907
764
|
#
|
|
908
|
-
# @param roots [Array<String>] search roots (relative to
|
|
909
|
-
#
|
|
910
|
-
#
|
|
911
|
-
# each root via `File.join(root, pattern)`. Multiple
|
|
912
|
-
# patterns union into one descriptor (`"**/*.erb",
|
|
913
|
-
# "**/*.html"` etc.).
|
|
765
|
+
# @param roots [Array<String>] search roots (relative to the project root, or absolute paths)
|
|
766
|
+
# @param patterns [Array<String>] glob suffixes joined under each root via `File.join(root, pattern)`.
|
|
767
|
+
# Multiple patterns union into one descriptor (`"**/*.erb", "**/*.html"` etc.).
|
|
914
768
|
# @return [Rigor::Cache::Descriptor]
|
|
915
769
|
def glob_descriptor(roots, *patterns)
|
|
916
770
|
files = collect_glob_files(Array(roots), patterns)
|
|
@@ -927,11 +781,10 @@ module Rigor
|
|
|
927
781
|
|
|
928
782
|
private
|
|
929
783
|
|
|
930
|
-
# ADR-40 — merge the manifest's declared `config_schema`
|
|
931
|
-
# `
|
|
932
|
-
#
|
|
933
|
-
#
|
|
934
|
-
# manifest (test doubles) keeps the raw config unchanged.
|
|
784
|
+
# ADR-40 — merge the manifest's declared `config_schema` `default:` values *under* the user-supplied
|
|
785
|
+
# config (user wins), so a plugin reads `config.fetch("key")` and gets the declared default with no
|
|
786
|
+
# `DEFAULT_*` constant. A class declared without a manifest (test doubles) keeps the raw config
|
|
787
|
+
# unchanged.
|
|
935
788
|
def merge_config_defaults(config)
|
|
936
789
|
unless self.class.instance_variable_defined?(:@manifest) && self.class.instance_variable_get(:@manifest)
|
|
937
790
|
return config
|
|
@@ -940,28 +793,24 @@ module Rigor
|
|
|
940
793
|
self.class.manifest.config_defaults.merge(config)
|
|
941
794
|
end
|
|
942
795
|
|
|
943
|
-
# ADR-37 slice 2 — the class name to match a `dynamic_return`
|
|
944
|
-
# `
|
|
945
|
-
#
|
|
946
|
-
# carriers decline (nil → no match).
|
|
796
|
+
# ADR-37 slice 2 — the class name to match a `dynamic_return` `receivers:` entry against, from a
|
|
797
|
+
# receiver `Type`. Covers the instance (`Nominal[X]`) and class (`Singleton[X]`) shapes; other carriers
|
|
798
|
+
# decline (nil → no match).
|
|
947
799
|
def dynamic_return_receiver_class_name(receiver_type)
|
|
948
800
|
case receiver_type
|
|
949
801
|
when Rigor::Type::Nominal, Rigor::Type::Singleton then receiver_type.class_name
|
|
950
802
|
end
|
|
951
803
|
end
|
|
952
804
|
|
|
953
|
-
# The gate for one `dynamic_return` rule. Method-name gate first —
|
|
954
|
-
#
|
|
955
|
-
# (ADR-52
|
|
956
|
-
# cost. A receiver-less rule (ADR-52 WD2) skips the ancestry check
|
|
957
|
-
# entirely and fires on the method name alone.
|
|
805
|
+
# The gate for one `dynamic_return` rule. Method-name gate first — a Symbol-array probe vs the receiver
|
|
806
|
+
# ancestry resolution below (ADR-52 WD1); both are pure predicates, so order only affects cost. A
|
|
807
|
+
# receiver-less rule (ADR-52 WD2) skips the ancestry check entirely and fires on the method name alone.
|
|
958
808
|
def dynamic_return_rule_applies?(rule, call_node, class_name, environment, scope)
|
|
959
809
|
return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(call_node.name)
|
|
960
810
|
|
|
961
811
|
if rule[:file_methods]
|
|
962
|
-
# The path is read here, not in `dynamic_return_type`, so a
|
|
963
|
-
#
|
|
964
|
-
# `file_methods:` rules (other gate forms never touch it).
|
|
812
|
+
# The path is read here, not in `dynamic_return_type`, so a spec-double scope without
|
|
813
|
+
# `source_path` only affects `file_methods:` rules (other gate forms never touch it).
|
|
965
814
|
path = scope.respond_to?(:source_path) ? scope.source_path : nil
|
|
966
815
|
return false unless resolved_dynamic_return_file_methods(rule, path).include?(call_node.name)
|
|
967
816
|
end
|
|
@@ -973,12 +822,10 @@ module Rigor
|
|
|
973
822
|
receivers.any? { |c| class_matches_receiver?(class_name, c, environment) }
|
|
974
823
|
end
|
|
975
824
|
|
|
976
|
-
# ADR-52 slice 4 — the rule's method-name set. A static Array is
|
|
977
|
-
#
|
|
978
|
-
# `
|
|
979
|
-
#
|
|
980
|
-
# cache key is namespaced so a rule that makes both `methods:` and
|
|
981
|
-
# `receivers:` callable keeps two distinct memo slots.
|
|
825
|
+
# ADR-52 slice 4 — the rule's method-name set. A static Array is returned as-is (`#include?` over
|
|
826
|
+
# Symbols); a run-time callable is `instance_exec`'d against this plugin and memoised as a Symbol Set,
|
|
827
|
+
# same lazy/idempotent contract as a callable `receivers:`. The cache key is namespaced so a rule that
|
|
828
|
+
# makes both `methods:` and `receivers:` callable keeps two distinct memo slots.
|
|
982
829
|
def resolved_dynamic_return_methods(rule)
|
|
983
830
|
methods = rule[:methods]
|
|
984
831
|
return methods unless methods.respond_to?(:call)
|
|
@@ -987,15 +834,12 @@ module Rigor
|
|
|
987
834
|
Array(instance_exec(&methods)).to_set(&:to_sym).freeze
|
|
988
835
|
end
|
|
989
836
|
|
|
990
|
-
# ADR-52 slice 5a — the rule's per-file method-name set. The
|
|
991
|
-
# `
|
|
992
|
-
#
|
|
993
|
-
# file
|
|
994
|
-
#
|
|
995
|
-
#
|
|
996
|
-
# declines — fail-closed, consistent with the gate's purpose. A
|
|
997
|
-
# raising callable degrades to "declines this dispatch" via
|
|
998
|
-
# `dynamic_return_type`'s surrounding rescue.
|
|
837
|
+
# ADR-52 slice 5a — the rule's per-file method-name set. The `file_methods:` callable is
|
|
838
|
+
# `instance_exec`'d with the file path and memoised per `(rule, path)` — one resolution per analysed
|
|
839
|
+
# file, the per-file analogue of the run-wide `methods:` memo. A nil path (synthetic call sites with no
|
|
840
|
+
# file context) resolves to the empty set: the gate has nothing to key on, so the rule declines —
|
|
841
|
+
# fail-closed, consistent with the gate's purpose. A raising callable degrades to "declines this
|
|
842
|
+
# dispatch" via `dynamic_return_type`'s surrounding rescue.
|
|
999
843
|
EMPTY_NAME_SET = Set.new.freeze
|
|
1000
844
|
private_constant :EMPTY_NAME_SET
|
|
1001
845
|
|
|
@@ -1006,30 +850,26 @@ module Rigor
|
|
|
1006
850
|
Array(instance_exec(path, &rule[:file_methods])).to_set(&:to_sym).freeze
|
|
1007
851
|
end
|
|
1008
852
|
|
|
1009
|
-
# ADR-52 slice 3 — the rule's receiver class-name Array. A static
|
|
1010
|
-
#
|
|
1011
|
-
#
|
|
1012
|
-
#
|
|
1013
|
-
#
|
|
1014
|
-
#
|
|
1015
|
-
# callable that raises degrades to "no receivers match" (the rule
|
|
1016
|
-
# declines), never a crash, consistent with the surrounding rescue.
|
|
853
|
+
# ADR-52 slice 3 — the rule's receiver class-name Array. A static Array is returned as-is; a run-time
|
|
854
|
+
# callable is `instance_exec`'d against this plugin (so it reads the `#prepare`-built indexes) and
|
|
855
|
+
# memoised per rule for the run. Resolution is lazy — first reached during file analysis, always after
|
|
856
|
+
# `#prepare` — and the callable is required to be idempotent, so the memoised set is stable. A callable
|
|
857
|
+
# that raises degrades to "no receivers match" (the rule declines), never a crash, consistent with the
|
|
858
|
+
# surrounding rescue.
|
|
1017
859
|
def resolved_dynamic_return_receivers(rule)
|
|
1018
860
|
receivers = rule[:receivers]
|
|
1019
861
|
return receivers unless receivers.respond_to?(:call)
|
|
1020
862
|
|
|
1021
|
-
# `||= {}` keeps the path correct even when a caller bypassed
|
|
1022
|
-
#
|
|
1023
|
-
#
|
|
1024
|
-
# `initialize`, so the `||=` is a no-op there (never a FrozenError).
|
|
863
|
+
# `||= {}` keeps the path correct even when a caller bypassed `initialize` (`allocate` in unit specs
|
|
864
|
+
# that inject a fake index); a self-freezing plugin already has the Hash from `initialize`, so the
|
|
865
|
+
# `||=` is a no-op there (never a FrozenError).
|
|
1025
866
|
(@dynamic_return_runtime_cache ||= {})[rule] ||=
|
|
1026
867
|
Array(instance_exec(&receivers)).map { |c| c.to_s.dup.freeze }.freeze
|
|
1027
868
|
end
|
|
1028
869
|
|
|
1029
|
-
# True when `class_name` equals or inherits from `constraint`,
|
|
1030
|
-
#
|
|
1031
|
-
#
|
|
1032
|
-
# "no match" on any resolution failure (false-positive-safe).
|
|
870
|
+
# True when `class_name` equals or inherits from `constraint`, matched through
|
|
871
|
+
# `Environment#class_ordering` (the mechanism `MacroBlockSelfType` / `additional_initializers` use).
|
|
872
|
+
# Degrades to "no match" on any resolution failure (false-positive-safe).
|
|
1033
873
|
def class_matches_receiver?(class_name, constraint, environment)
|
|
1034
874
|
return true if class_name == constraint
|
|
1035
875
|
return false if environment.nil?
|
|
@@ -1051,23 +891,16 @@ module Rigor
|
|
|
1051
891
|
|
|
1052
892
|
public
|
|
1053
893
|
|
|
1054
|
-
# ADR-32 WD5 — the `Cache::Descriptor::PluginEntry`
|
|
1055
|
-
#
|
|
1056
|
-
#
|
|
1057
|
-
#
|
|
1058
|
-
# caching per-file synthesizer output) compose this entry
|
|
1059
|
-
# into their own cache descriptor so a config change to
|
|
1060
|
-
# the plugin (e.g. flipping `require_magic_comment:`)
|
|
894
|
+
# ADR-32 WD5 — the `Cache::Descriptor::PluginEntry` template carrying this plugin's id, version, and a
|
|
895
|
+
# SHA-256 digest of its (canonicalised) config hash. Callers outside the plugin (e.g.
|
|
896
|
+
# `Environment.for_project` caching per-file synthesizer output) compose this entry into their own
|
|
897
|
+
# cache descriptor so a config change to the plugin (e.g. flipping `require_magic_comment:`)
|
|
1061
898
|
# invalidates the dependent cache.
|
|
1062
899
|
def plugin_entry
|
|
1063
|
-
# Built fresh on each call rather than memoised so a
|
|
1064
|
-
#
|
|
1065
|
-
#
|
|
1066
|
-
#
|
|
1067
|
-
# cost is a single `Data.define`-backed value-object
|
|
1068
|
-
# build; the cache key derivation downstream is the
|
|
1069
|
-
# expensive step, and it's already memoised inside
|
|
1070
|
-
# `Cache::Store`.
|
|
900
|
+
# Built fresh on each call rather than memoised so a plugin subclass that freezes itself in
|
|
901
|
+
# `initialize` (e.g. `Rigor::Plugin::RbsInline` per ADR-32) doesn't trip a FrozenError on first read.
|
|
902
|
+
# The construction cost is a single `Data.define`-backed value-object build; the cache key derivation
|
|
903
|
+
# downstream is the expensive step, and it's already memoised inside `Cache::Store`.
|
|
1071
904
|
Cache::Descriptor::PluginEntry.new(
|
|
1072
905
|
id: manifest.id,
|
|
1073
906
|
version: manifest.version,
|
|
@@ -1077,14 +910,10 @@ module Rigor
|
|
|
1077
910
|
|
|
1078
911
|
private
|
|
1079
912
|
|
|
1080
|
-
# ADR-60 WD3 — the cache KEY descriptor: the plugin's
|
|
1081
|
-
#
|
|
1082
|
-
#
|
|
1083
|
-
#
|
|
1084
|
-
# digests). The IoBoundary read history deliberately does NOT
|
|
1085
|
-
# enter the key — it is recorded post-compute into the
|
|
1086
|
-
# dependency descriptor instead (see
|
|
1087
|
-
# {#producer_dependency_descriptor}).
|
|
913
|
+
# ADR-60 WD3 — the cache KEY descriptor: the plugin's PluginEntry template composed with an optional
|
|
914
|
+
# plugin-author-supplied extension carrying IDENTITY inputs (gem-version pins, `ConfigEntry` rows,
|
|
915
|
+
# configuration-file digests). The IoBoundary read history deliberately does NOT enter the key — it is
|
|
916
|
+
# recorded post-compute into the dependency descriptor instead (see {#producer_dependency_descriptor}).
|
|
1088
917
|
def compose_key_descriptor(extra)
|
|
1089
918
|
auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
|
|
1090
919
|
return auto_built if extra.nil?
|
|
@@ -1092,18 +921,13 @@ module Rigor
|
|
|
1092
921
|
Cache::Descriptor.compose(auto_built, extra)
|
|
1093
922
|
end
|
|
1094
923
|
|
|
1095
|
-
# ADR-60 WD3 — the dependency descriptor stored beside the
|
|
1096
|
-
#
|
|
1097
|
-
# in-block `io_boundary` read is captured, plus the evaluated
|
|
1098
|
-
# `watch:` glob rows.
|
|
924
|
+
# ADR-60 WD3 — the dependency descriptor stored beside the producer's value, built AFTER the block ran
|
|
925
|
+
# so every in-block `io_boundary` read is captured, plus the evaluated `watch:` glob rows.
|
|
1099
926
|
#
|
|
1100
|
-
# The boundary snapshot may carry `ConfigEntry` rows (URL
|
|
1101
|
-
#
|
|
1102
|
-
#
|
|
1103
|
-
#
|
|
1104
|
-
# That is deliberate: it is sound (never stale) and
|
|
1105
|
-
# URL-reading producers are rare; a remote document has no
|
|
1106
|
-
# cheap local re-validation anyway.
|
|
927
|
+
# The boundary snapshot may carry `ConfigEntry` rows (URL fetches, see {IoBoundary#open_url}).
|
|
928
|
+
# `Descriptor#fresh?` refuses any non-file/glob slot, so including them makes the entry permanently
|
|
929
|
+
# stale → the producer recomputes EVERY run. That is deliberate: it is sound (never stale) and
|
|
930
|
+
# URL-reading producers are rare; a remote document has no cheap local re-validation anyway.
|
|
1107
931
|
def producer_dependency_descriptor(producer)
|
|
1108
932
|
boundary = io_boundary.cache_descriptor
|
|
1109
933
|
Cache::Descriptor.new(
|
|
@@ -1113,15 +937,11 @@ module Rigor
|
|
|
1113
937
|
)
|
|
1114
938
|
end
|
|
1115
939
|
|
|
1116
|
-
# ADR-60 WD3 — evaluates a producer's `watch:` declaration
|
|
1117
|
-
#
|
|
1118
|
-
# `
|
|
1119
|
-
#
|
|
1120
|
-
#
|
|
1121
|
-
# GlobEntry per (root, pattern) pair. Roots are expanded to
|
|
1122
|
-
# absolute paths (matching {#glob_descriptor}) so freshness
|
|
1123
|
-
# re-validation does not depend on the validating process's
|
|
1124
|
-
# working directory.
|
|
940
|
+
# ADR-60 WD3 — evaluates a producer's `watch:` declaration into {Cache::Descriptor::GlobEntry} rows. A
|
|
941
|
+
# Proc is `instance_exec`'d on this plugin instance (so `#init`-built search roots are in scope); the
|
|
942
|
+
# result — like the static form — is an Array of `[roots, pattern, ...]` tuples, one GlobEntry per
|
|
943
|
+
# (root, pattern) pair. Roots are expanded to absolute paths (matching {#glob_descriptor}) so freshness
|
|
944
|
+
# re-validation does not depend on the validating process's working directory.
|
|
1125
945
|
def watch_glob_entries(watch)
|
|
1126
946
|
return [] if watch.nil?
|
|
1127
947
|
|
|
@@ -1135,14 +955,11 @@ module Rigor
|
|
|
1135
955
|
end.uniq
|
|
1136
956
|
end
|
|
1137
957
|
|
|
1138
|
-
# ADR-60 WD3 — `fetch_or_validate` stores a
|
|
1139
|
-
#
|
|
1140
|
-
#
|
|
1141
|
-
#
|
|
1142
|
-
#
|
|
1143
|
-
# with a non-Marshal-clean value keeps working unchanged. A
|
|
1144
|
-
# nil callable returns nil — the store's default whole-pair
|
|
1145
|
-
# Marshal round-trip applies.
|
|
958
|
+
# ADR-60 WD3 — `fetch_or_validate` stores a `[value, dependency_descriptor]` pair, but the producer's
|
|
959
|
+
# declared `serialize:`/`deserialize:` contract covers the VALUE alone. These wrappers apply the custom
|
|
960
|
+
# callable to the value half and Marshal the descriptor half, so a producer with a non-Marshal-clean
|
|
961
|
+
# value keeps working unchanged. A nil callable returns nil — the store's default whole-pair Marshal
|
|
962
|
+
# round-trip applies.
|
|
1146
963
|
def pair_serializer(serialize)
|
|
1147
964
|
return nil if serialize.nil?
|
|
1148
965
|
|