lkml 0.1.0 → 0.2.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.
data/lib/lkml/parser.rb CHANGED
@@ -1,26 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Parses a sequence of tokenized LookML into a parse tree.
3
+ require "logger"
4
4
 
5
- require_relative 'tokens'
6
- require_relative 'tree'
5
+ require_relative "tokens"
6
+ require_relative "tree"
7
+ require_relative "utils"
7
8
 
8
9
  module Lkml
10
+ unless const_defined?(:AttributeError)
11
+ class AttributeError < StandardError
12
+ end
13
+ end
14
+
15
+ DELIMITER = ". "
16
+
9
17
  class CommaSeparatedValues
10
- attr_accessor :trailing_comma, :leading_comma
18
+ attr_accessor :values, :trailing_comma, :leading_comma
11
19
 
12
20
  def initialize
13
- @_values = []
21
+ @values = []
14
22
  @trailing_comma = nil
15
23
  @leading_comma = nil
16
24
  end
17
25
 
18
26
  def append(value)
19
- @_values << value
27
+ @values << value
20
28
  end
21
29
 
22
- def values
23
- @_values
30
+ def frozen_tuple
31
+ @values.dup.freeze
24
32
  end
25
33
  end
26
34
 
@@ -28,15 +36,22 @@ module Lkml
28
36
  attr_accessor :tokens, :index, :progress, :depth, :log_debug
29
37
 
30
38
  def initialize(stream)
31
- stream.each { |token| raise TypeError, "Unsupported token: #{token}" unless token.is_a?(Tokens::Token) }
39
+ stream.each do |token|
40
+ raise TypeError, "Type #{token.class} for #{token} is not a valid token type." unless token.is_a?(Tokens::Token)
41
+ end
32
42
  @tokens = stream
33
43
  @index = 0
34
44
  @progress = 0
35
45
  @depth = -1
46
+ @logger = Logger.new($stderr)
47
+ @logger.level = Logger::WARN
48
+ @log_debug = false
36
49
  end
37
50
 
38
- def jump_to_index(index)
39
- @index = index
51
+ attr_reader :logger
52
+
53
+ def jump_to_index(idx)
54
+ @index = idx
40
55
  end
41
56
 
42
57
  def peek
@@ -55,18 +70,21 @@ module Lkml
55
70
 
56
71
  def consume_token_value
57
72
  token = consume
58
- raise "Token #{token} does not have a consumable value." if token.value.nil?
73
+ raise AttributeError unless token.is_a?(Tokens::ContentToken)
59
74
 
60
75
  token.value
61
76
  end
62
77
 
63
78
  def consume_trivia(only_newlines: false)
64
- valid_tokens = [Tokens::CommentToken]
65
- valid_tokens += only_newlines ? [Tokens::LinebreakToken] : [Tokens::WhitespaceToken]
66
-
67
- trivia = ''
79
+ valid = if only_newlines
80
+ [Tokens::CommentToken, Tokens::LinebreakToken]
81
+ else
82
+ [Tokens::CommentToken,
83
+ Tokens::WhitespaceToken]
84
+ end
85
+ trivia = +""
68
86
  loop do
69
- break unless check(*valid_tokens)
87
+ break unless check(*valid)
70
88
 
71
89
  trivia += consume_token_value
72
90
  end
@@ -77,262 +95,349 @@ module Lkml
77
95
  mark = @index
78
96
  consume_trivia if skip_trivia
79
97
 
98
+ if @log_debug
99
+ names = token_types.map(&:name).join(" or ")
100
+ tok = peek_safe
101
+ @logger.debug(format("%sCheck %s == %s", delimiter_repeat(1 + @depth), tok.inspect, names))
102
+ end
103
+
104
+ token_types.each do |token_type|
105
+ unless token_type <= Tokens::Token || token_type == Tokens::Token
106
+ raise TypeError, "#{token_type} is not a valid token type."
107
+ end
108
+ end
109
+
80
110
  token = begin
81
- peek
82
- rescue StandardError
111
+ @tokens.fetch(@index)
112
+ rescue IndexError
83
113
  nil
84
114
  end
85
- result = token_types.any? { |type| token.is_a?(type) }
86
-
115
+ result = token && token_types.any? { |t| token.is_a?(t) }
87
116
  jump_to_index(mark) if skip_trivia
88
117
  result
89
118
  end
90
119
 
