liquid 4.0.3 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +33 -0
  3. data/README.md +6 -0
  4. data/lib/liquid.rb +17 -5
  5. data/lib/liquid/block.rb +31 -14
  6. data/lib/liquid/block_body.rb +164 -54
  7. data/lib/liquid/condition.rb +39 -18
  8. data/lib/liquid/context.rb +106 -51
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -18
  12. data/lib/liquid/expression.rb +29 -34
  13. data/lib/liquid/extensions.rb +2 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +30 -23
  19. data/lib/liquid/locales/en.yml +3 -1
  20. data/lib/liquid/parse_context.rb +16 -4
  21. data/lib/liquid/parse_tree_visitor.rb +2 -2
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +63 -44
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +24 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +33 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +25 -14
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +26 -0
  47. data/lib/liquid/tags/for.rb +68 -44
  48. data/lib/liquid/tags/if.rb +35 -23
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +34 -47
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +23 -19
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +55 -71
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +5 -3
  61. data/lib/liquid/variable.rb +46 -41
  62. data/lib/liquid/variable_lookup.rb +8 -6
  63. data/lib/liquid/version.rb +2 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +47 -1
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +608 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +73 -61
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +19 -7
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +30 -21
  79. data/test/integration/standard_filter_test.rb +339 -281
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +107 -51
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +70 -54
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +118 -124
  97. data/test/integration/trim_mode_test.rb +78 -44
  98. data/test/integration/variable_test.rb +43 -32
  99. data/test/test_helper.rb +75 -22
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +79 -77
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +11 -9
  105. data/test/{integration → unit}/parse_tree_visitor_test.rb +2 -2
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +19 -17
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +73 -47
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/lib/liquid/truffle.rb +0 -5
  123. data/test/truffle/truffle_test.rb +0 -9
  124. data/test/unit/context_unit_test.rb +0 -489
  125. data/test/unit/strainer_unit_test.rb +0 -164
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Error < ::StandardError
3
5
  attr_accessor :line_number
@@ -5,7 +7,7 @@ module Liquid
5
7
  attr_accessor :markup_context
6
8
 
7
9
  def to_s(with_prefix = true)
8
- str = ""
10
+ str = +""
9
11
  str << message_prefix if with_prefix
10
12
  str << super()
11
13
 
@@ -20,11 +22,11 @@ module Liquid
20
22
  private
21
23
 
22
24
  def message_prefix
23
- str = ""
24
- if is_a?(SyntaxError)
25
- str << "Liquid syntax error"
25
+ str = +""
26
+ str << if is_a?(SyntaxError)
27
+ "Liquid syntax error"
26
28
  else
27
- str << "Liquid error"
29
+ "Liquid error"
28
30
  end
29
31
 
30
32
  if line_number
@@ -38,19 +40,19 @@ module Liquid
38
40
  end
39
41
  end
40
42
 
41
- ArgumentError = Class.new(Error)
42
- ContextError = Class.new(Error)
43
- FileSystemError = Class.new(Error)
44
- StandardError = Class.new(Error)
45
- SyntaxError = Class.new(Error)
46
- StackLevelError = Class.new(Error)
47
- TaintedError = Class.new(Error)
48
- MemoryError = Class.new(Error)
49
- ZeroDivisionError = Class.new(Error)
50
- FloatDomainError = Class.new(Error)
51
- UndefinedVariable = Class.new(Error)
43
+ ArgumentError = Class.new(Error)
44
+ ContextError = Class.new(Error)
45
+ FileSystemError = Class.new(Error)
46
+ StandardError = Class.new(Error)
47
+ SyntaxError = Class.new(Error)
48
+ StackLevelError = Class.new(Error)
49
+ MemoryError = Class.new(Error)
50
+ ZeroDivisionError = Class.new(Error)
51
+ FloatDomainError = Class.new(Error)
52
+ UndefinedVariable = Class.new(Error)
52
53
  UndefinedDropMethod = Class.new(Error)
