Skip to content

Commit

Permalink
add configuration and support for freezing string literals in compile…
Browse files Browse the repository at this point in the history
…d templates
  • Loading branch information
mitchellhenke committed Oct 25, 2023
1 parent b1aee6d commit 500a675
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 13 deletions.
8 changes: 4 additions & 4 deletions lib/view_component/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def set_original_view_context(view_context)
#
# @return [String]
def render_in(view_context, &block)
self.class.compile(raise_errors: true)
self.class.compile(raise_errors: true, frozen_string_literal: ViewComponent::Base.config.frozen_string_literal)

@view_context = view_context
self.__vc_original_view_context ||= view_context
Expand Down Expand Up @@ -466,7 +466,7 @@ def inherited(child)
def render_template_for(variant = nil)
# Force compilation here so the compiler always redefines render_template_for.
# This is mostly a safeguard to prevent infinite recursion.
self.class.compile(raise_errors: true, force: true)
self.class.compile(raise_errors: true, force: true, frozen_string_literal: #{ViewComponent::Base.config.frozen_string_literal})
# .compile replaces this method; call the new one
render_template_for(variant)
end
Expand Down Expand Up @@ -519,8 +519,8 @@ def ensure_compiled
# Do as much work as possible in this step, as doing so reduces the amount
# of work done each time a component is rendered.
# @private
def compile(raise_errors: false, force: false)
compiler.compile(raise_errors: raise_errors, force: force)
def compile(raise_errors: false, force: false, frozen_string_literal: false)
compiler.compile(raise_errors: raise_errors, force: force, frozen_string_literal: frozen_string_literal)
end

# @private
Expand Down
26 changes: 18 additions & 8 deletions lib/view_component/compiler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def development?
self.class.mode == DEVELOPMENT_MODE
end

def compile(raise_errors: false, force: false)
def compile(raise_errors: false, force: false, frozen_string_literal: false)
return if compiled? && !force
return if component_class == ViewComponent::Base

Expand All @@ -48,12 +48,17 @@ def compile(raise_errors: false, force: false)

redefinition_lock.synchronize do
component_class.silence_redefinition_of_method("call")
# rubocop:disable Style/EvalWithLocation
component_class.class_eval <<-RUBY, template.path, template.lineno
source = <<-SOURCE
def call
#{compiled_inline_template(template)}
end
RUBY
SOURCE
# rubocop:disable Style/EvalWithLocation
if frozen_string_literal
component_class.class_eval("# frozen_string_literal: true\n#{source}", template.path, template.lineno - 1)
else
component_class.class_eval(source, template.path, template.lineno)
end
# rubocop:enable Style/EvalWithLocation

component_class.define_method("_call_#{safe_class_name}", component_class.instance_method(:call))
Expand All @@ -71,12 +76,17 @@ def render_template_for(variant = nil)

redefinition_lock.synchronize do
component_class.silence_redefinition_of_method(method_name)
source = <<-SOURCE
def #{method_name}
#{compiled_template(template[:path])}
end
SOURCE
# rubocop:disable Style/EvalWithLocation
component_class.class_eval <<-RUBY, template[:path], 0
def #{method_name}
#{compiled_template(template[:path])}
if frozen_string_literal
component_class.class_eval("# frozen_string_literal: true\n#{source}", template[:path], -1)
else
component_class.class_eval(source, template[:path], 0)
end
RUBY
# rubocop:enable Style/EvalWithLocation
end
end
Expand Down
8 changes: 7 additions & 1 deletion lib/view_component/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def defaults
preview_paths: default_preview_paths,
test_controller: "ApplicationController",
default_preview_layout: nil,
capture_compatibility_patch_enabled: false
capture_compatibility_patch_enabled: false,
frozen_string_literal: false
})
end

Expand Down Expand Up @@ -154,6 +155,11 @@ def defaults
# previews.
# Defaults to `false`.

# @!attribute frozen_string_literal
# @return [Boolean]
# Enables compiling templates with frozen_string_literal
# Defaults to `false`.

def default_preview_paths
return [] unless defined?(Rails.root) && Dir.exist?("#{Rails.root}/test/components/previews")

Expand Down
1 change: 1 addition & 0 deletions test/sandbox/test/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def test_defaults_are_correct
assert_equal @config.render_monkey_patch_enabled, true
assert_equal @config.show_previews, true
assert_equal @config.preview_paths, ["#{Rails.root}/test/components/previews"]
assert_equal @config.frozen_string_literal, false
end

def test_all_methods_are_documented
Expand Down
24 changes: 24 additions & 0 deletions test/sandbox/test/rendering_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1094,4 +1094,28 @@ def test_content_security_policy_nonce

assert_selector("script", text: "\n//<![CDATA[\n \"alert('hello')\"\n\n//]]>\n", visible: :hidden)
end

def test_frozen_string_literal_disabled
old_value = ViewComponent::Base.config.frozen_string_literal
ViewComponent::Base.config.frozen_string_literal = false

with_new_cache do
render_inline(MutatedStringComponent.new)
assert_includes rendered_content, "ab"
end
ensure
ViewComponent::Base.config.frozen_string_literal = old_value
end

def test_frozen_string_literal_enabled
old_value = ViewComponent::Base.config.frozen_string_literal
ViewComponent::Base.config.frozen_string_literal = true
with_new_cache do
assert_raises FrozenError do
render_inline(MutatedStringComponent.new)
end
end
ensure
ViewComponent::Base.config.frozen_string_literal = old_value
end
end

0 comments on commit 500a675

Please sign in to comment.