120
+ def peek_safe
121
+ @tokens.fetch(@index)
122
+ end
123
+
91
124
  def parse
92
125
  advance if check(Tokens::StreamStartToken)
93
126
  prefix = consume_trivia
94
127
  container = parse_container
95
128
  suffix = consume_trivia
96
- DocumentNode.new(container, prefix: prefix, suffix: suffix)
129
+ Tree::DocumentNode.new(container, prefix, suffix)
97
130
  end
98
131
 
99
132
  def parse_container
100
- items = []
101
- until check(Tokens::StreamEndToken, Tokens::BlockEndToken, skip_trivia: true)
102
- block = parse_block
103
- if block
104
- items << block
105
- next
133
+ backtrack_if_none do
134
+ if @log_debug
135
+ grammar = "[expression] = (block / pair / list)*"
136
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
106
137
  end
138
+ items = []
139
+ until check(Tokens::StreamEndToken, Tokens::BlockEndToken, skip_trivia: true)
140
+ block = parse_block
141
+ if block
142
+ items << block
143
+ next
144
+ end
107
145
 
108
- pair = parse_pair
109
- if pair
110
- items << pair
111
- next
112
- end
146
+ pair = parse_pair
147
+ if pair
148
+ items << pair
149
+ next
150
+ end
151
+
152
+ list = parse_list
153
+ if list
154
+ items << list
155
+ next
156
+ end
157
+
158
+ token = @tokens[@progress]
159
+ raise SyntaxError, "Unable to find a matching expression for '#{token.id}' " \
160
+ "on line #{token.line_number}"
113
161
 
114
- list = parse_list
115
- if list
116
- items << list
117
- next
118
162
  end
119
163
 
120
- token = @tokens[@progress]
121
- raise SyntaxError, "Unable to find a matching expression for '#{token}' on line #{token.line_number}"
164
+ Tree::ContainerNode.new(items: items.freeze, top_level: @depth.zero?)
122
165
  end
123
-
124
- ContainerNode.new(items, top_level: @depth.zero?)
125
166
  end
126
167
 
127
168
  def parse_block
128
- key = parse_key
129
- return key if key.nil?
169
+ backtrack_if_none do
170
+ if @log_debug
171
+ grammar = "[block] = key literal? '{' expression '}'"
172
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
173
+ end
130
174
 
131
- name = if check(Tokens::LiteralToken)
132
- token = consume
133
- SyntaxToken.new(token.value, token.line_number)
134
- end
175
+ key = parse_key
176
+ next unless key
135
177
 
136
- prefix = consume_trivia
137
- return nil unless check(Tokens::BlockStartToken)
178
+ name = nil
179
+ if check(Tokens::LiteralToken)
180
+ token = consume
181
+ name = Tree::SyntaxToken.new(token.value, token.line_number)
182
+ end
138
183
 
139
- advance
140
- suffix = consume_trivia
141
- left_brace = LeftCurlyBrace.new(prefix: prefix, suffix: suffix)
184
+ prefix = consume_trivia
185
+ next nil unless check(Tokens::BlockStartToken)
142
186
 
143
- container = parse_container
187
+ advance
188
+ suffix = consume_trivia
189
+ left_brace = Tree::LeftCurlyBrace.new("{", nil, prefix, suffix)
144
190
 
145
- prefix = consume_trivia
146
- return unless check(Tokens::BlockEndToken)
191
+ container = parse_container
147
192
 
148
- advance
149
- suffix = consume_trivia(only_newlines: true)
150
- right_brace = RightCurlyBrace.new(prefix: prefix, suffix: suffix)
151
-
152
- BlockNode.new(
153
- key[0],
154
- colon: key[1],
155
- name: name,
156
- left_brace: left_brace,
157
- container: container,
158
- right_brace: right_brace
159
- )
193
+ prefix = consume_trivia
194
+ next nil unless check(Tokens::BlockEndToken)
195
+
196
+ advance
197
+ suffix = consume_trivia(only_newlines: true)
198
+ right_brace = Tree::RightCurlyBrace.new("}", nil, prefix, suffix)
199
+
200
+ blk = Tree::BlockNode.new(
201
+ type: key[0],
202
+ colon: key[1],
203
+ name: name,
204
+ left_brace: left_brace,
205
+ container: container,
206
+ right_brace: right_brace
207
+ )
208
+ @logger.debug("#{delimiter_repeat(@depth)}Successfully parsed block.") if @log_debug
209
+ blk
210
+ end
160
211
  end