53
- UndefinedFilter = Class.new(Error)
54
+ UndefinedFilter = Class.new(Error)
54
55
  MethodOverrideError = Class.new(Error)
55
- InternalError = Class.new(Error)
56
+ DisabledError = Class.new(Error)
57
+ InternalError = Class.new(Error)
56
58
  end
@@ -1,45 +1,40 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Expression
3
- class MethodLiteral
4
- attr_reader :method_name, :to_s
5
-
6
- def initialize(method_name, to_s)
7
- @method_name = method_name
8
- @to_s = to_s
9
- end
10
-
11
- def to_liquid
12
- to_s
13
- end
14
- end
15
-
16
5
  LITERALS = {
17
- nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
18
- 'true'.freeze => true,
19
- 'false'.freeze => false,
20
- 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
21
- 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
6
+ nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
7
+ 'true' => true,
8
+ 'false' => false,
9
+ 'blank' => '',
10
+ 'empty' => ''
22
11
  }.freeze
23
12
 
24
- SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
25
- DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
26
- INTEGERS_REGEX = /\A(-?\d+)\z/
27
- FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
28
- RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
13
+ SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
14
+ DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
15
+ INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
16
+ FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
17
+
18
+ # Use an atomic group (?>...) to avoid pathological backtracing from
19
+ # malicious input as described in https://github.com/Shopify/liquid/issues/1357
20
+ RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/
29
21
 
30
22
  def self.parse(markup)
31
- if LITERALS.key?(markup)
32
- LITERALS[markup]
23
+ case markup
24
+ when nil
25
+ nil
26
+ when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
27
+ Regexp.last_match(1)
28
+ when INTEGERS_REGEX
29
+ Regexp.last_match(1).to_i
30
+ when RANGES_REGEX
31
+ RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
32
+ when FLOATS_REGEX
33
+ Regexp.last_match(1).to_f
33
34
  else
34
- case markup
35
- when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
36
- $1
37
- when INTEGERS_REGEX
38
- $1.to_i
39
- when RANGES_REGEX
40
- RangeLookup.parse($1, $2)
41
- when FLOATS_REGEX
42
- $1.to_f
35
+ markup = markup.strip
36
+ if LITERALS.key?(markup)
37
+ LITERALS[markup]
43
38
  else
44
39
  VariableLookup.parse(markup)
45
40
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
4
  require 'date'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
3
5
  #
@@ -44,8 +46,8 @@ module Liquid
44
46
  class LocalFileSystem
45
47
  attr_accessor :root
46
48
 
47
- def initialize(root, pattern = "_%s.liquid".freeze)
48
- @root = root
49
+ def initialize(root, pattern = "_%s.liquid")
50
+ @root = root
49
51
  @pattern = pattern
50
52
  end
51
53
 
@@ -57,9 +59,9 @@ module Liquid
57
59
  end
58
60
 
59
61
  def full_path(template_path)
60
- raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
62
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
61
63
 
62
- full_path = if template_path.include?('/'.freeze)
64
+ full_path = if template_path.include?('/')
63
65
  File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
64
66
  else
65
67
  File.join(root, @pattern % template_path)
@@ -1,13 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class ForloopDrop < Drop
3
5
  def initialize(name, length, parentloop)
4
- @name = name
5
- @length = length
6
+ @name = name
7
+ @length = length
6
8
  @parentloop = parentloop
7
- @index = 0
9
+ @index = 0
8
10
  end
9
11
 
10
- attr_reader :name, :length, :parentloop
12
+ attr_reader :length, :parentloop
13
+
14
+ def name
15
+ Usage.increment('forloop_drop_name')
16
+ @name
17
+ end
11
18
 
12
19
  def index
13
20
  @index + 1
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Liquid
@@ -26,13 +28,13 @@ module Liquid
26
28
  def interpolate(name, vars)
27
29
  name.gsub(/%\{(\w+)\}/) do
28
30
  # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
