From 5f042926a8ebf5e4ed33da7c1492349a01d6e140 Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Sat, 26 Aug 2023 10:16:36 -0400 Subject: [PATCH] Fix slot names that start with call When a component is registered that starts with `call` the compiler detects it as a template method which is not correct. This changes the compiler to look for `call(_|$)` instead of `call` to avoid this issue. This means that slots can't (and couldn't) start with `call_` since we still rely on the `call_` naming convention to generate the template methods. To make this more dev friendly, this adds a check that raises an error if a slot name starts with `call_`. A more long-term fix would be to use some kind of template method container instead of relying on the `call_` naming convention. Fixes https://github.com/ViewComponent/view_component/issues/1825 --- lib/view_component/compiler.rb | 4 ++-- lib/view_component/errors.rb | 9 ++++++--- lib/view_component/slotable.rb | 8 ++++++++ test/sandbox/app/components/header_component.rb | 5 +++++ test/sandbox/test/slotable_test.rb | 14 ++++++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 test/sandbox/app/components/header_component.rb diff --git a/lib/view_component/compiler.rb b/lib/view_component/compiler.rb index 72c7367c4..8a343e3eb 100644 --- a/lib/view_component/compiler.rb +++ b/lib/view_component/compiler.rb @@ -219,12 +219,12 @@ def inline_calls component_class.included_modules ) - view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call/) }.uniq + view_component_ancestors.flat_map { |ancestor| ancestor.instance_methods(false).grep(/^call(_|$)/) }.uniq end end def inline_calls_defined_on_self - @inline_calls_defined_on_self ||= component_class.instance_methods(false).grep(/^call/) + @inline_calls_defined_on_self ||= instance_methods = component_class.instance_methods(false).grep(/^call(_|$)/) end def variants diff --git a/lib/view_component/errors.rb b/lib/view_component/errors.rb index 36f9ee570..b896f53d1 100644 --- a/lib/view_component/errors.rb +++ b/lib/view_component/errors.rb @@ -104,7 +104,10 @@ class InvalidSlotDefinitionError < BaseError "string, or callable (that is proc, lambda, etc)" end - class SlotPredicateNameError < StandardError + class InvalidSlotNameError < StandardError + end + + class SlotPredicateNameError < InvalidSlotNameError MESSAGE = "COMPONENT declares a slot named SLOT_NAME, which ends with a question mark.\n\n" \ "This isn't allowed because the ViewComponent framework already provides predicate " \ @@ -126,7 +129,7 @@ def initialize(klass_name, slot_name) end end - class ReservedSingularSlotNameError < StandardError + class ReservedSingularSlotNameError < InvalidSlotNameError MESSAGE = "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \ "To fix this issue, choose a different name." @@ -136,7 +139,7 @@ def initialize(klass_name, slot_name) end end - class ReservedPluralSlotNameError < StandardError + class ReservedPluralSlotNameError < InvalidSlotNameError MESSAGE = "COMPONENT declares a slot named SLOT_NAME, which is a reserved word in the ViewComponent framework.\n\n" \ "To fix this issue, choose a different name." diff --git a/lib/view_component/slotable.rb b/lib/view_component/slotable.rb index ecb4826a2..d46094b3c 100644 --- a/lib/view_component/slotable.rb +++ b/lib/view_component/slotable.rb @@ -295,6 +295,7 @@ def validate_plural_slot_name(slot_name) raise ReservedPluralSlotNameError.new(name, slot_name) end + raise_if_slot_conflicts_with_call(slot_name) raise_if_slot_ends_with_question_mark(slot_name) raise_if_slot_registered(slot_name) end @@ -308,6 +309,7 @@ def validate_singular_slot_name(slot_name) raise ReservedSingularSlotNameError.new(name, slot_name) end + raise_if_slot_conflicts_with_call(slot_name) raise_if_slot_ends_with_question_mark(slot_name) raise_if_slot_registered(slot_name) end @@ -322,6 +324,12 @@ def raise_if_slot_registered(slot_name) def raise_if_slot_ends_with_question_mark(slot_name) raise SlotPredicateNameError.new(name, slot_name) if slot_name.to_s.ends_with?("?") end + + def raise_if_slot_conflicts_with_call(slot_name) + if slot_name.start_with?("call_") + raise InvalidSlotNameError, "Slot cannot start with 'call_'. Please rename #{slot_name}" + end + end end def get_slot(slot_name) diff --git a/test/sandbox/app/components/header_component.rb b/test/sandbox/app/components/header_component.rb new file mode 100644 index 000000000..8a8d8406c --- /dev/null +++ b/test/sandbox/app/components/header_component.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class HeaderComponent < ViewComponent::Base + renders_one :callout_title +end diff --git a/test/sandbox/test/slotable_test.rb b/test/sandbox/test/slotable_test.rb index 3a931167e..dc3c1f6b8 100644 --- a/test/sandbox/test/slotable_test.rb +++ b/test/sandbox/test/slotable_test.rb @@ -719,4 +719,18 @@ def test_slot_with_content_shorthand assert component.title.content? end + + def test_slot_names_can_not_start_with_call_ + assert_raises ViewComponent::InvalidSlotNameError do + Class.new(ViewComponent::Base) do + renders_one :call_out_title + end + end + + assert_raises ViewComponent::InvalidSlotNameError do + Class.new(ViewComponent::Base) do + renders_many :call_out_titles + end + end + end end