161
212
 
162
213
  def parse_pair
163
- key = parse_key
164
- return nil if key.nil?
214
+ backtrack_if_none do
215
+ if @log_debug
216
+ grammar = "[pair] = key value"
217
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
218
+ end
219
+
220
+ key = parse_key
221
+ next unless key
165
222
 
166
- value = parse_value(parse_prefix: true, parse_suffix: true)
167
- return nil if value.nil?
223
+ value = parse_value(parse_prefix: true, parse_suffix: true)
224
+ next unless value
168
225
 
169
- PairNode.new(key[0], value, colon: key[1])
226
+ Tree::PairNode.new(type: key[0], value: value, colon: key[1])
227
+ end
170
228
  end
171
229
 
172
230
  def parse_key
173
- prefix = consume_trivia
174
- return nil unless check(Tokens::LiteralToken)
175
-
176
- token = consume
177
- key = SyntaxToken.new(token.value, token.line_number, prefix: prefix)
231
+ backtrack_if_none do
232
+ if @log_debug
233
+ grammar = "[key] = literal ':'"
234
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
235
+ end
178
236
 
179
- prefix = consume_trivia
237
+ prefix = consume_trivia
238
+ next nil unless check(Tokens::LiteralToken)
180
239
 
181
- colon = nil
182
- while check(Tokens::ValueToken)
183
240
  token = consume
184
- suffix = consume_trivia
185
- colon = Colon.new(token.line_number, prefix: prefix, suffix: suffix)
186
- end
187
- return nil unless colon
241
+ key = Tree::SyntaxToken.new(token.value, token.line_number, prefix)
242
+
243
+ prefix = consume_trivia
188
244
 
189
- [key, colon]
245
+ colon = nil
246
+ while check(Tokens::ValueToken)
247
+ token = consume
248
+ suffix = consume_trivia
249
+ colon = Tree::Colon.new(":", token.line_number, prefix, suffix)
250
+ end
251
+ next unless colon
252
+
253
+ @logger.debug(format("%sSuccessfully parsed key.", delimiter_repeat(@depth))) if @log_debug
254
+
255
+ [key, colon]
256
+ end
190
257
  end
191
258
 
192
- def parse_value(parse_prefix: false, parse_suffix: false) # rubocop:disable Metrics/CyclomaticComplexity
193
- prefix = parse_prefix ? consume_trivia : ''
259
+ def parse_value(parse_prefix: false, parse_suffix: false)
260
+ backtrack_if_none do
261
+ if @log_debug
262
+ grammar = "[value] = literal / quoted_literal / expression_block"
263
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
264
+ end
194
265
 
195
- if check(Tokens::LiteralToken)
196
- token = consume
197
- if token.value == '-' && consume_trivia
198
- return nil unless check(Tokens::LiteralToken)
266
+ prefix = parse_prefix ? consume_trivia : ""
199
267
 
268
+ if check(Tokens::LiteralToken)
200
269
  token = consume
201
- token.value = "-#{token.value}"
270
+ if token.value == "-" && !consume_trivia.empty?
271
+ next nil unless check(Tokens::LiteralToken)
202
272
 
203
- end
204
- suffix = parse_suffix ? consume_trivia : ''
205
- SyntaxToken.new(token.value, token.line_number, prefix: prefix, suffix: suffix)
206
- elsif check(Tokens::QuotedLiteralToken)
207
- token = consume
208
- suffix = parse_suffix ? consume_trivia : ''
209
- QuotedSyntaxToken.new(token.value, token.line_number, prefix: prefix, suffix: suffix)
210
- elsif check(Tokens::ExpressionBlockToken)
211
- token = consume
212
- match = token.value.match(/\A(\s*)(.*?)(\s*)\z/)
213
- expr_prefix, value, expr_suffix = if match
214
- [match[1], match[2], match[3]]
215
- else
216
- ['', token.value, '']
217
- end
218
- prefix += expr_prefix
273
+ token = consume
274
+ token.value = "-#{token.value}"
275
+
276
+ end
277
+ suffix = parse_suffix ? consume_trivia : ""
278
+ @logger.debug(format("%sSuccessfully parsed value.", delimiter_repeat(@depth))) if @log_debug
279
+ Tree::SyntaxToken.new(token.value, token.line_number, prefix, suffix)
280
+ elsif check(Tokens::QuotedLiteralToken)
281
+ token = consume
282
+ suffix = parse_suffix ? consume_trivia : ""
283
+ @logger.debug(format("%sSuccessfully parsed value.", delimiter_repeat(@depth))) if @log_debug
284
+ Tree::QuotedSyntaxToken.new(token.value, token.line_number, prefix, suffix)
285
+ elsif check(Tokens::ExpressionBlockToken)
286
+ token = consume
287
+ expr_prefix, value, expr_suffix = Utils.strip(token.value)
288
+ prefix += expr_prefix
219
289
 
