hcl-checker 1.6.2 → 2.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.
data/assets/parse.y CHANGED
@@ -1,5 +1,6 @@
1
1
  class HCL::Checker::Parser
2
2
  token BOOL
3
+ NULL
3
4
  FLOAT
4
5
  NUMBER
5
6
  COMMA
@@ -8,6 +9,25 @@ token BOOL
8
9
  EQUAL
9
10
  STRING
10
11
  MINUS
12
+ PLUS
13
+ STAR
14
+ SLASH
15
+ PERCENT
16
+ ANDAND
17
+ OROR
18
+ EQEQ
19
+ NEQ
20
+ LT
21
+ GT
22
+ LE
23
+ GE
24
+ BANG
25
+ QUESTION
26
+ COLON
27
+ ARROW
28
+ FOR
29
+ IN
30
+ IF
11
31
  LEFTBRACE
12
32
  RIGHTBRACE
13
33
  LEFTBRACKET
@@ -18,6 +38,18 @@ token BOOL
18
38
  EPLUS
19
39
  EMINUS
20
40
 
41
+ prechigh
42
+ nonassoc UMINUS
43
+ right BANG
44
+ left STAR SLASH PERCENT
45
+ left PLUS MINUS
46
+ nonassoc LT GT LE GE
47
+ nonassoc EQEQ NEQ
48
+ left ANDAND
49
+ left OROR
50
+ right QUESTION COLON
51
+ preclow
52
+
21
53
  rule
22
54
  target: top { @result = flatten_objectlist(val[0])}
23
55
 
@@ -42,6 +74,15 @@ rule
42
74
  { result = flatten_objectlist(val[1]) }
43
75
  | LEFTBRACE RIGHTBRACE
44
76
  { return {} }
77
+ | LEFTBRACE forobjectexpr RIGHTBRACE
78
+ { result = val[1] }
79
+ ;
80
+
81
+ forobjectexpr:
82
+ FOR foriter COLON expr ARROW expr
83
+ { result = "for #{val[1]} : #{render(val[3])} => #{render(val[5])}" }
84
+ | FOR foriter COLON expr ARROW expr IF expr
85
+ { result = "for #{val[1]} : #{render(val[3])} => #{render(val[5])} if #{render(val[7])}" }
45
86
  ;
46
87
 
47
88
  objectkey:
@@ -49,27 +90,103 @@ rule
49
90
  { result = val[0] }
50
91
  | STRING
51
92
  { result = val[0] }
93
+ | FOR
94
+ { result = val[0] }
95
+ | IN
96
+ { result = val[0] }
97
+ | IF
98
+ { result = val[0] }
99
+ | NULL
100
+ { result = val[0] }
52
101
  ;
53
102
 
54
103
  objectitem:
55
- objectkey EQUAL number
56
- { result = val[0], val[2] }
57
- | objectkey EQUAL BOOL
58
- { result = val[0], val[2] }
59
- | objectkey EQUAL STRING
60
- { result = val[0], val[2] }
61
- | objectkey EQUAL object
62
- { result = val[0], val[2] }
63
- | objectkey EQUAL list
64
- { result = val[0], val[2] }
65
- | objectkey EQUAL IDENTIFIER LEFTPARENTHESES IDENTIFIER RIGHTPARENTHESES
66
- { result = val[0], "#{val[2]}(#{val[4]})" }
67
- | objectkey EQUAL IDENTIFIER
104
+ objectkey EQUAL expr
68
105
  { result = val[0], val[2] }
69
106
  | block
70
107
  { result = val[0] }
71
108
  ;
72
109
 
