1# rubocop:disable Naming/FileName 2# frozen_string_literal: true 3 4module Gitlab 5 module TemplateParser 6 # AST nodes to evaluate when rendering a template. 7 # 8 # Evaluating an AST is done by walking over the nodes and calling 9 # `evaluate`. This method takes two arguments: 10 # 11 # 1. An instance of `EvalState`, used for tracking data such as the number 12 # of nested loops. 13 # 2. An object used as the data for the current scope. This can be an Array, 14 # Hash, String, or something else. It's up to the AST node to determine 15 # what to do with it. 16 # 17 # While tree walking interpreters (such as implemented here) aren't usually 18 # the fastest type of interpreter, they are: 19 # 20 # 1. Fast enough for our use case 21 # 2. Easy to implement and maintain 22 # 23 # In addition, our AST interpreter doesn't allow for arbitrary code 24 # execution, unlike existing template engines such as Mustache 25 # (https://github.com/mustache/mustache/issues/244) or ERB. 26 # 27 # Our interpreter also takes care of limiting the number of nested loops. 28 # And unlike Liquid, our interpreter is much smaller and thus has a smaller 29 # attack surface. Liquid isn't without its share of issues, such as 30 # https://github.com/Shopify/liquid/pull/1071. 31 # 32 # We also evaluated using Handlebars using the project 33 # https://github.com/SmartBear/ruby-handlebars. Sadly, this implementation 34 # of Handlebars doesn't support control of whitespace 35 # (https://github.com/SmartBear/ruby-handlebars/issues/37), and the project 36 # didn't appear to be maintained that much. 37 # 38 # This doesn't mean these template engines aren't good, instead it means 39 # they won't work for our use case. For more information, refer to the 40 # comment https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50063#note_469293322. 41 module AST 42 # An identifier in a selector. 43 Identifier = Struct.new(:name) do 44 def evaluate(state, data) 45 return data if name == 'it' 46 47 data[name] if data.is_a?(Hash) 48 end 49 end 50 51 # An integer used in a selector. 52 Integer = Struct.new(:value) do 53 def evaluate(state, data) 54 data[value] if data.is_a?(Array) 55 end 56 end 57 58 # A selector used for loading a value. 59 Selector = Struct.new(:steps) do 60 def evaluate(state, data) 61 steps.reduce(data) do |current, step| 62 break if current.nil? 63 64 step.evaluate(state, current) 65 end 66 end 67 end 68 69 # A tag used for displaying a value in the output. 70 Variable = Struct.new(:selector) do 71 def evaluate(state, data) 72 selector.evaluate(state, data).to_s 73 end 74 end 75 76 # A collection of zero or more expressions. 77 Expressions = Struct.new(:nodes) do 78 def evaluate(state, data) 79 nodes.map { |node| node.evaluate(state, data) }.join('') 80 end 81 end 82 83 # A single text node. 84 Text = Struct.new(:text) do 85 def evaluate(*) 86 text 87 end 88 end 89 90 # An `if` expression, with an optional `else` clause. 91 If = Struct.new(:condition, :true_body, :false_body) do 92 def evaluate(state, data) 93 result = 94 if truthy?(condition.evaluate(state, data)) 95 true_body.evaluate(state, data) 96 elsif false_body 97 false_body.evaluate(state, data) 98 end 99 100 result.to_s 101 end 102 103 def truthy?(value) 104 # We treat empty collections and such as false, removing the need for 105 # some sort of `if length(x) > 0` expression. 106 value.respond_to?(:empty?) ? !value.empty? : !!value 107 end 108 end 109 110 # An `each` expression. 111 Each = Struct.new(:collection, :body) do 112 def evaluate(state, data) 113 values = collection.evaluate(state, data) 114 115 return '' unless values.respond_to?(:each) 116 117 # While unlikely to happen, it's possible users attempt to nest many 118 # loops in order to negatively impact the GitLab instance. To make 119 # this more difficult, we limit the number of nested loops a user can 120 # create. 121 state.enter_loop do 122 values.map { |value| body.evaluate(state, value) }.join('') 123 end 124 end 125 end 126 127 # A class for transforming a raw Parslet AST into a more structured/easier 128 # to work with AST. 129 # 130 # For more information about Parslet transformations, refer to the 131 # documentation at http://kschiess.github.io/parslet/transform.html. 132 class Transformer < Parslet::Transform 133 rule(ident: simple(:name)) { Identifier.new(name.to_s) } 134 rule(int: simple(:name)) { Integer.new(name.to_i) } 135 rule(text: simple(:text)) { Text.new(text.to_s) } 136 rule(exprs: subtree(:nodes)) { Expressions.new(nodes) } 137 rule(selector: sequence(:steps)) { Selector.new(steps) } 138 rule(selector: simple(:step)) { Selector.new([step]) } 139 rule(variable: simple(:selector)) { Variable.new(selector) } 140 rule(each: simple(:values), body: simple(:body)) do 141 Each.new(values, body) 142 end 143 144 rule(if: simple(:cond), true_body: simple(:true_body)) do 145 If.new(cond, true_body) 146 end 147 148 rule( 149 if: simple(:cond), 150 true_body: simple(:true_body), 151 false_body: simple(:false_body) 152 ) do 153 If.new(cond, true_body, false_body) 154 end 155 end 156 end 157 end 158end 159 160# rubocop:enable Naming/FileName 161