aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view/template/error.rb
blob: d0ea03e228b951d216627f020744fe902319f23b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# frozen_string_literal: true

require "active_support/core_ext/enumerable"

module ActionView
  # = Action View Errors
  class ActionViewError < StandardError #:nodoc:
  end

  class EncodingError < StandardError #:nodoc:
  end

  class WrongEncodingError < EncodingError #:nodoc:
    def initialize(string, encoding)
      @string, @encoding = string, encoding
    end

    def message
      @string.force_encoding(Encoding::ASCII_8BIT)
      "Your template was not saved as valid #{@encoding}. Please " \
      "either specify #{@encoding} as the encoding for your template " \
      "in your text editor, or mark the template with its " \
      "encoding by inserting the following as the first line " \
      "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \
      "The source of your template was:\n\n#{@string}"
    end
  end

  class MissingTemplate < ActionViewError #:nodoc:
    attr_reader :path

    def initialize(paths, path, prefixes, partial, details, *)
      @path = path
      prefixes = Array(prefixes)
      template_type = if partial
        "partial"
      elsif /layouts/i.match?(path)
        "layout"
      else
        "template"
      end

      if partial && path.present?
        path = path.sub(%r{([^/]+)$}, "_\\1")
      end
      searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }

      out  = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
      out += paths.compact.map { |p| "  * #{p.to_s.inspect}\n" }.join
      super out
    end
  end

  class Template
    # The Template::Error exception is raised when the compilation or rendering of the template
    # fails. This exception then gathers a bunch of intimate details and uses it to report a
    # precise exception message.
    class Error < ActionViewError #:nodoc:
      SOURCE_CODE_RADIUS = 3

      # Override to prevent #cause resetting during re-raise.
      attr_reader :cause

      def initialize(template)
        super($!.message)
        set_backtrace($!.backtrace)
        @cause = $!
        @template, @sub_templates = template, nil
      end

      def file_name
        @template.identifier
      end

      def sub_template_message
        if @sub_templates
          "Trace of template inclusion: " +
          @sub_templates.collect(&:inspect).join(", ")
        else
          ""
        end
      end

      def source_extract(indentation = 0, output = :console)
        return unless num = line_number
        num = num.to_i

        source_code = @template.source.split("\n")

        start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
        end_on_line   = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min

        indent = end_on_line.to_s.size + indentation
        return unless source_code = source_code[start_on_line..end_on_line]

        formatted_code_for(source_code, start_on_line, indent, output)
      end

      def sub_template_of(template_path)
        @sub_templates ||= []
        @sub_templates << template_path
      end

      def line_number
        @line_number ||=
          if file_name
            regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
            $1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
          end
      end

      def annotated_source_code
        source_extract(4)
      end

      private

        def source_location
          if line_number
            "on line ##{line_number} of "
          else
            "in "
          end + file_name
        end

        def formatted_code_for(source_code, line_counter, indent, output)
          start_value = (output == :html) ? {} : []
          source_code.inject(start_value) do |result, line|
            line_counter += 1
            if output == :html
              result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line])
            else
              result << "%#{indent}s: %s" % [line_counter, line]
            end
          end
        end
    end
  end

  TemplateError = Template::Error

  class SyntaxErrorInTemplate < TemplateError #:nodoc
    def initialize(template, offending_code_string)
      @offending_code_string = offending_code_string
      super(template)
    end

    def message
      <<~MESSAGE
        Encountered a syntax error while rendering template: check #{@offending_code_string}
      MESSAGE
    end

    def annotated_source_code
      @offending_code_string.split("\n").map.with_index(1) { |line, index|
        indentation = " " * 4
        "#{index}:#{indentation}#{line}"
      }
    end
  end
end