110
+ expr:
111
+ primary
112
+ { result = val[0] }
113
+ | expr PLUS expr
114
+ { result = "#{render(val[0])} + #{render(val[2])}" }
115
+ | expr MINUS expr
116
+ { result = "#{render(val[0])} - #{render(val[2])}" }
117
+ | expr STAR expr
118
+ { result = "#{render(val[0])} * #{render(val[2])}" }
119
+ | expr SLASH expr
120
+ { result = "#{render(val[0])} / #{render(val[2])}" }
121
+ | expr PERCENT expr
122
+ { result = "#{render(val[0])} % #{render(val[2])}" }
123
+ | expr ANDAND expr
124
+ { result = "#{render(val[0])} && #{render(val[2])}" }
125
+ | expr OROR expr
126
+ { result = "#{render(val[0])} || #{render(val[2])}" }
127
+ | expr EQEQ expr
128
+ { result = "#{render(val[0])} == #{render(val[2])}" }
129
+ | expr NEQ expr
130
+ { result = "#{render(val[0])} != #{render(val[2])}" }
131
+ | expr LT expr
132
+ { result = "#{render(val[0])} < #{render(val[2])}" }
133
+ | expr GT expr
134
+ { result = "#{render(val[0])} > #{render(val[2])}" }
135
+ | expr LE expr
136
+ { result = "#{render(val[0])} <= #{render(val[2])}" }
137
+ | expr GE expr
138
+ { result = "#{render(val[0])} >= #{render(val[2])}" }
139
+ | expr QUESTION expr COLON expr
140
+ { result = "#{render(val[0])} ? #{render(val[2])} : #{render(val[4])}" }
141
+ | BANG expr
142
+ { result = "!#{render(val[1])}" }
143
+ | MINUS expr =UMINUS
144
+ { result = negate(val[1]) }
145
+ ;
146
+
147
+ primary:
148
+ traversal
149
+ { result = val[0] }
150
+ | number
151
+ { result = val[0] }
152
+ | BOOL
153
+ { result = val[0] }
154
+ | NULL
155
+ { result = nil }
156
+ | STRING
157
+ { result = val[0] }
158
+ | list
159
+ { result = val[0] }
160
+ | object
161
+ { result = val[0] }
162
+ | LEFTPARENTHESES expr RIGHTPARENTHESES
163
+ { result = "(#{render(val[1])})" }
164
+ ;
165
+
166
+ traversal:
167
+ IDENTIFIER
168
+ { result = val[0] }
169
+ | traversal PERIOD IDENTIFIER
170
+ { result = "#{val[0]}.#{val[2]}" }
171
+ | traversal PERIOD STAR
172
+ { result = "#{val[0]}.*" }
173
+ | traversal LEFTBRACKET expr RIGHTBRACKET
174
+ { result = "#{val[0]}[#{render(val[2])}]" }
175
+ | traversal LEFTBRACKET STAR RIGHTBRACKET
176
+ { result = "#{val[0]}[*]" }
177
+ | traversal LEFTPARENTHESES RIGHTPARENTHESES
178
+ { result = "#{val[0]}()" }
179
+ | traversal LEFTPARENTHESES arglist RIGHTPARENTHESES
180
+ { result = "#{val[0]}(#{val[2].map { |a| render(a) }.join(', ')})" }
181
+ ;
182
+
183
+ arglist:
184
+ expr
185
+ { result = [val[0]] }
186
+ | arglist COMMA expr
187
+ { result = val[0] << val[2] }
188
+ ;
189
+
73
190
  block:
74
191
  block_id object
75
192
  { result = val[0], val[1] }
@@ -82,6 +199,14 @@ rule
82
199
  { result = val[0] }
83
200
  | STRING
84
201
  { result = val[0] }
202
+ | FOR
203
+ { result = val[0] }
204
+ | IN
205
+ { result = val[0] }
206
+ | IF
207
+ { result = val[0] }
208
+ | NULL
209
+ { result = val[0] }
85
210
  ;
86
211
 
87
212
  list:
@@ -89,6 +214,22 @@ rule
89
214
  { result = val[1] }
90
215
  | LEFTBRACKET RIGHTBRACKET
91
216
  { return [] }
217
+ | LEFTBRACKET forlistexpr RIGHTBRACKET
218
+ { result = val[1] }
219
+ ;
220
+
221
+ forlistexpr:
222
+ FOR foriter COLON expr
223
+ { result = "for #{val[1]} : #{render(val[3])}" }
224
+ | FOR foriter COLON expr IF expr
225
+ { result = "for #{val[1]} : #{render(val[3])} if #{render(val[5])}" }
226
+ ;
227
+
228
+ foriter:
229
+ IDENTIFIER IN expr
230
+ { result = "#{val[0]} in #{render(val[2])}" }
231
+ | IDENTIFIER COMMA IDENTIFIER IN expr
232
+ { result = "#{val[0]}, #{val[2]} in #{render(val[4])}" }
92
233
  ;
93
234
 
94
235
  listitems:
