lib/action_view/render_parser.rb
# frozen_string_literal: true require "action_view/ripper_ast_parser" module ActionView class RenderParser # :nodoc: def initialize(name, code) @name = name @code = code @parser = RipperASTParser end def render_calls render_nodes = @parser.parse_render_nodes(@code) render_nodes.map do |method, nodes| nodes.map { |n| send(:parse_render, n) } end.flatten.compact end private def directory File.dirname(@name) end def resolve_path_directory(path) if path.include?("/") path else "#{directory}/#{path}" end end # Convert # render("foo", ...) # into either # render(template: "foo", ...) # or # render(partial: "foo", ...) def normalize_args(string, options_hash) if options_hash { partial: string, locals: options_hash } else { partial: string } end end def parse_render(node) node = node.argument_nodes if (node.length == 1 || node.length == 2) && !node[0].hash? if node.length == 1 options = normalize_args(node[0], nil) elsif node.length == 2 options = normalize_args(node[0], node[1]) end return nil unless options parse_render_from_options(options) elsif node.length == 1 && node[0].hash? options = parse_hash_to_symbols(node[0]) return nil unless options parse_render_from_options(options) else nil end end def parse_hash(node) node.hash? && node.to_hash end def parse_hash_to_symbols(node) hash = parse_hash(node) return unless hash hash.transform_keys do |key_node| key = parse_sym(key_node) return unless key key end end ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template] RENDER_TYPE_KEYS = [:partial, :template, :layout] def parse_render_from_options(options_hash) renders = [] keys = options_hash.keys if (keys & RENDER_TYPE_KEYS).size < 1 # Must have at least one of render keys return nil end if (keys - ALL_KNOWN_KEYS).any? # de-opt in case of unknown option return nil end render_type = (keys & RENDER_TYPE_KEYS)[0] node = options_hash[render_type] if node.string? template = resolve_path_directory(node.to_string) else if node.variable_reference? dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "") elsif node.vcall? dependency = node.variable_name elsif node.call? dependency = node.call_method_name else return end object_template = true template = "#{dependency.pluralize}/#{dependency.singularize}" end return unless template if spacer_template = render_template_with_spacer?(options_hash) virtual_path = partial_to_virtual_path(:partial, spacer_template) renders << virtual_path end if options_hash.key?(:object) || options_hash.key?(:collection) || object_template return nil if options_hash.key?(:object) && options_hash.key?(:collection) return nil unless options_hash.key?(:partial) end virtual_path = partial_to_virtual_path(render_type, template) renders << virtual_path # Support for rendering multiple templates (i.e. a partial with a layout) if layout_template = render_template_with_layout?(render_type, options_hash) virtual_path = partial_to_virtual_path(:layout, layout_template) renders << virtual_path end renders end def parse_str(node) node.string? && node.to_string end def parse_sym(node) node.symbol? && node.to_symbol end private def render_template_with_layout?(render_type, options_hash) if render_type != :layout && options_hash.key?(:layout) parse_str(options_hash[:layout]) end end def render_template_with_spacer?(options_hash) if options_hash.key?(:spacer_template) parse_str(options_hash[:spacer_template]) end end def partial_to_virtual_path(render_type, partial_path) if render_type == :partial || render_type == :layout partial_path.gsub(%r{(/|^)([^/]*)\z}, '\1_\2') else partial_path end end def layout_to_virtual_path(layout_path) "layouts/#{layout_path}" end end end