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
|
@@ -8,27 +8,21 @@ require_relative "helper_table"
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Plugin
|
|
10
10
|
class RailsRoutes < Rigor::Plugin::Base
|
|
11
|
-
# Statically interprets `config/routes.rb`'s DSL via
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# in the plugin's README and degrades silently on
|
|
15
|
-
# constructs it doesn't recognise.
|
|
11
|
+
# Statically interprets `config/routes.rb`'s DSL via Prism — never executes the file. The interpreter
|
|
12
|
+
# is deliberately narrow; it covers the subset documented in the plugin's README and degrades
|
|
13
|
+
# silently on constructs it doesn't recognise.
|
|
16
14
|
#
|
|
17
|
-
# Recognised DSL surface (per the Rails-plugins
|
|
18
|
-
# roadmap):
|
|
15
|
+
# Recognised DSL surface (per the Rails-plugins roadmap):
|
|
19
16
|
#
|
|
20
|
-
# - `Rails.application.routes.draw do ... end` (entry
|
|
21
|
-
# block; the body is interpreted)
|
|
17
|
+
# - `Rails.application.routes.draw do ... end` (entry block; the body is interpreted)
|
|
22
18
|
# - `resources :name [, only: [...] | except: [...]]`
|
|
23
19
|
# - `resource :name`
|
|
24
20
|
# - `get/post/patch/put/delete "path", to:, as:`
|
|
25
21
|
# - `root to: "..."` / `root "..."`
|
|
26
22
|
# - `scope "path", as: :name do ... end` (with `as:` key)
|
|
27
23
|
# - One level of `namespace :foo do ... end`
|
|
28
|
-
# - One level of nested `resources` (`resources :users
|
|
29
|
-
#
|
|
30
|
-
# - `member do ... end` / `collection do ... end`
|
|
31
|
-
# inside `resources`
|
|
24
|
+
# - One level of nested `resources` (`resources :users do; resources :posts; end`)
|
|
25
|
+
# - `member do ... end` / `collection do ... end` inside `resources`
|
|
32
26
|
#
|
|
33
27
|
# Out of scope for v0.1.0 (silent skips):
|
|
34
28
|
#
|
|
@@ -40,12 +34,10 @@ module Rigor
|
|
|
40
34
|
module RoutesParser
|
|
41
35
|
# Standard resource actions Rails generates by default.
|
|
42
36
|
DEFAULT_RESOURCE_ACTIONS = %i[index show new create edit update destroy].freeze
|
|
43
|
-
# Default actions for `resource` (singular) — no index,
|
|
44
|
-
# no `:id` segment.
|
|
37
|
+
# Default actions for `resource` (singular) — no index, no `:id` segment.
|
|
45
38
|
DEFAULT_SINGULAR_ACTIONS = %i[show new create edit update destroy].freeze
|
|
46
39
|
|
|
47
|
-
# Helper-name conventions per action. `:show` and
|
|
48
|
-
# `:update` / `:destroy` share the singular-form
|
|
40
|
+
# Helper-name conventions per action. `:show` and `:update` / `:destroy` share the singular-form
|
|
49
41
|
# helper (Rails dedupes).
|
|
50
42
|
ACTION_HTTP_METHODS = {
|
|
51
43
|
index: :get,
|
|
@@ -71,10 +63,8 @@ module Rigor
|
|
|
71
63
|
context = Context.new(file_reader: file_reader)
|
|
72
64
|
interpret(parse_result.value, context)
|
|
73
65
|
|
|
74
|
-
# Apply name-transform alias rules discovered during
|
|
75
|
-
#
|
|
76
|
-
# "#{name}_url", ...) end` GitLab pattern — see
|
|
77
|
-
# `collect_alias_rules` below).
|
|
66
|
+
# Apply name-transform alias rules discovered during the walk (the `direct(name.sub(X, Y)) do
|
|
67
|
+
# |...| send("#{name}_url", ...) end` GitLab pattern — see `collect_alias_rules` below).
|
|
78
68
|
apply_alias_rules(context)
|
|
79
69
|
|
|
80
70
|
# Each helper has both `_path` and `_url` forms.
|
|
@@ -93,21 +83,15 @@ module Rigor
|
|
|
93
83
|
HelperTable.new(paired, custom_helpers: custom_helpers, devise_resources: context.devise_resources)
|
|
94
84
|
end
|
|
95
85
|
|
|
96
|
-
# For every registered alias rule `(from_str, to_str,
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# `
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# project,
|
|
104
|
-
#
|
|
105
|
-
# `project` and forwards the rest, so the alias's arity
|
|
106
|
-
# is one LESS than the original helper's
|
|
107
|
-
# (`namespace_project_blob_path(namespace_id, project_id,
|
|
108
|
-
# blob_id)` → `project_blob_path(project, blob_id)`).
|
|
109
|
-
# We register each alias under the original entry's
|
|
110
|
-
# arity AND `original - 1` to span both interpretations.
|
|
86
|
+
# For every registered alias rule `(from_str, to_str, arity_delta)`, find every existing entry whose
|
|
87
|
+
# name contains `from_str` and register an aliased entry with `from_str → to_str` substitution. The
|
|
88
|
+
# pattern matches GitLab's shorthand-helper idiom: a loop over registered route names that invokes
|
|
89
|
+
# `direct(new_name) do |project, *args| send("#{name}_url", project&.namespace, project, *args) end`
|
|
90
|
+
# where `new_name = name.sub(FROM, TO)`. The direct block extracts the namespace from `project` and
|
|
91
|
+
# forwards the rest, so the alias's arity is one LESS than the original helper's
|
|
92
|
+
# (`namespace_project_blob_path(namespace_id, project_id, blob_id)` →
|
|
93
|
+
# `project_blob_path(project, blob_id)`). We register each alias under the original entry's arity
|
|
94
|
+
# AND `original - 1` to span both interpretations.
|
|
111
95
|
def apply_alias_rules(context)
|
|
112
96
|
return if context.alias_rules.empty?
|
|
113
97
|
|
|
@@ -119,10 +103,8 @@ module Rigor
|
|
|
119
103
|
new_name = entry.name.sub(from, to)
|
|
120
104
|
next if new_name == entry.name
|
|
121
105
|
|
|
122
|
-
# Register at original arity AND at arity-1 so the
|
|
123
|
-
#
|
|
124
|
-
# collapses namespace_id + project_id into project)
|
|
125
|
-
# passes the arity check.
|
|
106
|
+
# Register at original arity AND at arity-1 so the common GitLab `|project, *args|` pattern
|
|
107
|
+
# (which collapses namespace_id + project_id into project) passes the arity check.
|
|
126
108
|
[entry.arity, [entry.arity - 1, 0].max].uniq.each do |arity|
|
|
127
109
|
aliases << HelperTable::Entry.new(
|
|
128
110
|
name: new_name, arity: arity,
|
|
@@ -135,9 +117,8 @@ module Rigor
|
|
|
135
117
|
context.entries.concat(aliases)
|
|
136
118
|
end
|
|
137
119
|
|
|
138
|
-
# Per-parse mutable accumulator. Tracks the current
|
|
139
|
-
#
|
|
140
|
-
# entries collected so far.
|
|
120
|
+
# Per-parse mutable accumulator. Tracks the current nesting prefix (namespaces + parent resource)
|
|
121
|
+
# and the entries collected so far.
|
|
141
122
|
class Context
|
|
142
123
|
attr_reader :entries, :file_reader, :devise_resources, :alias_rules
|
|
143
124
|
|
|
@@ -149,22 +130,15 @@ module Rigor
|
|
|
149
130
|
# - `{ kind: :scope, parent: "user", arity_segments: [":user_id"] }`
|
|
150
131
|
# - `{ kind: :as_scope, name: "event", path: "/:event_slug", arity: 1 }`
|
|
151
132
|
@stack = []
|
|
152
|
-
# Devise resource segments (singularised) declared
|
|
153
|
-
# via `devise_for :resource`. Drives the
|
|
133
|
+
# Devise resource segments (singularised) declared via `devise_for :resource`. Drives the
|
|
154
134
|
# OmniAuth-helper recognition in `HelperTable#omniauth_match?`.
|
|
155
135
|
@devise_resources = []
|
|
156
|
-
# Registered `concern :name do ... end` blocks.
|
|
157
|
-
#
|
|
158
|
-
# node. `resources :foo, concerns: :name do ... end`
|
|
159
|
-
# replays the body at the resource's site.
|
|
136
|
+
# Registered `concern :name do ... end` blocks. Keyed by Symbol name; value is the block's body
|
|
137
|
+
# node. `resources :foo, concerns: :name do ... end` replays the body at the resource's site.
|
|
160
138
|
@concerns = {}
|
|
161
|
-
# `(from_str, to_str)` pairs collected from
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
# `apply_alias_rules` to generate substituted-name
|
|
165
|
-
# aliases for every matching entry — closes
|
|
166
|
-
# GitLab's `namespace_project_*` → `project_*`
|
|
167
|
-
# shorthand idiom.
|
|
139
|
+
# `(from_str, to_str)` pairs collected from iterative `direct(name.sub(FROM, TO)) do ... end`
|
|
140
|
+
# patterns. Applied after parsing via `apply_alias_rules` to generate substituted-name aliases
|
|
141
|
+
# for every matching entry — closes GitLab's `namespace_project_*` → `project_*` shorthand idiom.
|
|
168
142
|
@alias_rules = []
|
|
169
143
|
end
|
|
170
144
|
|
|
@@ -200,17 +174,12 @@ module Rigor
|
|
|
200
174
|
@stack.pop
|
|
201
175
|
end
|
|
202
176
|
|
|
203
|
-
# `resource :foo do ... end` (SINGULAR resource) —
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
# resources have no `:id`). So `resource :instance
|
|
208
|
-
# do; resources :domain_blocks; end` generates
|
|
209
|
-
# `instance_domain_blocks_path` (arity 0), not
|
|
210
|
-
# `instance_domain_blocks_path(:id)`.
|
|
177
|
+
# `resource :foo do ... end` (SINGULAR resource) — adds the resource name to the helper prefix
|
|
178
|
+
# and path for nested declarations, but DOESN'T contribute a dynamic `:id` segment (singular
|
|
179
|
+
# resources have no `:id`). So `resource :instance do; resources :domain_blocks; end` generates
|
|
180
|
+
# `instance_domain_blocks_path` (arity 0), not `instance_domain_blocks_path(:id)`.
|
|
211
181
|
#
|
|
212
|
-
# Helper-prefix segment uses the AS-GIVEN name (no
|
|
213
|
-
# singularising — `resource :foo` keeps `foo`).
|
|
182
|
+
# Helper-prefix segment uses the AS-GIVEN name (no singularising — `resource :foo` keeps `foo`).
|
|
214
183
|
def push_singular_resource(parent_name)
|
|
215
184
|
name = parent_name.to_s
|
|
216
185
|
@stack.push(kind: :singular_scope, parent: name, path_segment: "/#{name}")
|
|
@@ -219,14 +188,10 @@ module Rigor
|
|
|
219
188
|
@stack.pop
|
|
220
189
|
end
|
|
221
190
|
|
|
222
|
-
# `member do ... end` / `collection do ... end` —
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
# resource. The frame also carries the immediate
|
|
227
|
-
# parent's singular / plural names so member /
|
|
228
|
-
# collection actions can pick the correct form
|
|
229
|
-
# (`memorialize_account_path(id)` vs
|
|
191
|
+
# `member do ... end` / `collection do ... end` — records the mode so subsequent shorthand
|
|
192
|
+
# HTTP-verb calls (`post :memorialize`) inside the block can derive their helper name from the
|
|
193
|
+
# enclosing resource. The frame also carries the immediate parent's singular / plural names so
|
|
194
|
+
# member / collection actions can pick the correct form (`memorialize_account_path(id)` vs
|
|
230
195
|
# `memorialize_accounts_path`).
|
|
231
196
|
def push_action_block(mode, parent_singular, parent_plural)
|
|
232
197
|
@stack.push(kind: :"#{mode}_block", parent_singular: parent_singular, parent_plural: parent_plural)
|
|
@@ -235,11 +200,9 @@ module Rigor
|
|
|
235
200
|
@stack.pop
|
|
236
201
|
end
|
|
237
202
|
|
|
238
|
-
# `with_options only: [:index], concerns: :batch do
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
# override the defaults). `effective_options_for`
|
|
242
|
-
# below merges them in caller-precedence order.
|
|
203
|
+
# `with_options only: [:index], concerns: :batch do ... end` — every call inside the block
|
|
204
|
+
# inherits these default options (each call's own options override the defaults).
|
|
205
|
+
# `effective_options_for` below merges them in caller-precedence order.
|
|
243
206
|
def push_with_options(defaults)
|
|
244
207
|
@stack.push(kind: :with_options, defaults: defaults)
|
|
245
208
|
yield
|
|
@@ -247,15 +210,10 @@ module Rigor
|
|
|
247
210
|
@stack.pop
|
|
248
211
|
end
|
|
249
212
|
|
|
250
|
-
# The merged default-options chain from every
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
# with the node's own option hash so a bare
|
|
255
|
-
# `resources :links` inside `with_options only:
|
|
256
|
-
# [:index], concerns: :batch do ... end` is
|
|
257
|
-
# treated as if it had those options written
|
|
258
|
-
# inline.
|
|
213
|
+
# The merged default-options chain from every enclosing `with_options` frame on the stack
|
|
214
|
+
# (outer-most first; inner frames take precedence). `handle_resources` / `handle_resource` merge
|
|
215
|
+
# this with the node's own option hash so a bare `resources :links` inside `with_options only:
|
|
216
|
+
# [:index], concerns: :batch do ... end` is treated as if it had those options written inline.
|
|
259
217
|
def with_options_defaults
|
|
260
218
|
defaults = {}
|
|
261
219
|
@stack.each do |frame|
|
|
@@ -266,10 +224,8 @@ module Rigor
|
|
|
266
224
|
defaults
|
|
267
225
|
end
|
|
268
226
|
|
|
269
|
-
# Returns the top-most `:scope` frame's singular /
|
|
270
|
-
#
|
|
271
|
-
# / resource block. Used by `handle_member_or_collection`
|
|
272
|
-
# to push the action-block frame.
|
|
227
|
+
# Returns the top-most `:scope` frame's singular / plural names, or `nil` when not inside a
|
|
228
|
+
# resources / resource block. Used by `handle_member_or_collection` to push the action-block frame.
|
|
273
229
|
def innermost_resource
|
|
274
230
|
scope_frame = @stack.rfind { |f| f[:kind] == :scope }
|
|
275
231
|
return nil if scope_frame.nil?
|
|
@@ -277,17 +233,14 @@ module Rigor
|
|
|
277
233
|
{ singular: scope_frame[:parent], plural: scope_frame[:parent_plural] || scope_frame[:parent] }
|
|
278
234
|
end
|
|
279
235
|
|
|
280
|
-
# Top-most `:member_block` / `:collection_block`
|
|
281
|
-
#
|
|
282
|
-
# context. `handle_explicit_route` uses this to
|
|
283
|
-
# detect a `post :memorialize` symbol-only call shape.
|
|
236
|
+
# Top-most `:member_block` / `:collection_block` frame, or nil when not in a shorthand action
|
|
237
|
+
# context. `handle_explicit_route` uses this to detect a `post :memorialize` symbol-only call shape.
|
|
284
238
|
def innermost_action_block
|
|
285
239
|
@stack.rfind { |f| %i[member_block collection_block].include?(f[:kind]) }
|
|
286
240
|
end
|
|
287
241
|
|
|
288
|
-
# `scope "/:slug", as: "foo" do ... end` — adds a
|
|
289
|
-
#
|
|
290
|
-
# arithmetic that push_resource uses.
|
|
242
|
+
# `scope "/:slug", as: "foo" do ... end` — adds a helper-name prefix without the "parent
|
|
243
|
+
# resource" arity arithmetic that push_resource uses.
|
|
291
244
|
def push_as_scope(name, path, arity_count)
|
|
292
245
|
@stack.push(kind: :as_scope, name: name.to_s, path: path, arity: arity_count)
|
|
293
246
|
yield
|
|
@@ -295,20 +248,16 @@ module Rigor
|
|
|
295
248
|
@stack.pop
|
|
296
249
|
end
|
|
297
250
|
|
|
298
|
-
# Helper-name prefix from namespaces (`admin_`,
|
|
299
|
-
# `admin_users_`, …).
|
|
251
|
+
# Helper-name prefix from namespaces (`admin_`, `admin_users_`, …).
|
|
300
252
|
def helper_prefix
|
|
301
253
|
segments = @stack.filter_map { |frame| frame_helper_segment(frame) }
|
|
302
254
|
segments.map { |segment| "#{segment}_" }.join
|
|
303
255
|
end
|
|
304
256
|
|
|
305
|
-
# The chain of resource-singular segments above the
|
|
306
|
-
#
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
# resources :issues do; get 'foo', as: 'bar'; end; end`
|
|
310
|
-
# → `bar_project_issue_path`). Returns `""` outside any
|
|
311
|
-
# `:scope` / `:singular_scope` frame.
|
|
257
|
+
# The chain of resource-singular segments above the current scope, in order — used to form
|
|
258
|
+
# member-route helper names of the form `<as>_<singular_chain>_path`. Includes nested resources
|
|
259
|
+
# (`resources :projects do; resources :issues do; get 'foo', as: 'bar'; end; end` →
|
|
260
|
+
# `bar_project_issue_path`). Returns `""` outside any `:scope` / `:singular_scope` frame.
|
|
312
261
|
def resource_singular_chain
|
|
313
262
|
segments = @stack.filter_map do |frame|
|
|
314
263
|
case frame[:kind]
|
|
@@ -319,12 +268,10 @@ module Rigor
|
|
|
319
268
|
segments.empty? ? "" : "#{segments.join('_')}_"
|
|
320
269
|
end
|
|
321
270
|
|
|
322
|
-
# Namespace-only prefix (drops the resource-singular
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
# `admin_foo_project_path` — namespace first, then
|
|
327
|
-
# `:as` action prefix, then singular chain.
|
|
271
|
+
# Namespace-only prefix (drops the resource-singular segments that `helper_prefix` includes).
|
|
272
|
+
# Used so that an `:as => 'foo'` inside `namespace :admin do resources :projects do; ...; end;
|
|
273
|
+
# end` generates `admin_foo_project_path` — namespace first, then `:as` action prefix, then
|
|
274
|
+
# singular chain.
|
|
328
275
|
def namespace_only_prefix
|
|
329
276
|
segments = @stack.filter_map do |frame|
|
|
330
277
|
case frame[:kind]
|
|
@@ -335,34 +282,28 @@ module Rigor
|
|
|
335
282
|
segments.empty? ? "" : "#{segments.join('_')}_"
|
|
336
283
|
end
|
|
337
284
|
|
|
338
|
-
# True when we're inside a `resources` or `resource`
|
|
339
|
-
#
|
|
340
|
-
# `:singular_scope` frame). Drives member-route
|
|
341
|
-
# helper-name generation in `handle_explicit_route`.
|
|
285
|
+
# True when we're inside a `resources` or `resource` scope (the AST has pushed a `:scope` or
|
|
286
|
+
# `:singular_scope` frame). Drives member-route helper-name generation in `handle_explicit_route`.
|
|
342
287
|
def inside_resource_scope?
|
|
343
288
|
@stack.any? { |frame| %i[scope singular_scope].include?(frame[:kind]) }
|
|
344
289
|
end
|
|
345
290
|
|
|
346
|
-
# The innermost `:scope` frame (plural resource),
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
# :member` form to derive the helper name.
|
|
291
|
+
# The innermost `:scope` frame (plural resource), carrying `parent` (singular) and
|
|
292
|
+
# `parent_plural`. Used by the inline `:on => :collection` / `:on => :member` form to derive the
|
|
293
|
+
# helper name.
|
|
350
294
|
def innermost_scope_frame
|
|
351
295
|
@stack.rfind { |f| f[:kind] == :scope }
|
|
352
296
|
end
|
|
353
297
|
|
|
354
|
-
# Path prefix — including the parent's `:user_id`
|
|
355
|
-
#
|
|
356
|
-
# path prefix.
|
|
298
|
+
# Path prefix — including the parent's `:user_id` segments for nested resources and the
|
|
299
|
+
# namespace path prefix.
|
|
357
300
|
def path_prefix
|
|
358
301
|
parts = @stack.flat_map { |frame| frame_path_segments(frame) }
|
|
359
302
|
parts.join
|
|
360
303
|
end
|
|
361
304
|
|
|
362
|
-
# Number of dynamic segments (`:user_id`-style)
|
|
363
|
-
#
|
|
364
|
-
# compute helper arity for nested resources.
|
|
365
|
-
# Each nested-resource `:scope` frame contributes 1;
|
|
305
|
+
# Number of dynamic segments (`:user_id`-style) captured by the parent scope chain. Used to
|
|
306
|
+
# compute helper arity for nested resources. Each nested-resource `:scope` frame contributes 1;
|
|
366
307
|
# each `:as_scope` frame contributes its own arity.
|
|
367
308
|
def parent_segment_count
|
|
368
309
|
@stack.sum do |frame|
|
|
@@ -381,10 +322,8 @@ module Rigor
|
|
|
381
322
|
when :namespace then frame[:name]
|
|
382
323
|
when :scope then frame[:parent]
|
|
383
324
|
when :as_scope
|
|
384
|
-
# An `:as_scope` frame with an empty `:name` is a
|
|
385
|
-
#
|
|
386
|
-
# without `:as` — contributes path/arity but no
|
|
387
|
-
# helper-prefix segment.
|
|
325
|
+
# An `:as_scope` frame with an empty `:name` is a path-only frame pushed by `scope(path: 'X')`
|
|
326
|
+
# without `:as` — contributes path/arity but no helper-prefix segment.
|
|
388
327
|
frame[:name].to_s.empty? ? nil : frame[:name]
|
|
389
328
|
when :singular_scope then frame[:parent]
|
|
390
329
|
end
|
|
@@ -394,12 +333,9 @@ module Rigor
|
|
|
394
333
|
case frame[:kind]
|
|
395
334
|
when :namespace then ["/#{frame[:name]}"]
|
|
396
335
|
when :scope
|
|
397
|
-
# Use the as-given plural when available
|
|
398
|
-
# (`
|
|
399
|
-
#
|
|
400
|
-
# pluralize round-trip is lossy for irregular
|
|
401
|
-
# forms (`media → medium → medias`) but the
|
|
402
|
-
# original name is always correct.
|
|
336
|
+
# Use the as-given plural when available (`parent_plural` was captured at push_resource
|
|
337
|
+
# time) — the singularize / pluralize round-trip is lossy for irregular forms (`media →
|
|
338
|
+
# medium → medias`) but the original name is always correct.
|
|
403
339
|
plural = frame[:parent_plural] || pluralize(frame[:parent])
|
|
404
340
|
["/#{plural}/:#{frame[:parent]}_id"]
|
|
405
341
|
when :as_scope then frame[:path] ? [frame[:path]] : []
|
|
@@ -408,18 +344,14 @@ module Rigor
|
|
|
408
344
|
end
|
|
409
345
|
end
|
|
410
346
|
|
|
411
|
-
# ADR-39: inflection delegates to the shared
|
|
412
|
-
# `
|
|
413
|
-
#
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
# `async_refresh`→`async_refreshes`) was reverse-engineered
|
|
420
|
-
# `ActiveSupport::Inflector` behaviour, so the real inflector
|
|
421
|
-
# produces the same result for those and the correct one for the
|
|
422
|
-
# words the replica never covered.
|
|
347
|
+
# ADR-39: inflection delegates to the shared `Rigor::Plugin::Inflector`, which calls the real
|
|
348
|
+
# `ActiveSupport::Inflector` (the authority Rails itself uses to generate helper names) when
|
|
349
|
+
# available, falling back to a built-in approximation otherwise. This replaces the hand-tuned
|
|
350
|
+
# AS-replica these methods used to carry — every special case they accreted (`news` uncountable,
|
|
351
|
+
# `media`→`medium`, `databases`→`database`, `custom_css` ss-preservation,
|
|
352
|
+
# `async_refresh`→`async_refreshes`) was reverse-engineered `ActiveSupport::Inflector` behaviour,
|
|
353
|
+
# so the real inflector produces the same result for those and the correct one for the words the
|
|
354
|
+
# replica never covered.
|
|
423
355
|
def singularize(word)
|
|
424
356
|
Rigor::Plugin::Inflector.singularize(word)
|
|
425
357
|
end
|
|
@@ -447,21 +379,15 @@ module Rigor
|
|
|
447
379
|
# `Rails.application.routes.draw do ... end`
|
|
448
380
|
interpret_block_body(node, context)
|
|
449
381
|
else
|
|
450
|
-
# `draw(:admin)` — routing partial at
|
|
451
|
-
# config/routes/{name}.rb.
|
|
382
|
+
# `draw(:admin)` — routing partial at config/routes/{name}.rb.
|
|
452
383
|
load_drawn_routes(node, context)
|
|
453
384
|
end
|
|
454
385
|
when :draw_all
|
|
455
|
-
# `draw_all :user` — from the
|
|
456
|
-
#
|
|
457
|
-
#
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
# GitLab's `draw_all :user` loads
|
|
461
|
-
# `config/routes/user.rb` containing `devise_for
|
|
462
|
-
# :users, controllers: ...` plus the broader user
|
|
463
|
-
# route catalogue; without this every Devise
|
|
464
|
-
# session helper reads as `unknown-helper`.
|
|
386
|
+
# `draw_all :user` — from the `action_dispatch-draw_all` gem (used by GitLab FOSS). Same
|
|
387
|
+
# single-file load semantics as `draw` — the gem just allows multiple route-dir search paths
|
|
388
|
+
# (we look in the one we know about). GitLab's `draw_all :user` loads
|
|
389
|
+
# `config/routes/user.rb` containing `devise_for :users, controllers: ...` plus the broader
|
|
390
|
+
# user route catalogue; without this every Devise session helper reads as `unknown-helper`.
|
|
465
391
|
load_drawn_routes(node, context)
|
|
466
392
|
when :namespace
|
|
467
393
|
handle_namespace(node, context)
|
|
@@ -474,21 +400,15 @@ module Rigor
|
|
|
474
400
|
when :scope
|
|
475
401
|
handle_scope(node, context)
|
|
476
402
|
when :get, :post, :patch, :put, :delete, :match
|
|
477
|
-
# `match 'login', :to => 'a#b', :as => :signin, :via => [:get, :post]`
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
#
|
|
482
|
-
# generation logic is identical, so reuse the same
|
|
483
|
-
# handler. Redmine relies heavily on `match` for
|
|
484
|
-
# multi-method endpoints (`account/lost_password`,
|
|
485
|
-
# `news/preview`, etc.).
|
|
403
|
+
# `match 'login', :to => 'a#b', :as => :signin, :via => [:get, :post]` generates the same
|
|
404
|
+
# helper-name shape as `get` / `post` (first arg = path string, `:as` overrides the derived
|
|
405
|
+
# helper name). The only difference is the HTTP-method set (driven by `:via`); the helper-name
|
|
406
|
+
# generation logic is identical, so reuse the same handler. Redmine relies heavily on `match`
|
|
407
|
+
# for multi-method endpoints (`account/lost_password`, `news/preview`, etc.).
|
|
486
408
|
handle_explicit_route(node, context)
|
|
487
409
|
when :member, :collection
|
|
488
|
-
# Inside a `resources` block, `member do ... end`
|
|
489
|
-
#
|
|
490
|
-
# routes. Interpreted only when we have a parent
|
|
491
|
-
# scope (otherwise the call is meaningless).
|
|
410
|
+
# Inside a `resources` block, `member do ... end` / `collection do ... end` introduces extra
|
|
411
|
+
# routes. Interpreted only when we have a parent scope (otherwise the call is meaningless).
|
|
492
412
|
handle_member_or_collection(node, context)
|
|
493
413
|
when :devise_for
|
|
494
414
|
handle_devise_for(node, context)
|
|
@@ -501,19 +421,14 @@ module Rigor
|
|
|
501
421
|
when :concern
|
|
502
422
|
handle_concern_definition(node, context)
|
|
503
423
|
when :direct
|
|
504
|
-
# `direct(:name) do |args...| ... end` — Rails DSL
|
|
505
|
-
#
|
|
506
|
-
#
|
|
507
|
-
# from the block's required-parameter count. Only
|
|
508
|
-
# literal Symbol/String names are handled here;
|
|
509
|
-
# non-literal forms are caught by the
|
|
424
|
+
# `direct(:name) do |args...| ... end` — Rails DSL for a custom URL helper. We register
|
|
425
|
+
# `name_path` and (auto-paired) `name_url`. The arity comes from the block's required-parameter
|
|
426
|
+
# count. Only literal Symbol/String names are handled here; non-literal forms are caught by the
|
|
510
427
|
# `detect_alias_rule_in_each` walker upstream.
|
|
511
428
|
handle_direct(node, context)
|
|
512
429
|
when :each
|
|
513
|
-
# `.each do |name| ... end` — scan the block for
|
|
514
|
-
# the
|
|
515
|
-
# alias-generation idiom. If found, record the
|
|
516
|
-
# (X, Y) pair so `apply_alias_rules` can expand
|
|
430
|
+
# `.each do |name| ... end` — scan the block for the GitLab `direct(name.sub(X, Y)) do ... end`
|
|
431
|
+
# alias-generation idiom. If found, record the (X, Y) pair so `apply_alias_rules` can expand
|
|
517
432
|
# every matching registered entry.
|
|
518
433
|
detect_alias_rule_in_each(node, context)
|
|
519
434
|
interpret_block_body(node, context)
|
|
@@ -522,11 +437,9 @@ module Rigor
|
|
|
522
437
|
end
|
|
523
438
|
end
|
|
524
439
|
|
|
525
|
-
# `direct(:name) do |arg1, arg2, ...| ... end` — Rails
|
|
526
|
-
#
|
|
527
|
-
#
|
|
528
|
-
# auto-pairing in `parse` adds `name_url`). Only handled
|
|
529
|
-
# for literal Symbol or String first-arg.
|
|
440
|
+
# `direct(:name) do |arg1, arg2, ...| ... end` — Rails adds a custom URL helper. We register
|
|
441
|
+
# `name_path` with the block's required-arg count as the arity (the auto-pairing in `parse` adds
|
|
442
|
+
# `name_url`). Only handled for literal Symbol or String first-arg.
|
|
530
443
|
def handle_direct(node, context)
|
|
531
444
|
name = Rigor::Source::Literals.symbol_or_string_name(node.arguments&.arguments&.first)
|
|
532
445
|
return if name.nil? || name.empty?
|
|
@@ -556,13 +469,10 @@ module Rigor
|
|
|
556
469
|
# Walks a `.each do |loop_var| ... end` body looking for:
|
|
557
470
|
# - `<new_name_var> = <loop_var>.sub(FROM_STR, TO_STR)`
|
|
558
471
|
# - `direct(<new_name_var>) do |...| ... end`
|
|
559
|
-
# When both shapes appear with the same `<new_name_var>`,
|
|
560
|
-
#
|
|
561
|
-
#
|
|
562
|
-
# is
|
|
563
|
-
# helpers — GitLab's idiom. Other iterations that happen
|
|
564
|
-
# to match the shape are rare; the FP risk is bounded
|
|
565
|
-
# because alias generation only adds entries.
|
|
472
|
+
# When both shapes appear with the same `<new_name_var>`, registers `(FROM_STR, TO_STR)` as an
|
|
473
|
+
# alias rule. The iterated collection (`Rails.application.routes.set`) is assumed to yield the
|
|
474
|
+
# names of already-registered helpers — GitLab's idiom. Other iterations that happen to match the
|
|
475
|
+
# shape are rare; the FP risk is bounded because alias generation only adds entries.
|
|
566
476
|
def detect_alias_rule_in_each(each_node, context)
|
|
567
477
|
block = each_node.block
|
|
568
478
|
return unless block.is_a?(Prism::BlockNode)
|
|
@@ -573,8 +483,7 @@ module Rigor
|
|
|
573
483
|
body = block.body
|
|
574
484
|
return if body.nil?
|
|
575
485
|
|
|
576
|
-
# First pass: gather every `<var> = <loop_var>.sub(FROM, TO)`
|
|
577
|
-
# assignment.
|
|
486
|
+
# First pass: gather every `<var> = <loop_var>.sub(FROM, TO)` assignment.
|
|
578
487
|
sub_assignments = {}
|
|
579
488
|
walk_for_alias_pattern(body) do |node|
|
|
580
489
|
next unless node.is_a?(Prism::LocalVariableWriteNode)
|
|
@@ -596,9 +505,8 @@ module Rigor
|
|
|
596
505
|
|
|
597
506
|
return if sub_assignments.empty?
|
|
598
507
|
|
|
599
|
-
# Second pass: find `direct(<var>) do ... end` calls
|
|
600
|
-
#
|
|
601
|
-
# variables.
|
|
508
|
+
# Second pass: find `direct(<var>) do ... end` calls whose first arg is one of the captured
|
|
509
|
+
# assignment variables.
|
|
602
510
|
walk_for_alias_pattern(body) do |node|
|
|
603
511
|
next unless node.is_a?(Prism::CallNode) && node.name == :direct
|
|
604
512
|
next if node.arguments.nil?
|
|
@@ -644,14 +552,11 @@ module Rigor
|
|
|
644
552
|
context.push_namespace(name) { interpret_block_body(node, context) }
|
|
645
553
|
end
|
|
646
554
|
|
|
647
|
-
# `devise_for :users [, skip: [...], path: "..."]` —
|
|
648
|
-
#
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
652
|
-
# helper SET depends on the literal name and we cannot
|
|
653
|
-
# statically resolve a variable here. `skip:` is read
|
|
654
|
-
# so the project's omitted controllers do not register.
|
|
555
|
+
# `devise_for :users [, skip: [...], path: "..."]` — generates the standard Devise route-helper
|
|
556
|
+
# catalogue for the named resource. Symbol-literal first arg only; non-literal forms (e.g. dynamic
|
|
557
|
+
# resource names built from constants) are silently skipped because the helper SET depends on the
|
|
558
|
+
# literal name and we cannot statically resolve a variable here. `skip:` is read so the project's
|
|
559
|
+
# omitted controllers do not register.
|
|
655
560
|
def handle_devise_for(node, context)
|
|
656
561
|
resource = symbol_argument(node, 0)
|
|
657
562
|
return if resource.nil?
|
|
@@ -664,18 +569,13 @@ module Rigor
|
|
|
664
569
|
end
|
|
665
570
|
end
|
|
666
571
|
|
|
667
|
-
# `with_options X do ... end` — applies the `X`
|
|
668
|
-
#
|
|
669
|
-
#
|
|
670
|
-
#
|
|
671
|
-
# :tags; end` lets us register the inner resources
|
|
672
|
-
# with the implicit defaults — closing `batch_*_path`
|
|
673
|
-
# cluster generated via the `:batch` concern.
|
|
572
|
+
# `with_options X do ... end` — applies the `X` options hash as defaults for every call inside the
|
|
573
|
+
# block. Mastodon's `with_options only: [:index], concerns: :batch do resources :links; resources
|
|
574
|
+
# :tags; end` lets us register the inner resources with the implicit defaults — closing
|
|
575
|
+
# `batch_*_path` cluster generated via the `:batch` concern.
|
|
674
576
|
#
|
|
675
|
-
# We push a `:with_options` frame carrying the
|
|
676
|
-
#
|
|
677
|
-
# `handle_resources` / `handle_resource`) merges them
|
|
678
|
-
# in before reading specific option keys.
|
|
577
|
+
# We push a `:with_options` frame carrying the defaults; `effective_options_for(node)` (in
|
|
578
|
+
# `handle_resources` / `handle_resource`) merges them in before reading specific option keys.
|
|
679
579
|
def handle_with_options(node, context)
|
|
680
580
|
defaults = options_hash(node)
|
|
681
581
|
context.push_with_options(defaults) do
|
|
@@ -683,18 +583,13 @@ module Rigor
|
|
|
683
583
|
end
|
|
684
584
|
end
|
|
685
585
|
|
|
686
|
-
# `mount Foo::Engine, at: '/path', as: :name` —
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
# helpers are out of scope (they'd need the engine's
|
|
691
|
-
# routes.rb available; almost no static parser
|
|
692
|
-
# follows that). Mastodon uses `mount Sidekiq::Web,
|
|
693
|
-
# at: 'sidekiq', as: :sidekiq` and similar.
|
|
586
|
+
# `mount Foo::Engine, at: '/path', as: :name` — mounted Rails engines. The mount adds a single
|
|
587
|
+
# helper for the mount point itself: `<as>_path` / `<as>_url` (arity 0). The engine's own internal
|
|
588
|
+
# helpers are out of scope (they'd need the engine's routes.rb available; almost no static parser
|
|
589
|
+
# follows that). Mastodon uses `mount Sidekiq::Web, at: 'sidekiq', as: :sidekiq` and similar.
|
|
694
590
|
#
|
|
695
|
-
# When `as:` is omitted Rails derives a helper name
|
|
696
|
-
#
|
|
697
|
-
# statically, so we silently skip.
|
|
591
|
+
# When `as:` is omitted Rails derives a helper name from the engine class — that's harder to
|
|
592
|
+
# compute statically, so we silently skip.
|
|
698
593
|
def handle_mount(node, context)
|
|
699
594
|
options = options_hash(node)
|
|
700
595
|
as_name = options[:as]
|
|
@@ -710,15 +605,11 @@ module Rigor
|
|
|
710
605
|
)
|
|
711
606
|
end
|
|
712
607
|
|
|
713
|
-
# `use_doorkeeper do ... end` — Doorkeeper gem's
|
|
714
|
-
#
|
|
715
|
-
# `
|
|
716
|
-
#
|
|
717
|
-
#
|
|
718
|
-
# the project's omitted controllers don't register.
|
|
719
|
-
# `controllers <hash>` mappings inside the block change
|
|
720
|
-
# the serving controller class but not the helper
|
|
721
|
-
# names — they can stay unmodelled.
|
|
608
|
+
# `use_doorkeeper do ... end` — Doorkeeper gem's standard OAuth route helpers (`oauth_token_path`,
|
|
609
|
+
# `oauth_authorization_path`, `oauth_application_path`, etc.). We generate the full catalogue plus
|
|
610
|
+
# walk the block body for `skip_controllers <names>` calls so the project's omitted controllers
|
|
611
|
+
# don't register. `controllers <hash>` mappings inside the block change the serving controller
|
|
612
|
+
# class but not the helper names — they can stay unmodelled.
|
|
722
613
|
def handle_use_doorkeeper(node, context)
|
|
723
614
|
skip = collect_doorkeeper_skips(node)
|
|
724
615
|
DoorkeeperRoutes.generate(skip: skip).each do |entry|
|
|
@@ -750,37 +641,27 @@ module Rigor
|
|
|
750
641
|
|
|
751
642
|
# `scope "/:slug", as: "event" do ... end`
|
|
752
643
|
#
|
|
753
|
-
# When `as:` is present the block body is interpreted under a
|
|
754
|
-
#
|
|
755
|
-
#
|
|
756
|
-
# (`:slug`) are counted so nested-resource arities stay
|
|
757
|
-
# correct.
|
|
644
|
+
# When `as:` is present the block body is interpreted under a new `:as_scope` stack frame that
|
|
645
|
+
# adds the given prefix to every helper registered inside. Dynamic path segments (`:slug`) are
|
|
646
|
+
# counted so nested-resource arities stay correct.
|
|
758
647
|
#
|
|
759
|
-
# When `as:` is absent the block is interpreted without any
|
|
760
|
-
#
|
|
761
|
-
# path, which matches Rails' behaviour for path-only scopes.
|
|
648
|
+
# When `as:` is absent the block is interpreted without any prefix change — helper names are
|
|
649
|
+
# unaffected by the scope's path, which matches Rails' behaviour for path-only scopes.
|
|
762
650
|
def handle_scope(node, context)
|
|
763
651
|
as_name = keyword_symbol(node, :as)
|
|
764
|
-
# `scope :path_arg, as: :name` (path as a positional
|
|
765
|
-
#
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
# `:project_id` outer scopes. The leading `/` is added
|
|
769
|
-
# if missing so the path-prefix join produces clean
|
|
770
|
-
# segments.
|
|
652
|
+
# `scope :path_arg, as: :name` (path as a positional arg) vs `scope(path: ':project_id', as:
|
|
653
|
+
# :project)` (path as a `:path` keyword). GitLab's project routes rely on the latter for the
|
|
654
|
+
# `*namespace_id` / `:project_id` outer scopes. The leading `/` is added if missing so the
|
|
655
|
+
# path-prefix join produces clean segments.
|
|
771
656
|
path = string_argument(node, 0) || keyword_value_string(node, :path)
|
|
772
657
|
path = "/#{path}" if path && !path.start_with?("/")
|
|
773
658
|
arity = path ? count_path_placeholders(path) : 0
|
|
774
659
|
|
|
775
660
|
if as_name.nil?
|
|
776
|
-
# Even without `:as`, a `scope(path: 'groups/*id')`
|
|
777
|
-
#
|
|
778
|
-
#
|
|
779
|
-
# `scope(
|
|
780
|
-
# do; get :edit, as: :edit_group end` → arity 1).
|
|
781
|
-
# Skip the frame when there's also no path
|
|
782
|
-
# contribution (a pure `scope(module: :foo)` with
|
|
783
|
-
# no helper-name / path effect).
|
|
661
|
+
# Even without `:as`, a `scope(path: 'groups/*id')` still contributes its path / arity segments
|
|
662
|
+
# to helpers declared inside (GitLab's `scope(path: 'groups/*id', controller: :groups) do; get
|
|
663
|
+
# :edit, as: :edit_group end` → arity 1). Skip the frame when there's also no path contribution
|
|
664
|
+
# (a pure `scope(module: :foo)` with no helper-name / path effect).
|
|
784
665
|
return interpret_block_body(node, context) if path.nil? || arity.zero?
|
|
785
666
|
|
|
786
667
|
context.push_as_scope("", path, arity) do
|
|
@@ -801,13 +682,10 @@ module Rigor
|
|
|
801
682
|
options = effective_options_for(node, context)
|
|
802
683
|
actions = restrict_actions_from(options, DEFAULT_RESOURCE_ACTIONS)
|
|
803
684
|
base_arity = context.parent_segment_count
|
|
804
|
-
# `resources :collections, as: :actor_collections`
|
|
805
|
-
#
|
|
806
|
-
# `
|
|
807
|
-
# `
|
|
808
|
-
# are unaffected. Mastodon uses this inside a
|
|
809
|
-
# concern (`resources :collections, only: [:show],
|
|
810
|
-
# as: :actor_collections`).
|
|
685
|
+
# `resources :collections, as: :actor_collections` remaps the helper name family —
|
|
686
|
+
# `actor_collections_path` for index, `actor_collection_path(id)` for show. Path / arity are
|
|
687
|
+
# unaffected. Mastodon uses this inside a concern (`resources :collections, only: [:show], as:
|
|
688
|
+
# :actor_collections`).
|
|
811
689
|
helper_name = options[:as] || name
|
|
812
690
|
|
|
813
691
|
register_resourceful_helpers(helper_name, actions, base_arity, context, plural: true)
|
|
@@ -827,30 +705,23 @@ module Rigor
|
|
|
827
705
|
base_arity = context.parent_segment_count
|
|
828
706
|
helper_name = options[:as] || name
|
|
829
707
|
|
|
830
|
-
# Singular resource — no `:id` segment, no `:index`
|
|
831
|
-
# / pluralised helper. The "show" helper is
|
|
708
|
+
# Singular resource — no `:id` segment, no `:index` / pluralised helper. The "show" helper is
|
|
832
709
|
# `<name>_path` (singular).
|
|
833
710
|
register_resourceful_helpers(helper_name, actions, base_arity, context, plural: false)
|
|
834
711
|
|
|
835
|
-
# Push a `:singular_scope` frame so nested
|
|
836
|
-
#
|
|
837
|
-
#
|
|
838
|
-
#
|
|
839
|
-
# do; resources :domain_blocks; end; end` →
|
|
840
|
-
# `instance_domain_blocks_path`). The singular
|
|
841
|
-
# frame adds NO `:id` segment to arity — singular
|
|
842
|
-
# resources don't carry one.
|
|
712
|
+
# Push a `:singular_scope` frame so nested declarations pick up the singular resource's name in
|
|
713
|
+
# their helper prefix (Mastodon's `resource :instance do; scope module: :instances do; resources
|
|
714
|
+
# :domain_blocks; end; end` → `instance_domain_blocks_path`). The singular frame adds NO `:id`
|
|
715
|
+
# segment to arity — singular resources don't carry one.
|
|
843
716
|
context.push_singular_resource(name) do
|
|
844
717
|
replay_concerns(node, context)
|
|
845
718
|
interpret_block_body(node, context)
|
|
846
719
|
end
|
|
847
720
|
end
|
|
848
721
|
|
|
849
|
-
# `concern :account_resources do ... end` registers the
|
|
850
|
-
#
|
|
851
|
-
#
|
|
852
|
-
# context yet). Concerns at the top level land in the
|
|
853
|
-
# Context's `concerns` map by Symbol name.
|
|
722
|
+
# `concern :account_resources do ... end` registers the body for later replay; we DO NOT interpret
|
|
723
|
+
# it at the definition site (the body has no parent-resource context yet). Concerns at the top
|
|
724
|
+
# level land in the Context's `concerns` map by Symbol name.
|
|
854
725
|
def handle_concern_definition(node, context)
|
|
855
726
|
name = symbol_argument(node, 0)
|
|
856
727
|
body = node.block&.body
|
|
@@ -859,12 +730,9 @@ module Rigor
|
|
|
859
730
|
context.register_concern(name, body)
|
|
860
731
|
end
|
|
861
732
|
|
|
862
|
-
# `resources :accounts, concerns: :account_resources do ... end`
|
|
863
|
-
#
|
|
864
|
-
#
|
|
865
|
-
# resource frame pushed). Supports both single-symbol
|
|
866
|
-
# (`concerns: :name`) and array-of-symbols
|
|
867
|
-
# (`concerns: [:a, :b]`) forms.
|
|
733
|
+
# `resources :accounts, concerns: :account_resources do ... end` — replays the registered concern
|
|
734
|
+
# body inside the current Context (which already has the accounts resource frame pushed). Supports
|
|
735
|
+
# both single-symbol (`concerns: :name`) and array-of-symbols (`concerns: [:a, :b]`) forms.
|
|
868
736
|
def replay_concerns(resource_node, context)
|
|
869
737
|
replay_concerns_from_options(effective_options_for(resource_node, context), context)
|
|
870
738
|
end
|
|
@@ -881,33 +749,25 @@ module Rigor
|
|
|
881
749
|
end
|
|
882
750
|
end
|
|
883
751
|
|
|
884
|
-
# Returns the merged option hash for a node — the
|
|
885
|
-
#
|
|
886
|
-
#
|
|
887
|
-
#
|
|
888
|
-
# `resources :foo` inside `with_options only: [:index],
|
|
889
|
-
# concerns: :batch do ... end` is treated as if it
|
|
890
|
-
# had those options written inline.
|
|
752
|
+
# Returns the merged option hash for a node — the caller's own options override `with_options`
|
|
753
|
+
# defaults from the surrounding Context stack. Used by `handle_resources` / `handle_resource` so a
|
|
754
|
+
# bare `resources :foo` inside `with_options only: [:index], concerns: :batch do ... end` is
|
|
755
|
+
# treated as if it had those options written inline.
|
|
891
756
|
def effective_options_for(node, context)
|
|
892
757
|
context.with_options_defaults.merge(options_hash(node))
|
|
893
758
|
end
|
|
894
759
|
|
|
895
|
-
# Reads the value of an options-hash key. Distinct from
|
|
896
|
-
# `
|
|
897
|
-
# accepts EITHER a single Symbol or an Array of Symbols
|
|
898
|
-
# — same Rails idiom Rails accepts.
|
|
760
|
+
# Reads the value of an options-hash key. Distinct from `keyword_symbol` / `keyword_array` because
|
|
761
|
+
# `concerns:` accepts EITHER a single Symbol or an Array of Symbols — same Rails idiom Rails accepts.
|
|
899
762
|
def keyword_value(node, key)
|
|
900
763
|
options_hash(node)[key]
|
|
901
764
|
end
|
|
902
765
|
|
|
903
766
|
def handle_root(node, context)
|
|
904
|
-
# `root to: "..."` / `root "..."` — single helper
|
|
905
|
-
# `
|
|
906
|
-
#
|
|
907
|
-
#
|
|
908
|
-
# registers an additional `home_path` / `home_url` alias
|
|
909
|
-
# for the same path. Mastodon and Solidus also use the
|
|
910
|
-
# `as:` form occasionally for analytics-friendly URL
|
|
767
|
+
# `root to: "..."` / `root "..."` — single helper `root_path`, arity 0, GET. Real-world Rails
|
|
768
|
+
# apps also use `root :to => 'welcome#index', :as => 'home'` (the canonical Redmine idiom across
|
|
769
|
+
# 230+ call sites), which registers an additional `home_path` / `home_url` alias for the same
|
|
770
|
+
# path. Mastodon and Solidus also use the `as:` form occasionally for analytics-friendly URL
|
|
911
771
|
# naming.
|
|
912
772
|
path = context.path_prefix.empty? ? "/" : context.path_prefix
|
|
913
773
|
context.entries << HelperTable::Entry.new(
|
|
@@ -925,23 +785,17 @@ module Rigor
|
|
|
925
785
|
end
|
|
926
786
|
|
|
927
787
|
def handle_explicit_route(node, context)
|
|
928
|
-
# Member / collection block shorthand: `post :memorialize`
|
|
929
|
-
#
|
|
930
|
-
#
|
|
931
|
-
# action
|
|
932
|
-
# action becomes `<action>_<singular_chain>_path(id)`,
|
|
933
|
-
# a collection action becomes
|
|
934
|
-
# `<action>_<plural_chain>_path`.
|
|
788
|
+
# Member / collection block shorthand: `post :memorialize` inside `member do ... end` (no path
|
|
789
|
+
# arg, just a SymbolNode). Rails generates a helper based on the action name + the enclosing
|
|
790
|
+
# resource: a member action becomes `<action>_<singular_chain>_path(id)`, a collection action
|
|
791
|
+
# becomes `<action>_<plural_chain>_path`.
|
|
935
792
|
return register_member_collection_action(node, context) if member_collection_shorthand?(node, context)
|
|
936
793
|
|
|
937
|
-
# `:on => :collection` / `:on => :member` inline form
|
|
938
|
-
#
|
|
939
|
-
#
|
|
940
|
-
#
|
|
941
|
-
#
|
|
942
|
-
# `report_time_entries_path` (collection-style: action
|
|
943
|
-
# prefix on the plural resource name). Treat the action
|
|
944
|
-
# name as the path's basename string (Redmine's idiom).
|
|
794
|
+
# `:on => :collection` / `:on => :member` inline form — Rails accepts this as a shorthand for
|
|
795
|
+
# `collection do ... end` / `member do ... end` wrappers around a single action. `get 'report',
|
|
796
|
+
# :on => :collection` inside `resources :time_entries` generates `report_time_entries_path`
|
|
797
|
+
# (collection-style: action prefix on the plural resource name). Treat the action name as the
|
|
798
|
+
# path's basename string (Redmine's idiom).
|
|
945
799
|
on_target = keyword_symbol(node, :on)
|
|
946
800
|
if on_target && context.inside_resource_scope?
|
|
947
801
|
path_arg = string_argument(node, 0) || symbol_argument(node, 0)&.to_s
|
|
@@ -949,36 +803,27 @@ module Rigor
|
|
|
949
803
|
return register_on_target_action(node, context, as_name, on_target) if as_name
|
|
950
804
|
end
|
|
951
805
|
|
|
952
|
-
# `get "/about", to: "static#about", as: :about` —
|
|
953
|
-
# path as
|
|
954
|
-
#
|
|
955
|
-
#
|
|
956
|
-
# the trailing keyword/options hash (its value is the
|
|
957
|
-
# `controller#action`). Without this, the arity check
|
|
958
|
-
# underestimates because the placeholder count comes
|
|
959
|
-
# from a nil path.
|
|
806
|
+
# `get "/about", to: "static#about", as: :about` — path as the first positional arg. Also handle
|
|
807
|
+
# the hashrocket-key form `get 'help/*path' => 'help#show', as: :help_page` where the path is the
|
|
808
|
+
# String key in the trailing keyword/options hash (its value is the `controller#action`). Without
|
|
809
|
+
# this, the arity check underestimates because the placeholder count comes from a nil path.
|
|
960
810
|
path = string_argument(node, 0) || hashrocket_path_key(node)
|
|
961
811
|
as_name = keyword_symbol(node, :as)
|
|
962
812
|
return if as_name.nil? && path.nil?
|
|
963
813
|
|
|
964
|
-
# When `as:` is omitted, Rails derives a helper name
|
|
965
|
-
#
|
|
966
|
-
#
|
|
967
|
-
#
|
|
968
|
-
#
|
|
969
|
-
#
|
|
970
|
-
# slash — Rails reuses the enclosing as_scope's name.
|
|
971
|
-
# We surface that by registering the helper with an
|
|
972
|
-
# empty `as_name`, which `explicit_route_helper_name`
|
|
973
|
-
# then composes from `helper_prefix.chomp("_")`.
|
|
814
|
+
# When `as:` is omitted, Rails derives a helper name from the path for static paths (no
|
|
815
|
+
# :segment). We do the same when the path has no placeholders. The special case `get '/'` inside
|
|
816
|
+
# a `:as_scope` (e.g. `scope path: '/blob/*id', as: :blob do; get '/' end`) has empty path
|
|
817
|
+
# content after stripping the leading slash — Rails reuses the enclosing as_scope's name. We
|
|
818
|
+
# surface that by registering the helper with an empty `as_name`, which
|
|
819
|
+
# `explicit_route_helper_name` then composes from `helper_prefix.chomp("_")`.
|
|
974
820
|
if as_name.nil?
|
|
975
821
|
return if path.nil? || path.include?(":")
|
|
976
822
|
|
|
977
823
|
as_name = path.delete_prefix("/").tr("/", "_")
|
|
978
824
|
if as_name.empty? && context.helper_prefix.empty?
|
|
979
|
-
# `get '/'` style — only meaningful when the
|
|
980
|
-
#
|
|
981
|
-
# generate a root helper, handled elsewhere).
|
|
825
|
+
# `get '/'` style — only meaningful when the helper_prefix is non-empty (otherwise Rails
|
|
826
|
+
# would generate a root helper, handled elsewhere).
|
|
982
827
|
return
|
|
983
828
|
end
|
|
984
829
|
end
|
|
@@ -987,11 +832,9 @@ module Rigor
|
|
|
987
832
|
total_placeholders = count_path_placeholders(path)
|
|
988
833
|
optional = count_optional_path_placeholders(path)
|
|
989
834
|
base_arity = context.parent_segment_count + total_placeholders
|
|
990
|
-
# Register the maximum-arity entry; if the path
|
|
991
|
-
#
|
|
992
|
-
# `
|
|
993
|
-
# for each smaller arity so the call-site arity check
|
|
994
|
-
# accepts `settings_project_path(:id)` (no `:tab`).
|
|
835
|
+
# Register the maximum-arity entry; if the path carries `(...)` optional segments (e.g.
|
|
836
|
+
# `'settings(/:tab)'`), register additional entries for each smaller arity so the call-site
|
|
837
|
+
# arity check accepts `settings_project_path(:id)` (no `:tab`).
|
|
995
838
|
(0..optional).each do |drop|
|
|
996
839
|
context.entries << HelperTable::Entry.new(
|
|
997
840
|
name: name, arity: base_arity - drop,
|
|
@@ -1001,28 +844,21 @@ module Rigor
|
|
|
1001
844
|
end
|
|
1002
845
|
end
|
|
1003
846
|
|
|
1004
|
-
# Builds the helper name for an explicit `get` / `post`
|
|
1005
|
-
#
|
|
847
|
+
# Builds the helper name for an explicit `get` / `post` / `match` route. Rails uses two distinct
|
|
848
|
+
# shapes:
|
|
1006
849
|
#
|
|
1007
|
-
# - **Outside a `resources` scope** (top-level or inside
|
|
1008
|
-
# `
|
|
1009
|
-
#
|
|
1010
|
-
#
|
|
1011
|
-
# `
|
|
1012
|
-
#
|
|
1013
|
-
#
|
|
1014
|
-
# `resources :projects do; get 'settings', as:
|
|
1015
|
-
# :settings; end` → `settings_project_path(:id)` (NOT
|
|
1016
|
-
# `project_settings_path`). The namespace prefix still
|
|
1017
|
-
# leads: `namespace :admin do; resources :projects do;
|
|
1018
|
-
# get 'foo', as: :bar; end; end` →
|
|
850
|
+
# - **Outside a `resources` scope** (top-level or inside `namespace` / `scope`):
|
|
851
|
+
# `<namespace_prefix><as>_path` — e.g. `match 'login', as: :signin` → `signin_path`; inside
|
|
852
|
+
# `namespace :admin` → `admin_signin_path`.
|
|
853
|
+
# - **Inside a `resources` scope**: the `:as` acts as the ACTION prefix on the resource's singular
|
|
854
|
+
# chain. `resources :projects do; get 'settings', as: :settings; end` →
|
|
855
|
+
# `settings_project_path(:id)` (NOT `project_settings_path`). The namespace prefix still leads:
|
|
856
|
+
# `namespace :admin do; resources :projects do; get 'foo', as: :bar; end; end` →
|
|
1019
857
|
# `admin_bar_project_path(:id)`.
|
|
1020
858
|
def explicit_route_helper_name(context, as_name)
|
|
1021
|
-
# Empty `as_name` is the `get '/'`-inside-an-as_scope
|
|
1022
|
-
#
|
|
1023
|
-
#
|
|
1024
|
-
# helper_prefix as-is (drop the trailing underscore)
|
|
1025
|
-
# — Rails reuses the enclosing scope's name verbatim.
|
|
859
|
+
# Empty `as_name` is the `get '/'`-inside-an-as_scope case (e.g. `scope path: '/blob/*id', as:
|
|
860
|
+
# :blob do; get '/' end` → `blob_path(:id)`). Use the helper_prefix as-is (drop the trailing
|
|
861
|
+
# underscore) — Rails reuses the enclosing scope's name verbatim.
|
|
1026
862
|
return "#{context.helper_prefix.chomp('_')}_path" if as_name.to_s.empty?
|
|
1027
863
|
|
|
1028
864
|
if context.inside_resource_scope?
|
|
@@ -1032,25 +868,20 @@ module Rigor
|
|
|
1032
868
|
end
|
|
1033
869
|
end
|
|
1034
870
|
|
|
1035
|
-
# True when the call is `<verb> :symbol [, options...]`
|
|
1036
|
-
#
|
|
1037
|
-
# - a `
|
|
1038
|
-
#
|
|
1039
|
-
#
|
|
1040
|
-
#
|
|
1041
|
-
# Rails defaults a bare symbol- or string-named verb
|
|
1042
|
-
# inside resources to a member action (GitLab's
|
|
1043
|
-
# `resource :application_settings do; match :general,
|
|
1044
|
-
# via: [...] end` → `general_application_settings_path`).
|
|
871
|
+
# True when the call is `<verb> :symbol [, options...]` or `<verb> 'string'` inside one of:
|
|
872
|
+
# - a `member do ... end` / `collection do ... end` block (canonical shorthand context), or
|
|
873
|
+
# - a `resources :name do ... end` / `resource :name do ... end` block at the *direct* nesting
|
|
874
|
+
# level. Rails defaults a bare symbol- or string-named verb inside resources to a member action
|
|
875
|
+
# (GitLab's `resource :application_settings do; match :general, via: [...] end` →
|
|
876
|
+
# `general_application_settings_path`).
|
|
1045
877
|
def member_collection_shorthand?(node, context)
|
|
1046
878
|
return false unless context.innermost_action_block || context.inside_resource_scope?
|
|
1047
879
|
|
|
1048
880
|
first_arg = node.arguments&.arguments&.first
|
|
1049
881
|
return true if first_arg.is_a?(Prism::SymbolNode)
|
|
1050
882
|
|
|
1051
|
-
# A plain action-name string with no `/` or `:` —
|
|
1052
|
-
#
|
|
1053
|
-
# `get :report` inside member/collection block).
|
|
883
|
+
# A plain action-name string with no `/` or `:` — treat it as if it were a symbol (`get 'report'`
|
|
884
|
+
# == `get :report` inside member/collection block).
|
|
1054
885
|
first_arg.is_a?(Prism::StringNode) &&
|
|
1055
886
|
!first_arg.unescaped.include?("/") &&
|
|
1056
887
|
!first_arg.unescaped.include?(":")
|
|
@@ -1058,20 +889,16 @@ module Rigor
|
|
|
1058
889
|
|
|
1059
890
|
# Generates the member / collection action helper.
|
|
1060
891
|
# Member: `<action>_<helper_prefix>path(id)`, arity =
|
|
1061
|
-
# parent_segment_count (which already includes the
|
|
1062
|
-
# enclosing resource's `:id`).
|
|
892
|
+
# parent_segment_count (which already includes the enclosing resource's `:id`).
|
|
1063
893
|
# Collection: `<action>_<plural_helper_prefix>path`,
|
|
1064
|
-
# arity = parent_segment_count - 1 (no `:id` segment;
|
|
1065
|
-
# the collection URL is /<resource>/<action>).
|
|
894
|
+
# arity = parent_segment_count - 1 (no `:id` segment; the collection URL is /<resource>/<action>).
|
|
1066
895
|
def register_member_collection_action(node, context)
|
|
1067
896
|
action_name = symbol_argument(node, 0)&.to_s ||
|
|
1068
897
|
string_argument(node, 0).to_s
|
|
1069
898
|
frame = context.innermost_action_block
|
|
1070
|
-
# No `member do` / `collection do` wrapper but we're
|
|
1071
|
-
# inside a
|
|
1072
|
-
#
|
|
1073
|
-
# action (e.g. `get :preview` → `preview_<singular>_path
|
|
1074
|
-
# (:id)`).
|
|
899
|
+
# No `member do` / `collection do` wrapper but we're inside a resources block — Rails defaults a
|
|
900
|
+
# bare `<verb> :symbol` inside resources to a MEMBER action (e.g. `get :preview` →
|
|
901
|
+
# `preview_<singular>_path(:id)`).
|
|
1075
902
|
if frame.nil?
|
|
1076
903
|
register_member_action(node, context, action_name)
|
|
1077
904
|
elsif frame[:kind] == :member_block
|
|
@@ -1081,12 +908,9 @@ module Rigor
|
|
|
1081
908
|
end
|
|
1082
909
|
end
|
|
1083
910
|
|
|
1084
|
-
# `get 'report', :on => :collection` (or `:member`)
|
|
1085
|
-
#
|
|
1086
|
-
#
|
|
1087
|
-
# name prefixes the resource's singular (member) or
|
|
1088
|
-
# plural (collection). Closes Redmine's
|
|
1089
|
-
# `report_time_entries_path` cluster.
|
|
911
|
+
# `get 'report', :on => :collection` (or `:member`) inline form inside `resources`. The helper name
|
|
912
|
+
# shape matches member_action / collection_action: the action name prefixes the resource's
|
|
913
|
+
# singular (member) or plural (collection). Closes Redmine's `report_time_entries_path` cluster.
|
|
1090
914
|
def register_on_target_action(node, context, action_name, on_target)
|
|
1091
915
|
scope_frame = context.innermost_scope_frame
|
|
1092
916
|
return if scope_frame.nil?
|
|
@@ -1114,12 +938,9 @@ module Rigor
|
|
|
1114
938
|
end
|
|
1115
939
|
|
|
1116
940
|
def register_member_action(node, context, action_name)
|
|
1117
|
-
# `helper_prefix` already ends with "_" (each segment
|
|
1118
|
-
#
|
|
1119
|
-
#
|
|
1120
|
-
# parent segment count — Rails member URLs carry the
|
|
1121
|
-
# enclosing resource's `:id`, which the :scope frame
|
|
1122
|
-
# already counts.
|
|
941
|
+
# `helper_prefix` already ends with "_" (each segment appends one); the formula below yields e.g.
|
|
942
|
+
# `memorialize_admin_account_path`. Arity equals the parent segment count — Rails member URLs
|
|
943
|
+
# carry the enclosing resource's `:id`, which the :scope frame already counts.
|
|
1123
944
|
name = "#{action_name}_#{context.helper_prefix}path"
|
|
1124
945
|
context.entries << HelperTable::Entry.new(
|
|
1125
946
|
name: name, arity: context.parent_segment_count,
|
|
@@ -1129,10 +950,8 @@ module Rigor
|
|
|
1129
950
|
end
|
|
1130
951
|
|
|
1131
952
|
def register_collection_action(node, context, action_name, frame)
|
|
1132
|
-
# Collection URL drops the immediate parent's `:id`
|
|
1133
|
-
#
|
|
1134
|
-
# singular form (in `helper_prefix`) for the plural
|
|
1135
|
-
# — the immediate-resource frame stored both.
|
|
953
|
+
# Collection URL drops the immediate parent's `:id` segment. The plural helper prefix swaps the
|
|
954
|
+
# singular form (in `helper_prefix`) for the plural — the immediate-resource frame stored both.
|
|
1136
955
|
plural_prefix = context.helper_prefix.sub(/#{frame[:parent_singular]}_\z/, "#{frame[:parent_plural]}_")
|
|
1137
956
|
name = "#{action_name}_#{plural_prefix}path"
|
|
1138
957
|
arity = [context.parent_segment_count - 1, 0].max
|
|
@@ -1159,8 +978,7 @@ module Rigor
|
|
|
1159
978
|
end
|
|
1160
979
|
|
|
1161
980
|
def handle_member_or_collection(node, context)
|
|
1162
|
-
# Only meaningful when we're inside a `resources` /
|
|
1163
|
-
# `resource` block. The Context's stack tells us.
|
|
981
|
+
# Only meaningful when we're inside a `resources` / `resource` block. The Context's stack tells us.
|
|
1164
982
|
resource = context.innermost_resource
|
|
1165
983
|
return interpret_block_body(node, context) if resource.nil?
|
|
1166
984
|
|
|
@@ -1171,25 +989,20 @@ module Rigor
|
|
|
1171
989
|
end
|
|
1172
990
|
|
|
1173
991
|
def in_singular_resource?(*)
|
|
1174
|
-
# Stub: always returns true so member / collection
|
|
1175
|
-
#
|
|
1176
|
-
#
|
|
1177
|
-
# a future caller could use it to tighten this check.
|
|
992
|
+
# Stub: always returns true so member / collection blocks descend. The singular-resource frame
|
|
993
|
+
# (`push_singular_resource`) is modelled in Context; a future caller could use it to tighten this
|
|
994
|
+
# check.
|
|
1178
995
|
true
|
|
1179
996
|
end
|
|
1180
997
|
|
|
1181
|
-
# Generate the standard helpers for a resource(s).
|
|
1182
|
-
#
|
|
1183
|
-
# `resource :profile`.
|
|
998
|
+
# Generate the standard helpers for a resource(s). `plural: true` for `resources :users`, `false`
|
|
999
|
+
# for `resource :profile`.
|
|
1184
1000
|
def register_resourceful_helpers(name, actions, base_arity, context, plural:)
|
|
1185
|
-
# Plural resources (`resources :users`) singularise
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
# :relationships` → `relationships_path`, not
|
|
1191
|
-
# `relationship_path`. The URL path uses `name`
|
|
1192
|
-
# in both shapes (Rails never singularises the URL).
|
|
1001
|
+
# Plural resources (`resources :users`) singularise for show / new / edit helpers →
|
|
1002
|
+
# `user_path(id)`. Singular resources (`resource :foo`) use the name AS-IS — singularising would
|
|
1003
|
+
# mangle deliberately-plural names like Mastodon's `resource :relationships` →
|
|
1004
|
+
# `relationships_path`, not `relationship_path`. The URL path uses `name` in both shapes (Rails
|
|
1005
|
+
# never singularises the URL).
|
|
1193
1006
|
singular = plural ? singularize_word(name.to_s) : name.to_s
|
|
1194
1007
|
path_base = "#{context.path_prefix}/#{name}"
|
|
1195
1008
|
|
|
@@ -1203,20 +1016,13 @@ module Rigor
|
|
|
1203
1016
|
end
|
|
1204
1017
|
end
|
|
1205
1018
|
|
|
1206
|
-
# Maps an action keyword to the route-helper entry it
|
|
1207
|
-
#
|
|
1208
|
-
# (`:
|
|
1209
|
-
#
|
|
1210
|
-
#
|
|
1211
|
-
#
|
|
1212
|
-
#
|
|
1213
|
-
# we emit them too, otherwise an `only: [:create]`
|
|
1214
|
-
# resource (e.g. Mastodon's `resource :inbox, only:
|
|
1215
|
-
# [:create]`) registers NO helpers and downstream
|
|
1216
|
-
# callers see a false `unknown-helper inbox_path`.
|
|
1217
|
-
# The HelperTable already dedupes by name, so a
|
|
1218
|
-
# resource that lists both `:show` and `:update` does
|
|
1219
|
-
# not double-register.
|
|
1019
|
+
# Maps an action keyword to the route-helper entry it produces. The five "named-helper" actions
|
|
1020
|
+
# (`:index` / `:show` / `:new` / `:edit` plus singular-resource `:show`) generate a distinct
|
|
1021
|
+
# helper; the three "verb-only" actions (`:create` / `:update` / `:destroy`) Rails serves under the
|
|
1022
|
+
# same path-helper Rails reuses for show / index forms — so we emit them too, otherwise an `only:
|
|
1023
|
+
# [:create]` resource (e.g. Mastodon's `resource :inbox, only: [:create]`) registers NO helpers and
|
|
1024
|
+
# downstream callers see a false `unknown-helper inbox_path`. The HelperTable already dedupes by
|
|
1025
|
+
# name, so a resource that lists both `:show` and `:update` does not double-register.
|
|
1220
1026
|
def entry_for_action(action, name:, singular:, base_arity:, path_base:, helper_prefix:, plural:)
|
|
1221
1027
|
case action
|
|
1222
1028
|
when :index then index_entry(plural, helper_prefix, name, base_arity, path_base, singular)
|
|
@@ -1229,18 +1035,14 @@ module Rigor
|
|
|
1229
1035
|
)
|
|
1230
1036
|
when :edit then edit_entry(plural, helper_prefix, singular, base_arity, path_base)
|
|
1231
1037
|
when :create
|
|
1232
|
-
# Plural `resources` collection POST shares the
|
|
1233
|
-
#
|
|
1234
|
-
#
|
|
1235
|
-
# (`<name>_path` → resource URL). Both shapes
|
|
1236
|
-
# produce a `<prefix><name>_path` entry; only the
|
|
1237
|
-
# arity / path differ.
|
|
1038
|
+
# Plural `resources` collection POST shares the index helper (`<name>_path` → collection URL).
|
|
1039
|
+
# Singular `resource` POST shares the show helper (`<name>_path` → resource URL). Both shapes
|
|
1040
|
+
# produce a `<prefix><name>_path` entry; only the arity / path differ.
|
|
1238
1041
|
create_entry(plural, helper_prefix, name, singular, base_arity, path_base)
|
|
1239
1042
|
when :update, :destroy
|
|
1240
|
-
# Member PATCH / PUT / DELETE on plural resources
|
|
1241
|
-
#
|
|
1242
|
-
#
|
|
1243
|
-
# `<prefix><name>_path` (no `:id`).
|
|
1043
|
+
# Member PATCH / PUT / DELETE on plural resources share the show helper
|
|
1044
|
+
# (`<prefix><singular>_path(id)`). Singular-resource PATCH / DELETE shares `<prefix><name>_path`
|
|
1045
|
+
# (no `:id`).
|
|
1244
1046
|
show_entry(plural, helper_prefix, singular, base_arity, path_base)
|
|
1245
1047
|
end
|
|
1246
1048
|
end
|
|
@@ -1264,27 +1066,18 @@ module Rigor
|
|
|
1264
1066
|
def index_entry(plural, helper_prefix, name, base_arity, path_base, singular)
|
|
1265
1067
|
return nil unless plural
|
|
1266
1068
|
|
|
1267
|
-
# Rails appends `_index_path` to the index helper
|
|
1268
|
-
#
|
|
1269
|
-
#
|
|
1270
|
-
#
|
|
1271
|
-
#
|
|
1272
|
-
#
|
|
1273
|
-
#
|
|
1274
|
-
# `resources :reblogged_by, controller:
|
|
1275
|
-
# :reblogged_by_accounts, only: :index` and similar
|
|
1276
|
-
# rely on this — calls like
|
|
1277
|
-
# `api_v1_status_reblogged_by_index_url(status.id)`
|
|
1278
|
-
# would otherwise read as `unknown-helper`.
|
|
1069
|
+
# Rails appends `_index_path` to the index helper name when the singular form of the resource
|
|
1070
|
+
# matches the plural form AND the noun isn't in the canonical UNCOUNTABLE list. The collision
|
|
1071
|
+
# would otherwise put both the index (`:id`-less) and show (`:id`-bearing) helpers under the same
|
|
1072
|
+
# name, and Rails disambiguates by suffixing the index form. Mastodon's `resources
|
|
1073
|
+
# :reblogged_by, controller: :reblogged_by_accounts, only: :index` and similar rely on this —
|
|
1074
|
+
# calls like `api_v1_status_reblogged_by_index_url(status.id)` would otherwise read as
|
|
1075
|
+
# `unknown-helper`.
|
|
1279
1076
|
#
|
|
1280
|
-
# Rails adds `_index_` whenever `singular == plural`
|
|
1281
|
-
#
|
|
1282
|
-
#
|
|
1283
|
-
#
|
|
1284
|
-
# was empirically wrong; only the
|
|
1285
|
-
# IRREGULAR-singular path (e.g. `media → medium`)
|
|
1286
|
-
# actually has `singular != plural` and skips
|
|
1287
|
-
# `_index_`.
|
|
1077
|
+
# Rails adds `_index_` whenever `singular == plural` — including for UNCOUNTABLE nouns (Redmine's
|
|
1078
|
+
# `resources :news` registers `news_index_path` for the index). The earlier "skip UNCOUNTABLE"
|
|
1079
|
+
# carve-out was empirically wrong; only the IRREGULAR-singular path (e.g. `media → medium`)
|
|
1080
|
+
# actually has `singular != plural` and skips `_index_`.
|
|
1288
1081
|
index_name = if name.to_s == singular
|
|
1289
1082
|
"#{helper_prefix}#{name}_index_path"
|
|
1290
1083
|
else
|
|
@@ -1322,9 +1115,8 @@ module Rigor
|
|
|
1322
1115
|
end
|
|
1323
1116
|
|
|
1324
1117
|
def restrict_actions_from(options, default)
|
|
1325
|
-
# `resources :foo, only: :show` is the same as
|
|
1326
|
-
#
|
|
1327
|
-
# Symbol shape from the source, so coerce here.
|
|
1118
|
+
# `resources :foo, only: :show` is the same as `only: [:show]` in Rails; `options_hash` preserves
|
|
1119
|
+
# the Symbol shape from the source, so coerce here.
|
|
1328
1120
|
if (only = options[:only])
|
|
1329
1121
|
Array(only) & default
|
|
1330
1122
|
elsif (except = options[:except])
|
|
@@ -1362,21 +1154,17 @@ module Rigor
|
|
|
1362
1154
|
options_hash(node)[key]
|
|
1363
1155
|
end
|
|
1364
1156
|
|
|
1365
|
-
# Reads a keyword option whose value is a literal String
|
|
1366
|
-
#
|
|
1367
|
-
#
|
|
1368
|
-
# ':project_id', ...)` shape parsing where `:path` is
|
|
1369
|
-
# passed as a keyword rather than a positional arg.
|
|
1157
|
+
# Reads a keyword option whose value is a literal String (returned as-is). Returns nil when the key
|
|
1158
|
+
# is missing or the value is non-literal. Used for `scope(path: ':project_id', ...)` shape parsing
|
|
1159
|
+
# where `:path` is passed as a keyword rather than a positional arg.
|
|
1370
1160
|
def keyword_value_string(node, key)
|
|
1371
1161
|
value = options_hash(node)[key]
|
|
1372
1162
|
value.is_a?(String) ? value : nil
|
|
1373
1163
|
end
|
|
1374
1164
|
|
|
1375
|
-
# `get 'help/*path' => 'help#show', as: :help_page` —
|
|
1376
|
-
# the
|
|
1377
|
-
#
|
|
1378
|
-
# `controller#action` String). Returns the path String,
|
|
1379
|
-
# or nil when no such hashrocket-key entry exists.
|
|
1165
|
+
# `get 'help/*path' => 'help#show', as: :help_page` — the path lives in a String-keyed AssocNode of
|
|
1166
|
+
# the trailing keyword/options hash (its value is the `controller#action` String). Returns the
|
|
1167
|
+
# path String, or nil when no such hashrocket-key entry exists.
|
|
1380
1168
|
def hashrocket_path_key(node)
|
|
1381
1169
|
args = node.arguments&.arguments || []
|
|
1382
1170
|
last = args.last
|
|
@@ -1409,29 +1197,24 @@ module Rigor
|
|
|
1409
1197
|
def count_path_placeholders(path)
|
|
1410
1198
|
return 0 if path.nil?
|
|
1411
1199
|
|
|
1412
|
-
# Both `:name` (regular segment) and `*name` (wildcard
|
|
1413
|
-
#
|
|
1414
|
-
#
|
|
1415
|
-
# `path: '*namespace_id'` outer scope adds 1 to the
|
|
1416
|
-
# arity of every helper underneath.
|
|
1200
|
+
# Both `:name` (regular segment) and `*name` (wildcard / "globbing" segment) are Rails path
|
|
1201
|
+
# parameters and contribute to helper arity. GitLab's `path: '*namespace_id'` outer scope adds 1
|
|
1202
|
+
# to the arity of every helper underneath.
|
|
1417
1203
|
path.scan(/[:*][a-z_][a-z0-9_]*/).size
|
|
1418
1204
|
end
|
|
1419
1205
|
|
|
1420
|
-
# Number of placeholders inside `(...)` groups — Rails'
|
|
1421
|
-
#
|
|
1422
|
-
#
|
|
1423
|
-
# callers accepting a `(required..required+optional)`
|
|
1424
|
-
# arity range register an entry per achievable arity.
|
|
1206
|
+
# Number of placeholders inside `(...)` groups — Rails' optional-segment syntax
|
|
1207
|
+
# (`'settings(/:tab)'`). Required arity is `count_path_placeholders - count_optional`; callers
|
|
1208
|
+
# accepting a `(required..required+optional)` arity range register an entry per achievable arity.
|
|
1425
1209
|
def count_optional_path_placeholders(path)
|
|
1426
1210
|
return 0 if path.nil?
|
|
1427
1211
|
|
|
1428
1212
|
path.scan(/\([^)]*\)/).sum { |group| group.scan(/[:*][a-z_][a-z0-9_]*/).size }
|
|
1429
1213
|
end
|
|
1430
1214
|
|
|
1431
|
-
# ADR-39 — delegates to the shared inflector (real
|
|
1432
|
-
# `
|
|
1433
|
-
#
|
|
1434
|
-
# both now share the one authoritative implementation.
|
|
1215
|
+
# ADR-39 — delegates to the shared inflector (real `ActiveSupport::Inflector` when available).
|
|
1216
|
+
# Previously a hand-tuned AS-replica kept in sync with `Context#singularize`; both now share the
|
|
1217
|
+
# one authoritative implementation.
|
|
1435
1218
|
def singularize_word(word)
|
|
1436
1219
|
Rigor::Plugin::Inflector.singularize(word)
|
|
1437
1220
|
end
|