@@ -101,15 +242,7 @@ rule
101
242
  ;
102
243
 
103
244
  listitem:
104
- number
105
- { result = val[0] }
106
- | STRING
107
- { result = val[0] }
108
- | list
109
- { result = val[0] }
110
- | object
111
- { result = val[0] }
112
- | IDENTIFIER
245
+ expr
113
246
  { result = val[0] }
114
247
  ;
115
248
 
@@ -162,15 +295,21 @@ require_relative 'lexer'
162
295
 
163
296
 
164
297
  def on_error(error_token_id, error_value, value_stack)
165
- error_message = value_stack.to_s.split(',').last.gsub(']', '')
166
- header = "Parse error at #{error_message} #{error_value} (invalid token: #{error_value})"
167
- raise Racc::ParseError, header
298
+ line, column = @current_position
299
+ message = "Parse error on line #{line}, column #{column}: unexpected #{error_value.inspect}"
300
+ message += " (after #{value_stack.last.inspect})" unless value_stack.empty?
301
+
302
+ raise Racc::ParseError, message
168
303
  end
169
304
 
170
305
 
171
306
  def parse(input, duplicate_mode = :array)
172
307
  @duplicate_mode = duplicate_mode
173
- @lexer = HCL::Checker::Lexer.new.lex(input)
308
+
309
+ lexer = HCL::Checker::Lexer.new
310
+ @lexer = lexer.lex(input)
311
+ @lexer_positions = lexer.token_positions
312
+
174
313
  do_parse
175
314
 
176
315
  @result
@@ -178,9 +317,18 @@ require_relative 'lexer'
178
317
 
179
318
 
180
319
  def next_token
320
+ @current_position = @lexer_positions.shift
181
321
  @lexer.shift
182
322
  end
183
323
 
324
+ def negate(value)
325
+ value.is_a?(Numeric) ? -value : "-#{render(value)}"
326
+ end
327
+
328
+ def render(value)
329
+ value.nil? ? 'null' : value.to_s
330
+ end
331
+
184
332
  def deep_merge(hash1, hash2)
185
333
  hash2.keys.each do |key|
186
334
  value1 = hash1[key]
data/bin/setup CHANGED
@@ -3,6 +3,12 @@ set -euo pipefail
3
3
  IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
+ # Installs gems into ./vendor/bundle instead of the system gem directory.
7
+ # On a Ruby installed via a distro package (e.g. apt's ruby3.3), the system
8
+ # gem directory is usually root-owned, so a plain `bundle install` fails
9
+ # with a Bundler::PermissionError unless this repo is configured to use a
10
+ # local, writable path instead.
11
+ bundle config set --local path 'vendor/bundle'
6
12
  bundle install
7
13
 
8
14
  # Do any other automated setup that you need to do here
data/hcl-checker.gemspec CHANGED
@@ -17,21 +17,11 @@ Gem::Specification.new do |spec|
17
17
  f.match(%r{^(test|spec|features)/})
18
18
  end
19
19
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.5.0'
20
+ spec.required_ruby_version = '>= 3.3'
21
21
 
22
- spec.add_development_dependency 'bundler', '~> 2.3.13'
23
- spec.add_development_dependency 'racc', '1.5.0'
24
- spec.add_development_dependency 'rake', '~> 12.3.3'
25
- spec.add_development_dependency 'rex', '2.0.12'
26
- spec.add_development_dependency 'rexical', '>= 1.0.7'
22
+ spec.add_development_dependency 'bundler', '~> 2.4'
23
+ spec.add_development_dependency 'racc', '~> 1.8'
24
+ spec.add_development_dependency 'rake', '~> 13.0'
25
+ spec.add_development_dependency 'rexical', '>= 1.0.8'
27
26
  spec.add_development_dependency 'rspec', '~> 3.0'
28
-
29
- spec.post_install_message = '
30
- Hello, me again. This version fixes:
31
-
32
- - Update dependencies due security issues
33
- - Support interpolation only expression list
34
-
35
- Thank you :)
36
- '
37
27
  end
@@ -1,6 +1,6 @@
1
1
  #--
2
2
  # DO NOT MODIFY!!!!
3
- # This file is automatically generated by rex 1.0.7
3
+ # This file is automatically generated by rex 1.0.8
4
4
  # from lexical definition file "./assets/lexer.rex".
