Welcome to mirror list, hosted at ThFree Co, Russian Federation.

ast.rb « template_parser « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: c6a5f66c3773698e5ea94224d345f2c0044280cb (plain)
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
# rubocop:disable Naming/FileName
# frozen_string_literal: true

module Gitlab
  module TemplateParser
    # AST nodes to evaluate when rendering a template.
    #
    # Evaluating an AST is done by walking over the nodes and calling
    # `evaluate`. This method takes two arguments:
    #
    # 1. An instance of `EvalState`, used for tracking data such as the number
    #    of nested loops.
    # 2. An object used as the data for the current scope. This can be an Array,
    #    Hash, String, or something else. It's up to the AST node to determine
    #    what to do with it.
    #
    # While tree walking interpreters (such as implemented here) aren't usually
    # the fastest type of interpreter, they are:
    #
    # 1. Fast enough for our use case
    # 2. Easy to implement and maintain
    #
    # In addition, our AST interpreter doesn't allow for arbitrary code
    # execution, unlike existing template engines such as Mustache
    # (https://github.com/mustache/mustache/issues/244) or ERB.
    #
    # Our interpreter also takes care of limiting the number of nested loops.
    # And unlike Liquid, our interpreter is much smaller and thus has a smaller
    # attack surface. Liquid isn't without its share of issues, such as
    # https://github.com/Shopify/liquid/pull/1071.
    #
    # We also evaluated using Handlebars using the project
    # https://github.com/SmartBear/ruby-handlebars. Sadly, this implementation
    # of Handlebars doesn't support control of whitespace
    # (https://github.com/SmartBear/ruby-handlebars/issues/37), and the project
    # didn't appear to be maintained that much.
    #
    # This doesn't mean these template engines aren't good, instead it means
    # they won't work for our use case. For more information, refer to the
    # comment https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50063#note_469293322.
    module AST
      # An identifier in a selector.
      Identifier = Struct.new(:name) do
        def evaluate(state, data)
          return data if name == 'it'

          data[name] if data.is_a?(Hash)
        end
      end

      # An integer used in a selector.
      Integer = Struct.new(:value) do
        def evaluate(state, data)
          data[value] if data.is_a?(Array)
        end
      end

      # A selector used for loading a value.
      Selector = Struct.new(:steps) do
        def evaluate(state, data)
          steps.reduce(data) do |current, step|
            break if current.nil?

            step.evaluate(state, current)
          end
        end
      end

      # A tag used for displaying a value in the output.
      Variable = Struct.new(:selector) do
        def evaluate(state, data)
          selector.evaluate(state, data).to_s
        end
      end

      # A collection of zero or more expressions.
      Expressions = Struct.new(:nodes) do
        def evaluate(state, data)
          nodes.map { |node| node.evaluate(state, data) }.join('')
        end
      end

      # A single text node.
      Text = Struct.new(:text) do
        def evaluate(*)
          text
        end
      end

      # An `if` expression, with an optional `else` clause.
      If = Struct.new(:condition, :true_body, :false_body) do
        def evaluate(state, data)
          result =
            if truthy?(condition.evaluate(state, data))
              true_body.evaluate(state, data)
            elsif false_body
              false_body.evaluate(state, data)
            end

          result.to_s
        end

        def truthy?(value)
          # We treat empty collections and such as false, removing the need for
          # some sort of `if length(x) > 0` expression.
          value.respond_to?(:empty?) ? !value.empty? : !!value
        end
      end

      # An `each` expression.
      Each = Struct.new(:collection, :body) do
        def evaluate(state, data)
          values = collection.evaluate(state, data)

          return '' unless values.respond_to?(:each)

          # While unlikely to happen, it's possible users attempt to nest many
          # loops in order to negatively impact the GitLab instance. To make
          # this more difficult, we limit the number of nested loops a user can
          # create.
          state.enter_loop do
            values.map { |value| body.evaluate(state, value) }.join('')
          end
        end
      end

      # A class for transforming a raw Parslet AST into a more structured/easier
      # to work with AST.
      #
      # For more information about Parslet transformations, refer to the
      # documentation at http://kschiess.github.io/parslet/transform.html.
      class Transformer < Parslet::Transform
        rule(ident: simple(:name)) { Identifier.new(name.to_s) }
        rule(int: simple(:name)) { Integer.new(name.to_i) }
        rule(text: simple(:text)) { Text.new(text.to_s) }
        rule(exprs: subtree(:nodes)) { Expressions.new(nodes) }
        rule(selector: sequence(:steps)) { Selector.new(steps) }
        rule(selector: simple(:step)) { Selector.new([step]) }
        rule(variable: simple(:selector)) { Variable.new(selector) }
        rule(each: simple(:values), body: simple(:body)) do
          Each.new(values, body)
        end

        rule(if: simple(:cond), true_body: simple(:true_body)) do
          If.new(cond, true_body)
        end

        rule(
          if: simple(:cond),
          true_body: simple(:true_body),
          false_body: simple(:false_body)
        ) do
          If.new(cond, true_body, false_body)
        end
      end
    end
  end
end

# rubocop:enable Naming/FileName