- (vars[$1.to_sym]).to_s
31
+ (vars[Regexp.last_match(1).to_sym]).to_s
30
32
  end
31
33
  end
32
34
 
33
35
  def deep_fetch_translation(name)
34
- name.split('.'.freeze).reduce(locale) do |level, cur|
35
- level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
36
+ name.split('.').reduce(locale) do |level, cur|
37
+ level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
36
38
  end
37
39
  end
38
40
  end
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # An interrupt is any command that breaks processing of a block (ex: a for loop).
3
5
  class Interrupt
4
6
  attr_reader :message
5
7
 
6
8
  def initialize(message = nil)
7
- @message = message || "interrupt".freeze
9
+ @message = message || "interrupt"
8
10
  end
9
11
  end
10
12
 
@@ -1,24 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "strscan"
2
4
  module Liquid
3
5
  class Lexer
4
6
  SPECIALS = {
5
- '|'.freeze => :pipe,
6
- '.'.freeze => :dot,
7
- ':'.freeze => :colon,
8
- ','.freeze => :comma,
9
- '['.freeze => :open_square,
10
- ']'.freeze => :close_square,
11
- '('.freeze => :open_round,
12
- ')'.freeze => :close_round,
13
- '?'.freeze => :question,
14
- '-'.freeze => :dash
7
+ '|' => :pipe,
8
+ '.' => :dot,
9
+ ':' => :colon,
10
+ ',' => :comma,
11
+ '[' => :open_square,
12
+ ']' => :close_square,
13
+ '(' => :open_round,
14
+ ')' => :close_round,
15
+ '?' => :question,
16
+ '-' => :dash,
15
17
  }.freeze
16
- IDENTIFIER = /[a-zA-Z_][\w-]*\??/
18
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
17
19
  SINGLE_STRING_LITERAL = /'[^\']*'/
18
20
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
19
- NUMBER_LITERAL = /-?\d+(\.\d+)?/
20
- DOTDOT = /\.\./
21
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
21
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
22
+ DOTDOT = /\.\./
23
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
22
24
  WHITESPACE_OR_NOTHING = /\s*/
23
25
 
24
26
  def initialize(input)
@@ -31,16 +33,21 @@ module Liquid
31
33
  until @ss.eos?
32
34
  @ss.skip(WHITESPACE_OR_NOTHING)
33
35
  break if @ss.eos?
34
- tok = case
35
- when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
36
- when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
37
- when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
38
- when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
39
- when t = @ss.scan(IDENTIFIER) then [:id, t]
40
- when t = @ss.scan(DOTDOT) then [:dotdot, t]
36
+ tok = if (t = @ss.scan(COMPARISON_OPERATOR))
37
+ [:comparison, t]
38
+ elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
39
+ [:string, t]
40
+ elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
41
+ [:string, t]
42
+ elsif (t = @ss.scan(NUMBER_LITERAL))
43
+ [:number, t]
44
+ elsif (t = @ss.scan(IDENTIFIER))
45
+ [:id, t]
46
+ elsif (t = @ss.scan(DOTDOT))
47
+ [:dotdot, t]
41
48
  else
42
- c = @ss.getch
43
- if s = SPECIALS[c]
49
+ c = @ss.getch
50
+ if (s = SPECIALS[c])
44
51
  [s, c]
45
52
  else
46
53
  raise SyntaxError, "Unexpected character #{c}"
@@ -20,7 +20,9 @@
20
20
  tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
21
21
  variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
22
22
  tag_never_closed: "'%{block_name}' tag was never closed"
23
- meta_syntax_error: "Liquid syntax error: #{e.message}"
24
23
  table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
24
+ render: "Syntax error in tag 'render' - Template name must be a quoted string"
25
25
  argument:
26
26
  include: "Argument error in tag 'include' - Illegal template name"
27
+ disabled:
28
+ tag: "usage is not allowed in this context"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class ParseContext
3
5
  attr_accessor :locale, :line_number, :trim_whitespace, :depth
@@ -5,9 +7,11 @@ module Liquid
5
7
 
