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