5
5
  #++
6
6
 
@@ -71,17 +71,29 @@ class HCL::Checker::Lexer
71
71
  when (text = @ss.scan(/true|false/))
72
72
  action { [:BOOL, to_boolean(text)]}
73
73
 
74
- when (text = @ss.scan(/\-?\d+\.\d+/))
74
+ when (text = @ss.scan(/null(?![a-zA-Z0-9_\-])/))
75
+ action { [:NULL, text]}
76
+
77
+ when (text = @ss.scan(/for(?![a-zA-Z0-9_\-])/))
78
+ action { [:FOR, text]}
79
+
80
+ when (text = @ss.scan(/in(?![a-zA-Z0-9_\-])/))
81
+ action { [:IN, text]}
82
+
83
+ when (text = @ss.scan(/if(?![a-zA-Z0-9_\-])/))
84
+ action { [:IF, text]}
85
+
86
+ when (text = @ss.scan(/\d+(\.\d+)?[eE][-+]?\d+|\d+\.\d+/))
75
87
  action { [:FLOAT, text.to_f] }
76
88
 
77
- when (text = @ss.scan(/-?\d+/))
89
+ when (text = @ss.scan(/\d+/))
78
90
  action { [:NUMBER, text.to_i] }
79
91
 
80
92
  when (text = @ss.scan(/\"/))
81
93
  action { [:STRING, consume_string(text)] }
82
94
 
83
95
  when (text = @ss.scan(/\<<\-?/))
84
- action { [:STRING, consume_heredoc] }
96
+ action { [:STRING, consume_heredoc(text.end_with?('-'))] }
85
97
 
86
98
  when (text = @ss.scan(/\{/))
87
99
  action { [:LEFTBRACE, text]}
@@ -104,15 +116,66 @@ class HCL::Checker::Lexer
104
116
  when (text = @ss.scan(/\,/))
105
117
  action { [:COMMA, text]}
106
118
 
107
- when (text = @ss.scan(/[a-zA-Z_][a-zA-Z0-9_\-\.]*/))
119
+ when (text = @ss.scan(/[a-zA-Z_][a-zA-Z0-9_\-]*/))
108
120
  action { [:IDENTIFIER, text]}
109
121
 
122
+ when (text = @ss.scan(/\=\=/))
123
+ action { [:EQEQ, text]}
124
+
125
+ when (text = @ss.scan(/\!\=/))
126
+ action { [:NEQ, text]}
127
+
128
+ when (text = @ss.scan(/\<\=/))
129
+ action { [:LE, text]}
130
+
131
+ when (text = @ss.scan(/\>\=/))
132
+ action { [:GE, text]}
133
+
134
+ when (text = @ss.scan(/\=\>/))
135
+ action { [:ARROW, text]}
136
+
137
+ when (text = @ss.scan(/\</))
138
+ action { [:LT, text]}
139
+
140
+ when (text = @ss.scan(/\>/))
141
+ action { [:GT, text]}
142
+
110
143
  when (text = @ss.scan(/\=/))
111
144
  action { [:EQUAL, text]}
112
145
 
113
146
  when (text = @ss.scan(/\-/))
114
147
  action { [:MINUS, text]}
115
148
 
149
+ when (text = @ss.scan(/\+/))
150
+ action { [:PLUS, text]}
151
+
152
+ when (text = @ss.scan(/\*/))
153
+ action { [:STAR, text]}
154
+
155
+ when (text = @ss.scan(/\//))
156
+ action { [:SLASH, text]}
157
+
158
+ when (text = @ss.scan(/\%/))
159
+ action { [:PERCENT, text]}
160
+
161
+ when (text = @ss.scan(/\&\&/))
162
+ action { [:ANDAND, text]}
163
+
164
+ when (text = @ss.scan(/\|\|/))
165
+ action { [:OROR, text]}
166
+
167
+ when (text = @ss.scan(/\?/))
168
+ action { [:QUESTION, text]}
169
+
170
+ when (text = @ss.scan(/\:/))
171
+ action { [:COLON, text]}
172
+
173
+ when (text = @ss.scan(/\./))
174
+ action { [:PERIOD, text]}
175
+
176
+ when (text = @ss.scan(/\!/))
177
+ action { [:BANG, text]}
178
+
116
179
 
117
180
  else
118
181
  text = @ss.string[@ss.pos .. -1]
@@ -125,34 +188,36 @@ class HCL::Checker::Lexer
125
188
  token
126
189
  end # def _next_token
127
190
 
191
+ attr_reader :token_positions
128
192
  def lex(input)
129
193
  scan_setup(input)
130
194
  tokens = []
195
+ @token_positions = []
131
196
  while token = next_token
132
197
  tokens << token
198
+ @token_positions << current_position
133
199
  end
134
200
  tokens
135
201
  end
202
+ def current_position
203
+ consumed = @ss.string[0...@ss.pos]
204
+ line = consumed.count("\n") + 1
205
+ column = consumed.length - (consumed.rindex("\n") || -1)
206
+ [line, column]
207
+ end
136
208
  def to_boolean(input)
137
- case input
138
- when /true/
139
- true
140
- when /false/
141
- false
142
- else
143
- raise "Invalid value for `to_boolean`, expected true/false got #{input}"
144
- end
209
+ input == 'true'
145
210
  end
146
211
  def consume_comment(input)
147
212
  nested = 1
148
213
  until nested.zero?
149
214
  case(text = @ss.scan_until(%r{/\*|\*/|\z}) )
150
215
  when %r{/\*\z}
151
- nested =+ 1
216
+ nested += 1
152
217
  when %r{\*/\z}
153
218
  nested -= 1
154
219
  else
155
- break
220
+ raise ScanError, 'unterminated block comment'
156
221
  end
157
222
  end
158
223
  end
@@ -160,13 +225,17 @@ class HCL::Checker::Lexer
160
225
  result = ''
161
226
  nested = 0
162
227
  loop do
163
- case(text = @ss.scan_until(%r{\"|\$\{|\}|\\}))
228
+ text = @ss.scan_until(%r{\"|\$\{|%\{|\}|\\})
229
+ raise ScanError, 'unterminated string' if text.nil?
230
+ case text
164
231
  when %r{\$\{\z}
165
232
  nested += 1
233
+ when %r{%\{\z}
234
+ nested += 1
166
235
  when %r{\}\z}
167
236
  nested -= 1 if nested.positive?
168
237
  when %r{\\\z}
169
- result += text.chop + @ss.getch
238
+ result += text.chop + decode_escape
170
239
  next
171
240
  end
172
241
  result += text.to_s
@@ -174,9 +243,38 @@ class HCL::Checker::Lexer
174
243
  end
175
244
  result.chop
176
245
  end
177
- def consume_heredoc
178
- token = Regexp.new @ss.scan_until(%r{\n})
179
- document = @ss.scan_until(token)
180
- document.chop
246
+ def decode_escape
247
+ char = @ss.getch
248
+ case char
249
+ when 'n' then "\n"
250
+ when 't' then "\t"
251
+ when 'r' then "\r"
252
+ when '"' then '"'
253
+ when '\\' then '\\'
254
+ when 'u'
255
+ hex = @ss.scan(/[0-9a-fA-F]{4}/)
256
+ hex ? [hex.to_i(16)].pack('U') : 'u'
257
+ when 'U'
258
+ hex = @ss.scan(/[0-9a-fA-F]{8}/)
259
+ hex ? [hex.to_i(16)].pack('U') : 'U'
260
+ else
261
+ char.to_s
262
+ end
263
+ end
264
+ def consume_heredoc(indented)
265
+ marker_line = @ss.scan_until(%r{\n})
266
+ raise ScanError, 'unterminated heredoc: missing marker' if marker_line.nil?
267
+ marker = marker_line.strip
268
+ closing = /^[ \t]*#{Regexp.escape(marker)}[ \t]*(?:\n|\z)/
269
+ raw = @ss.scan_until(closing)
270
+ raise ScanError, "unterminated heredoc: missing closing marker '#{marker}'" if raw.nil?
271
+ content = raw.sub(closing, '')
272
+ indented ? dedent(content) : content
273
+ end
274
+ def dedent(text)
275
+ lines = text.split("\n", -1)
276
+ indents = lines.reject { |line| line.strip.empty? }.map { |line| line[/\A[ \t]*/].length }
277
+ indent = indents.min || 0
278
+ lines.map { |line| line.strip.empty? ? line : line[indent..] }.join("\n")
181
279
  end
182
280
  end # class