6
8
  def initialize(options = {})
7
9
  @template_options = options ? options.dup : {}
8
- @locale = @template_options[:locale] ||= I18n.new
10
+
11
+ @locale = @template_options[:locale] ||= I18n.new
9
12
  @warnings = []
10
- self.depth = 0
13
+
14
+ self.depth = 0
11
15
  self.partial = false
12
16
  end
13
17
 
@@ -15,11 +19,19 @@ module Liquid
15
19
  @options[option_key]
16
20
  end
17
21
 
22
+ def new_block_body
23
+ Liquid::BlockBody.new
24
+ end
25
+
26
+ def parse_expression(markup)
27
+ Expression.parse(markup)
28
+ end
29
+
18
30
  def partial=(value)
19
31
  @partial = value
20
32
  @options = value ? partial_options : @template_options
33
+
21
34
  @error_mode = @options[:error_mode] || Template.error_mode
22
- value
23
35
  end
24
36
 
25
37
  def partial_options
@@ -28,7 +40,7 @@ module Liquid
28
40
  if dont_pass == true
29
41
  { locale: locale }
30
42
  elsif dont_pass.is_a?(Array)
31
- @template_options.reject { |k, v| dont_pass.include?(k) }
43
+ @template_options.reject { |k, _v| dont_pass.include?(k) }
32
44
  else
33
45
  @template_options
34
46
  end
@@ -11,7 +11,7 @@ module Liquid
11
11
  end
12
12
 
13
13
  def initialize(node, callbacks)
14
- @node = node
14
+ @node = node
15
15
  @callbacks = callbacks
16
16
  end
17
17
 
@@ -28,7 +28,7 @@ module Liquid
28
28
  item, new_context = @callbacks[node.class].call(node, context)
29
29
  [
30
30
  item,
31
- ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
31
+ ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
32
32
  ]
33
33
  end
34
34
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Parser
3
5
  def initialize(input)
4
- l = Lexer.new(input)
6
+ l = Lexer.new(input)
5
7
  @tokens = l.tokenize
6
- @p = 0 # pointer to current location
8
+ @p = 0 # pointer to current location
7
9
  end
8
10
 
9
11
  def jump(point)
@@ -46,11 +48,18 @@ module Liquid
46
48
 
47
49
  def expression
48
50
  token = @tokens[@p]
49
- if token[0] == :id
50
- variable_signature
51
- elsif [:string, :number].include? token[0]
51
+ case token[0]
52
+ when :id
53
+ str = consume
54
+ str << variable_lookups
55
+ when :open_square
56
+ str = consume
57
+ str << expression
58
+ str << consume(:close_square)
59
+ str << variable_lookups
60
+ when :string, :number
52
61
  consume
53
- elsif token.first == :open_round
62
+ when :open_round
54
63
  consume
55
64
  first = expression
56
65
  consume(:dotdot)
@@ -63,26 +72,29 @@ module Liquid
63
72
  end
64
73
 
65
74
  def argument
66
- str = ""
75
+ str = +""
67
76
  # might be a keyword argument (identifier: expression)
68
77
  if look(:id) && look(:colon, 1)
69
- str << consume << consume << ' '.freeze
78
+ str << consume << consume << ' '
70
79
  end
71
80
 
72
81
  str << expression
73
82
  str
74
83
  end
75
84
 
76
- def variable_signature
77
- str = consume(:id)
78
- while look(:open_square)
79
- str << consume
80
- str << expression
81
- str << consume(:close_square)
82
- end
83
- if look(:dot)
84
- str << consume
85
- str << variable_signature
85
+ def variable_lookups
86
+ str = +""
87
+ loop do
88
+ if look(:open_square)
89
+ str << consume
90
+ str << expression
91
+ str << consume(:close_square)
92
+ elsif look(:dot)
93
+ str << consume
94
+ str << consume(:id)
95
+ else
96
+ break
97
+ end
86
98
  end
87
99
  str
88
100
  end