220
- return nil unless check(Tokens::ExpressionBlockEndToken)
290
+ next nil unless check(Tokens::ExpressionBlockEndToken)
221
291
 
222
- advance
292
+ advance
223
293
 
224
- suffix = parse_suffix ? consume_trivia : ''
225
- ExpressionSyntaxToken.new(value, token.line_number, prefix: prefix, suffix: suffix, expr_suffix: expr_suffix)
294
+ suffix = parse_suffix ? consume_trivia : ""
295
+ @logger.debug(format("%sSuccessfully parsed value.", delimiter_repeat(@depth))) if @log_debug
296
+ Tree::ExpressionSyntaxToken.new(value, token.line_number, prefix, suffix, expr_suffix)
297
+ end
226
298
  end
227
299
  end
228
300
 
229
301
  def parse_list
230
- key = parse_key
231
- return key if key.nil?
232
-
233
- prefix = consume_trivia
234
- return nil unless check(Tokens::ListStartToken)
302
+ backtrack_if_none do
303
+ if @log_debug
304
+ grammar = "[list] = key '[' csv? ']'"
305
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
306
+ end
235
307
 
236
- advance
237
- left_bracket = LeftBracket.new(prefix: prefix)
308
+ key = parse_key
309
+ next unless key
238
310
 
239
- csv = parse_csv || CommaSeparatedValues.new
311
+ prefix = consume_trivia
312
+ next nil unless check(Tokens::ListStartToken)
240
313
 
241
- return unless check(Tokens::ListEndToken, skip_trivia: true)
314
+ advance
315
+ left_bracket = Tree::LeftBracket.new("[", nil, prefix, "")
242
316
 
243
- prefix = consume_trivia
244
- advance
245
- suffix = consume_trivia
246
- right_bracket = RightBracket.new(prefix: prefix, suffix: suffix)
247
- ListNode.new(
248
- key[0],
249
- items: csv.values,
250
- left_bracket: left_bracket,
251
- right_bracket: right_bracket,
252
- colon: key[1],
253
- leading_comma: csv.leading_comma,
254
- trailing_comma: csv.trailing_comma
255
- )
256
- end
317
+ csv = parse_csv
318
+ csv ||= CommaSeparatedValues.new
257
319
 
258
- def parse_csv # rubocop:disable Metrics/CyclomaticComplexity
259
- pair_mode = false
260
- csv = CommaSeparatedValues.new
261
- csv.leading_comma = parse_comma
320
+ next nil unless check(Tokens::ListEndToken, skip_trivia: true)
262
321
 
263
- pair = parse_pair
264
- if pair
265
- csv.append(pair)
266
- pair_mode = true
267
- elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
268
- value = parse_value(parse_prefix: true, parse_suffix: true)
269
- csv.append(value)
270
- else
271
- return nil
322
+ prefix = consume_trivia
323
+ advance
324
+ suffix = consume_trivia
325
+ right_bracket = Tree::RightBracket.new("]", nil, prefix, suffix)
326
+
327
+ Tree::ListNode.new(
328
+ type: key[0],
329
+ colon: key[1],
330
+ left_bracket: left_bracket,
331
+ items: csv.frozen_tuple,
332
+ right_bracket: right_bracket,
333
+ leading_comma: csv.leading_comma,
334
+ trailing_comma: csv.trailing_comma
335
+ )
272
336
  end
337
+ end
273
338
 
274
- until check(Tokens::ListEndToken, skip_trivia: true)
275
- return nil unless check(Tokens::CommaToken)
276
-
277
- index = @index
278
- advance
279
- if check(Tokens::ListEndToken, skip_trivia: true)
280
- jump_to_index(index)
281
- csv.trailing_comma = parse_comma
282
- break
339
+ def parse_csv
340
+ backtrack_if_none do
341
+ if @log_debug
342
+ grammar = "[csv] = \",\"? (literal / quoted_literal) (\",\" (literal / quoted_literal))* \",\"?"
343
+ @logger.debug(format("%sTry to parse %s", delimiter_repeat(@depth), grammar))
283
344
  end
