ruby-rego 0.1.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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/.reek.yml +80 -0
  3. data/.vscode/extensions.json +19 -0
  4. data/.vscode/launch.json +35 -0
  5. data/.vscode/settings.json +25 -0
  6. data/.vscode/tasks.json +117 -0
  7. data/.yardopts +12 -0
  8. data/ARCHITECTURE.md +39 -0
  9. data/CHANGELOG.md +25 -0
  10. data/CODE_OF_CONDUCT.md +10 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +183 -0
  13. data/RELEASING.md +37 -0
  14. data/Rakefile +38 -0
  15. data/SECURITY.md +26 -0
  16. data/Steepfile +10 -0
  17. data/TODO.md +35 -0
  18. data/benchmark/builtin_calls.rb +29 -0
  19. data/benchmark/complex_policy.rb +19 -0
  20. data/benchmark/comprehensions.rb +19 -0
  21. data/benchmark/simple_rules.rb +20 -0
  22. data/examples/README.md +27 -0
  23. data/examples/sample_config.yaml +2 -0
  24. data/examples/simple_policy.rego +7 -0
  25. data/examples/validation_policy.rego +11 -0
  26. data/exe/rego-validate +6 -0
  27. data/lib/ruby/rego/ast/base.rb +95 -0
  28. data/lib/ruby/rego/ast/binary_op.rb +64 -0
  29. data/lib/ruby/rego/ast/call.rb +27 -0
  30. data/lib/ruby/rego/ast/composite.rb +48 -0
  31. data/lib/ruby/rego/ast/comprehension.rb +63 -0
  32. data/lib/ruby/rego/ast/every.rb +37 -0
  33. data/lib/ruby/rego/ast/import.rb +32 -0
  34. data/lib/ruby/rego/ast/literal.rb +70 -0
  35. data/lib/ruby/rego/ast/module.rb +32 -0
  36. data/lib/ruby/rego/ast/package.rb +22 -0
  37. data/lib/ruby/rego/ast/query.rb +63 -0
  38. data/lib/ruby/rego/ast/reference.rb +58 -0
  39. data/lib/ruby/rego/ast/rule.rb +114 -0
  40. data/lib/ruby/rego/ast/unary_op.rb +42 -0
  41. data/lib/ruby/rego/ast/variable.rb +22 -0
  42. data/lib/ruby/rego/ast.rb +17 -0
  43. data/lib/ruby/rego/builtins/aggregates.rb +124 -0
  44. data/lib/ruby/rego/builtins/base.rb +95 -0
  45. data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
  46. data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
  47. data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
  48. data/lib/ruby/rego/builtins/collections.rb +137 -0
  49. data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
  50. data/lib/ruby/rego/builtins/comparisons.rb +84 -0
  51. data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
  52. data/lib/ruby/rego/builtins/registry.rb +199 -0
  53. data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
  54. data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
  55. data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
  56. data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
  57. data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
  58. data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
  59. data/lib/ruby/rego/builtins/strings/search.rb +63 -0
  60. data/lib/ruby/rego/builtins/strings/split.rb +19 -0
  61. data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
  62. data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
  63. data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
  64. data/lib/ruby/rego/builtins/strings.rb +58 -0
  65. data/lib/ruby/rego/builtins/types.rb +89 -0
  66. data/lib/ruby/rego/call_name.rb +55 -0
  67. data/lib/ruby/rego/cli.rb +1122 -0
  68. data/lib/ruby/rego/compiled_module.rb +114 -0
  69. data/lib/ruby/rego/compiler.rb +1097 -0
  70. data/lib/ruby/rego/environment/overrides.rb +33 -0
  71. data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
  72. data/lib/ruby/rego/environment.rb +230 -0
  73. data/lib/ruby/rego/environment_pool.rb +71 -0
  74. data/lib/ruby/rego/error_handling.rb +58 -0
  75. data/lib/ruby/rego/error_payload.rb +34 -0
  76. data/lib/ruby/rego/errors.rb +196 -0
  77. data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
  78. data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
  79. data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
  80. data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
  81. data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
  82. data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
  83. data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
  84. data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
  85. data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
  86. data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
  87. data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
  88. data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
  89. data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
  90. data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
  91. data/lib/ruby/rego/evaluator.rb +174 -0
  92. data/lib/ruby/rego/lexer/number_reader.rb +68 -0
  93. data/lib/ruby/rego/lexer/stream.rb +137 -0
  94. data/lib/ruby/rego/lexer/string_reader.rb +90 -0
  95. data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
  96. data/lib/ruby/rego/lexer.rb +206 -0
  97. data/lib/ruby/rego/location.rb +73 -0
  98. data/lib/ruby/rego/memoization.rb +67 -0
  99. data/lib/ruby/rego/parser/collections.rb +173 -0
  100. data/lib/ruby/rego/parser/expressions.rb +216 -0
  101. data/lib/ruby/rego/parser/precedence.rb +42 -0
  102. data/lib/ruby/rego/parser/query.rb +139 -0
  103. data/lib/ruby/rego/parser/references.rb +115 -0
  104. data/lib/ruby/rego/parser/rules.rb +310 -0
  105. data/lib/ruby/rego/parser.rb +210 -0
  106. data/lib/ruby/rego/policy.rb +50 -0
  107. data/lib/ruby/rego/result.rb +91 -0
  108. data/lib/ruby/rego/token.rb +206 -0
  109. data/lib/ruby/rego/unifier.rb +451 -0
  110. data/lib/ruby/rego/value.rb +379 -0
  111. data/lib/ruby/rego/version.rb +7 -0
  112. data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
  113. data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
  114. data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
  115. data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
  116. data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
  117. data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
  118. data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
  119. data/lib/ruby/rego.rb +72 -0
  120. data/sig/objspace.rbs +4 -0
  121. data/sig/psych.rbs +7 -0
  122. data/sig/rego_validate.rbs +382 -0
  123. data/sig/ruby/rego.rbs +2150 -0
  124. metadata +172 -0
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Template string lexer helpers.
6
+ class Lexer
7
+ private
8
+
9
+ # rubocop:disable Metrics/MethodLength
10
+ def read_template_string
11
+ start = capture_position
12
+ advance
13
+ return read_raw_template_string(start) if current_char == "`"
14
+
15
+ read_standard_template_string(start)
16
+ end
17
+
18
+ def read_standard_template_string(start)
19
+ advance
20
+ buffer = +""
21
+
22
+ until eof?
23
+ char_position = capture_position
24
+ char = advance
25
+ return build_token(TokenType::TEMPLATE_STRING, buffer, start) if char == "\""
26
+
27
+ break if char == "\n"
28
+
29
+ if char == "\\" && current_char == "{"
30
+ advance
31
+ buffer << TEMPLATE_ESCAPE
32
+ next
33
+ end
34
+
35
+ buffer << (char == "\\" ? read_escape_sequence(char_position) : char)
36
+ end
37
+
38
+ raise_unterminated_string(start)
39
+ end
40
+
41
+ def read_raw_template_string(start)
42
+ advance
43
+ buffer = +""
44
+
45
+ until eof?
46
+ char = advance
47
+ return build_token(TokenType::RAW_TEMPLATE_STRING, buffer, start) if char == "`"
48
+
49
+ if char == "\\" && current_char == "{"
50
+ advance
51
+ buffer << TEMPLATE_ESCAPE
52
+ else
53
+ buffer << char
54
+ end
55
+ end
56
+
57
+ raise_unterminated_raw_string(start)
58
+ end
59
+ # rubocop:enable Metrics/MethodLength
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "location"
5
+ require_relative "token"
6
+ require_relative "lexer/number_reader"
7
+ require_relative "lexer/stream"
8
+ require_relative "lexer/string_reader"
9
+ require_relative "lexer/template_string_reader"
10
+
11
+ module Ruby
12
+ module Rego
13
+ # Converts Rego source code into a stream of tokens.
14
+ # rubocop:disable Metrics/ClassLength
15
+ class Lexer
16
+ KEYWORDS = {
17
+ "package" => TokenType::PACKAGE,
18
+ "import" => TokenType::IMPORT,
19
+ "as" => TokenType::AS,
20
+ "default" => TokenType::DEFAULT,
21
+ "if" => TokenType::IF,
22
+ "contains" => TokenType::CONTAINS,
23
+ "some" => TokenType::SOME,
24
+ "in" => TokenType::IN,
25
+ "every" => TokenType::EVERY,
26
+ "not" => TokenType::NOT,
27
+ "and" => TokenType::AND,
28
+ "or" => TokenType::OR,
29
+ "with" => TokenType::WITH,
30
+ "else" => TokenType::ELSE,
31
+ "true" => TokenType::TRUE,
32
+ "false" => TokenType::FALSE,
33
+ "null" => TokenType::NULL,
34
+ "data" => TokenType::DATA,
35
+ "input" => TokenType::INPUT
36
+ }.freeze
37
+
38
+ SINGLE_CHAR_TOKENS = {
39
+ "(" => TokenType::LPAREN,
40
+ ")" => TokenType::RPAREN,
41
+ "[" => TokenType::LBRACKET,
42
+ "]" => TokenType::RBRACKET,
43
+ "{" => TokenType::LBRACE,
44
+ "}" => TokenType::RBRACE,
45
+ "." => TokenType::DOT,
46
+ "," => TokenType::COMMA,
47
+ ";" => TokenType::SEMICOLON,
48
+ "+" => TokenType::PLUS,
49
+ "-" => TokenType::MINUS,
50
+ "*" => TokenType::STAR,
51
+ "/" => TokenType::SLASH,
52
+ "%" => TokenType::PERCENT,
53
+ "|" => TokenType::PIPE,
54
+ "&" => TokenType::AMPERSAND
55
+ }.freeze
56
+
57
+ COMPOUND_TOKENS = {
58
+ ":" => [TokenType::COLON, TokenType::ASSIGN],
59
+ "=" => [TokenType::UNIFY, TokenType::EQ],
60
+ "!" => [nil, TokenType::NEQ],
61
+ "<" => [TokenType::LT, TokenType::LTE],
62
+ ">" => [TokenType::GT, TokenType::GTE]
63
+ }.freeze
64
+
65
+ NEWLINE_CHARS = ["\n", "\r"].freeze
66
+ WHITESPACE_CHARS = [" ", "\t"].freeze
67
+ EXPONENT_CHARS = %w[e E].freeze
68
+ SIGN_CHARS = %w[+ -].freeze
69
+
70
+ IDENTIFIER_START = /[A-Za-z_]/
71
+ IDENTIFIER_PART = /[A-Za-z0-9_]/
72
+ DIGIT = /\d/
73
+ HEX_DIGIT = /[0-9A-Fa-f]/
74
+
75
+ # Create a lexer for the provided source.
76
+ #
77
+ # @param source [String] Rego source code
78
+ def initialize(source)
79
+ @source = source.to_s
80
+ @position = 0
81
+ @line = 1
82
+ @column = 1
83
+ @offset = 0
84
+ end
85
+
86
+ # Tokenize the source into a list of tokens, including EOF.
87
+ #
88
+ # @return [Array<Token>] token stream
89
+ def tokenize
90
+ # @type var tokens: Array[Token]
91
+ tokens = []
92
+
93
+ loop do
94
+ skip_whitespace
95
+ break if eof?
96
+
97
+ tokens << next_token
98
+ end
99
+
100
+ tokens << eof_token
101
+ tokens
102
+ end
103
+
104
+ private
105
+
106
+ attr_reader :source, :position, :line, :column, :offset
107
+
108
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
109
+ def next_token
110
+ char = current_char
111
+ return eof_token if char.nil?
112
+
113
+ return read_newline if newline?(char)
114
+ return read_number if digit?(char)
115
+ return read_identifier if identifier_start?(char)
116
+ return read_template_string if template_string_start?(char)
117
+ return read_string if char == "\""
118
+ return read_raw_string if char == "`"
119
+
120
+ raise_error("Invalid number literal", capture_position, length: 1) if char == "." && digit?(peek(1))
121
+
122
+ token = simple_token_for(char)
123
+ return token if token
124
+
125
+ token = read_compound_token(char)
126
+ return token if token
127
+
128
+ raise_error("Unexpected character: #{char.inspect}", capture_position, length: 1)
129
+ end
130
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
131
+
132
+ def eof_token
133
+ start = capture_position
134
+ build_token(TokenType::EOF, nil, start)
135
+ end
136
+
137
+ def simple_token_for(char)
138
+ type = SINGLE_CHAR_TOKENS[char]
139
+ return nil unless type
140
+
141
+ start = capture_position
142
+ advance
143
+ build_token(type, nil, start)
144
+ end
145
+
146
+ def read_compound_token(char)
147
+ config = COMPOUND_TOKENS[char]
148
+ return nil unless config
149
+
150
+ single_type, double_type = config
151
+ start = capture_position
152
+ advance
153
+ return build_token(double_type, nil, start) if match?("=")
154
+ return build_token(single_type, nil, start) if single_type
155
+
156
+ raise_error("Unexpected character: #{char.inspect}", start, length: 1)
157
+ end
158
+
159
+ def skip_whitespace
160
+ loop do
161
+ char = current_char
162
+ break if char.nil?
163
+
164
+ if char == "#"
165
+ skip_comment
166
+ next
167
+ end
168
+
169
+ break unless whitespace?(char)
170
+
171
+ advance
172
+ end
173
+ end
174
+
175
+ def skip_comment
176
+ advance
177
+ advance until eof? || newline?(current_char)
178
+ end
179
+
180
+ def template_string_start?(char)
181
+ char == "$" && ["\"", "`"].include?(peek(1).to_s)
182
+ end
183
+
184
+ def read_newline
185
+ start = capture_position
186
+ advance
187
+ build_token(TokenType::NEWLINE, "\n", start)
188
+ end
189
+
190
+ def read_identifier
191
+ start = capture_position
192
+ buffer = +""
193
+
194
+ buffer << advance while identifier_part?(current_char)
195
+
196
+ return build_token(TokenType::UNDERSCORE, nil, start) if buffer == "_"
197
+
198
+ keyword = KEYWORDS[buffer]
199
+ return build_token(keyword, nil, start) if keyword
200
+
201
+ build_token(TokenType::IDENT, buffer, start)
202
+ end
203
+ end
204
+ # rubocop:enable Metrics/ClassLength
205
+ end
206
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Represents a source location in a Rego policy.
6
+ #
7
+ # @example
8
+ # location = Ruby::Rego::Location.new(line: 3, column: 12, offset: 42, length: 5)
9
+ # location.to_s # => "line 3, column 12, offset 42, length 5"
10
+ #
11
+ class Location
12
+ # Coerce a hash or location into a Location instance.
13
+ #
14
+ # @param position [Location, Hash] position data
15
+ # @return [Location]
16
+ def self.from(position)
17
+ return position if position.is_a?(Location)
18
+
19
+ new(
20
+ line: position.fetch(:line),
21
+ column: position.fetch(:column),
22
+ offset: position[:offset],
23
+ length: position[:length]
24
+ )
25
+ end
26
+
27
+ # Create a new source location.
28
+ #
29
+ # @param line [Integer] 1-based line number
30
+ # @param column [Integer] 1-based column number
31
+ # @param offset [Integer, nil] 0-based character offset
32
+ # @param length [Integer, nil] length of the token or span
33
+ def initialize(line:, column:, offset: nil, length: nil)
34
+ @line = line
35
+ @column = column
36
+ @offset = offset
37
+ @length = length
38
+ end
39
+
40
+ # Line number.
41
+ #
42
+ # @return [Integer]
43
+ attr_reader :line
44
+
45
+ # Column number.
46
+ #
47
+ # @return [Integer]
48
+ attr_reader :column
49
+
50
+ # Character offset.
51
+ #
52
+ # @return [Integer, nil]
53
+ attr_reader :offset
54
+
55
+ # Length of the token or span.
56
+ #
57
+ # @return [Integer, nil]
58
+ attr_reader :length
59
+
60
+ # Convert the location to a readable string.
61
+ #
62
+ # @return [String]
63
+ def to_s
64
+ {
65
+ line: line,
66
+ column: column,
67
+ offset: offset,
68
+ length: length
69
+ }.compact.map { |key, value| "#{key} #{value}" }.join(", ")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Shared memoization store for evaluation caches.
6
+ module Memoization
7
+ # Holds per-scope caches used during evaluation.
8
+ class Context
9
+ def initialize
10
+ @rule_values = {} # @type var @rule_values: Hash[String, Value]
11
+ @reference_values = {} # @type var @reference_values: Hash[AST::Reference, Value]
12
+ @reference_keys = {} # @type var @reference_keys: Hash[AST::Reference, Array[Object] | Object]
13
+ @function_values = {} # @type var @function_values: Hash[Array[Object], Value]
14
+ end
15
+
16
+ # @return [Hash]
17
+ attr_reader :rule_values
18
+
19
+ # @return [Hash]
20
+ attr_reader :reference_values
21
+
22
+ # @return [Hash]
23
+ attr_reader :reference_keys
24
+
25
+ # @return [Hash]
26
+ attr_reader :function_values
27
+ end
28
+
29
+ # Stack-based memoization store for nested scopes.
30
+ class Store
31
+ def initialize
32
+ @contexts = [Context.new]
33
+ end
34
+
35
+ # Reset memoized data for a new evaluation.
36
+ #
37
+ # @return [void]
38
+ def reset!
39
+ @contexts = [Context.new]
40
+ nil
41
+ end
42
+
43
+ # Reset memoized data without mutation semantics.
44
+ #
45
+ # @return [void]
46
+ def reset
47
+ reset!
48
+ end
49
+
50
+ # Run with a fresh memoization context.
51
+ #
52
+ # @return [Object]
53
+ def with_context
54
+ @contexts << Context.new
55
+ yield
56
+ ensure
57
+ @contexts.pop
58
+ end
59
+
60
+ # @return [Context]
61
+ def context
62
+ @contexts.last
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Parsing helpers for collection literals and comprehensions.
6
+ # :reek:TooManyMethods
7
+ # :reek:RepeatedConditional
8
+ # :reek:DataClump
9
+ # rubocop:disable Metrics/ClassLength
10
+ class Parser
11
+ private
12
+
13
+ # :reek:TooManyStatements
14
+ # :reek:DuplicateMethodCall
15
+ def parse_array
16
+ start = consume(TokenType::LBRACKET)
17
+ location = start.location
18
+ consume_newlines
19
+ return AST::ArrayLiteral.new(elements: [], location: location) if match?(TokenType::RBRACKET)
20
+
21
+ term = parse_expression(Precedence::OR)
22
+ return parse_array_comprehension(start, term) if pipe_token?
23
+
24
+ elements = parse_expression_list_until_with_first(TokenType::RBRACKET, term)
25
+ consume_newlines
26
+ consume(TokenType::RBRACKET, "Expected ']' after array literal.")
27
+ AST::ArrayLiteral.new(elements: elements, location: location)
28
+ end
29
+
30
+ def parse_object(start_token, first_key, first_value)
31
+ pairs = build_object_pairs(first_key, first_value)
32
+ consume_newlines
33
+ consume(TokenType::RBRACE, "Expected '}' after object literal.")
34
+ AST::ObjectLiteral.new(pairs: pairs, location: start_token.location)
35
+ end
36
+
37
+ def build_object_pairs(first_key, first_value)
38
+ pairs = [] # @type var pairs: Array[[AST::expression, AST::expression]]
39
+ pairs << [first_key, first_value]
40
+ append_object_pairs(pairs)
41
+ pairs
42
+ end
43
+
44
+ def parse_set(start_token, first_element = nil)
45
+ return empty_set_literal(start_token) if empty_set?(first_element)
46
+
47
+ elements = parse_expression_list_until_with_first(TokenType::RBRACE, first_element)
48
+ consume_newlines
49
+ consume(TokenType::RBRACE, "Expected '}' after set literal.")
50
+ AST::SetLiteral.new(elements: elements, location: start_token.location)
51
+ end
52
+
53
+ # :reek:TooManyStatements
54
+ def parse_braced_literal
55
+ start = consume(TokenType::LBRACE)
56
+ consume_newlines
57
+ return empty_object_literal(start) if rbrace_token?
58
+
59
+ first = parse_expression(Precedence::OR)
60
+ return parse_set_comprehension(start, first) if pipe_token?
61
+ return parse_object_literal_or_comprehension(start, first) if match?(TokenType::COLON)
62
+
63
+ parse_set(start, first)
64
+ end
65
+
66
+ def parse_object_literal_or_comprehension(start, key)
67
+ advance
68
+ consume_newlines
69
+ value = parse_expression(Precedence::OR)
70
+ return parse_object_comprehension(start, key, value) if pipe_token?
71
+
72
+ parse_object(start, key, value)
73
+ end
74
+
75
+ def parse_expression_list_until(end_token)
76
+ elements = [] # @type var elements: Array[AST::expression]
77
+ consume_newlines
78
+ elements << parse_expression
79
+ append_expression_list(elements, end_token)
80
+ elements
81
+ end
82
+
83
+ def parse_expression_list_until_with_first(end_token, first_element)
84
+ elements = [] # @type var elements: Array[AST::expression]
85
+ elements << first_element
86
+ consume_newlines
87
+ append_expression_list(elements, end_token)
88
+ elements
89
+ end
90
+
91
+ def parse_object_pair(key)
92
+ consume(TokenType::COLON, "Expected ':' after object key.")
93
+ consume_newlines
94
+ value = parse_expression
95
+ [key, value]
96
+ end
97
+
98
+ def append_expression_list(elements, end_token)
99
+ while match?(TokenType::COMMA)
100
+ advance
101
+ consume_newlines
102
+ break if match?(end_token)
103
+
104
+ elements << parse_expression
105
+ end
106
+ end
107
+
108
+ def append_object_pairs(pairs)
109
+ while match?(TokenType::COMMA)
110
+ advance
111
+ consume_newlines
112
+ break if rbrace_token?
113
+
114
+ pairs << parse_object_pair(parse_expression)
115
+ end
116
+ end
117
+
118
+ def parse_array_comprehension(start_token, term)
119
+ parse_comprehension(
120
+ start_token,
121
+ term,
122
+ TokenType::RBRACKET,
123
+ ["Expected '|' after array term.", "Expected ']' after array comprehension."],
124
+ AST::ArrayComprehension
125
+ )
126
+ end
127
+
128
+ def parse_object_comprehension(start_token, key, value)
129
+ parse_comprehension(
130
+ start_token,
131
+ [key, value],
132
+ TokenType::RBRACE,
133
+ ["Expected '|' after object term.", "Expected '}' after object comprehension."],
134
+ AST::ObjectComprehension
135
+ )
136
+ end
137
+
138
+ def parse_set_comprehension(start_token, term)
139
+ parse_comprehension(
140
+ start_token,
141
+ term,
142
+ TokenType::RBRACE,
143
+ ["Expected '|' after set term.", "Expected '}' after set comprehension."],
144
+ AST::SetComprehension
145
+ )
146
+ end
147
+
148
+ # :reek:LongParameterList
149
+ def parse_comprehension(start_token, term, end_token, messages, node_class)
150
+ pipe_message, end_message = messages
151
+ consume(TokenType::PIPE, pipe_message)
152
+ body = parse_query(end_token, newline_delimiter: true)
153
+ consume(end_token, end_message)
154
+ node_class.new(term: term, body: body, location: start_token.location)
155
+ end
156
+
157
+ def empty_set?(first_element)
158
+ rbrace_token? && !first_element
159
+ end
160
+
161
+ def empty_object_literal(start_token)
162
+ advance
163
+ AST::ObjectLiteral.new(pairs: [], location: start_token.location)
164
+ end
165
+
166
+ def empty_set_literal(start_token)
167
+ advance
168
+ AST::SetLiteral.new(elements: [], location: start_token.location)
169
+ end
170
+ end
171
+ # rubocop:enable Metrics/ClassLength
172
+ end
173
+ end