284
345
 
285
- if pair_mode
286
- pair = parse_pair
287
- return nil if pair.nil?
346
+ pair_mode = false
347
+ csv = CommaSeparatedValues.new
348
+ csv.leading_comma = parse_comma
288
349
 
350
+ pair = parse_pair
351
+ if pair
289
352
  csv.append(pair)
353
+ pair_mode = true
290
354
  elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
291
355
  value = parse_value(parse_prefix: true, parse_suffix: true)
292
356
  csv.append(value)
293
- elsif check(Tokens::ListEndToken, skip_trivia: true)
294
- break
295
357
  else
296
- return nil
358
+ next nil
359
+ end
360
+
361
+ # `next` inside `until` continues the until-loop (Ruby), not the outer backtrack block — use break + flag.
362
+ csv_abort = false
363
+ until check(Tokens::ListEndToken, skip_trivia: true)
364
+ unless check(Tokens::CommaToken)
365
+ csv_abort = true
366
+ break
367
+ end
368
+
369
+ comma_index = @index
370
+ advance
371
+ if check(Tokens::ListEndToken, skip_trivia: true)
372
+ jump_to_index(comma_index)
373
+ csv.trailing_comma = parse_comma
374
+ break
375
+ end
376
+
377
+ if pair_mode
378
+ pair = parse_pair
379
+ unless pair
380
+ csv_abort = true
381
+ break
382
+ end
383
+
384
+ csv.append(pair)
385
+ elsif check(Tokens::LiteralToken, Tokens::QuotedLiteralToken, skip_trivia: true)
386
+ value = parse_value(parse_prefix: true, parse_suffix: true)
387
+ csv.append(value)
388
+ elsif check(Tokens::ListEndToken, skip_trivia: true)
389
+ break
390
+ else
391
+ csv_abort = true
392
+ break
393
+ end
297
394
  end
298
- end
299
395
 
300
- csv
396
+ next nil if csv_abort
397
+
398
+ @logger.debug(format("%sSuccessfully parsed comma-separated values.", delimiter_repeat(@depth))) if @log_debug
399
+ csv
400
+ end
301
401
  end
302
402
 
303
403
  def parse_comma
304
- prefix = consume_trivia
404
+ backtrack_if_none do
405
+ prefix = consume_trivia
406
+ next nil unless check(Tokens::CommaToken)
407
+
408
+ advance
409
+ suffix = if check(Tokens::ListEndToken, skip_trivia: true)
410
+ ""
411
+ else
412
+ consume_trivia
413
+ end
414
+ Tree::Comma.new(",", nil, prefix, suffix)
415
+ end
416
+ end
305
417
 
306
- return unless check(Tokens::CommaToken)
418
+ private
307
419
 
308
- advance
309
- suffix = check(Tokens::ListEndToken, skip_trivia: true) ? '' : consume_trivia
310
- Comma.new(prefix: prefix, suffix: suffix)
420
+ def delimiter_repeat(count)
421
+ n = Integer(count)
422
+ return +"" if n <= 0
423
+
424
+ DELIMITER * n
311
425
  end
312
426
 
313
- def self.backtrack_if_none(method_name)
314
- original_method = instance_method(method_name)
315
-
316
- define_method(method_name) do |*args, **kwargs|
317
- mark = @index
318
- @depth += 1
319
- result = original_method.bind(self).call(*args, **kwargs)
320
- @depth -= 1
321
- if result.nil?
322
- @progress = [@index, @progress].max
323
- jump_to_index(mark)
427
+ def backtrack_if_none
428
+ mark = @index
429
+ @depth += 1
430
+ result =
431
+ begin
432
+ yield
433
+ ensure
434
+ @depth -= 1
324
435
  end
325
- result
436
+ if result.nil?
437
+ @progress = [@index, @progress].max
438
+ jump_to_index(mark)
326
439
  end
440
+ result
327
441
  end
328
-
329
- backtrack_if_none :parse_container
330
- backtrack_if_none :parse_block
331
- backtrack_if_none :parse_pair
332
- backtrack_if_none :parse_key
333
- backtrack_if_none :parse_value
334
- backtrack_if_none :parse_list
335
- backtrack_if_none :parse_csv
336
- backtrack_if_none :parse_comma
337
442
  end
338
443
  end