Class: RuboCop::NodePattern::Compiler

Inherits:
Object
  • Object
show all
Defined in:
lib/rubocop/node_pattern.rb

Overview

Builds Ruby code which implements a pattern

Constant Summary collapse

RSYM =
%r{:(?:[\w+@*/?!<>=~|%^-]+|\[\]=?)}
ID_CHAR =
/[a-zA-Z_-]/
META =
/\(|\)|\{|\}|\[|\]|\$\.\.\.|\$|!|\^|\.\.\./
NUMBER =
/-?\d+(?:\.\d+)?/
TOKEN =
/\G(?:[\s,]+|#{META}|%\d*|#{NUMBER}|\#?#{ID_CHAR}+[\!\?]?\(?|#{RSYM}|.)/
NODE =
/\A#{ID_CHAR}+\Z/
PREDICATE =
/\A#{ID_CHAR}+\?\(?\Z/
WILDCARD =
/\A_#{ID_CHAR}*\Z/
FUNCALL =
/\A\##{ID_CHAR}+[\!\?]?\(?\Z/
LITERAL =
/\A(?:#{RSYM}|#{NUMBER}|nil)\Z/
PARAM =
/\A%\d*\Z/
CLOSING =
/\A(?:\)|\}|\])\Z/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(str, node_var = 'node0') ⇒ Compiler

Returns a new instance of Compiler.



118
119
120
121
122
123
124
125
126
127
128
# File 'lib/rubocop/node_pattern.rb', line 118

def initialize(str, node_var = 'node0')
  @string   = str
  @root     = node_var

  @temps    = 0  # avoid name clashes between temp variables
  @captures = 0  # number of captures seen
  @unify    = {} # named wildcard -> temp variable number
  @params   = 0  # highest % (param) number seen

  run(node_var)
end

Instance Attribute Details

#match_codeObject (readonly)

Returns the value of attribute match_code.



116
117
118
# File 'lib/rubocop/node_pattern.rb', line 116

def match_code
  @match_code
end

Instance Method Details

#compile_arg(token) ⇒ Object



385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/rubocop/node_pattern.rb', line 385

def compile_arg(token)
  case token
  when WILDCARD  then
    name   = token[1..-1]
    number = @unify[name] || fail_due_to('invalid in arglist: ' + token)
    "temp#{number}"
  when LITERAL   then token
  when PARAM     then get_param(token[1..-1])
  when CLOSING   then fail_due_to("#{token} in invalid position")
  when nil       then fail_due_to('pattern ended prematurely')
  else fail_due_to("invalid token in arglist: #{token.inspect}")
  end
end

#compile_args(tokens) ⇒ Object



378
379
380
381
382
383
# File 'lib/rubocop/node_pattern.rb', line 378

def compile_args(tokens)
  args = []
  args << compile_arg(tokens.shift) until tokens.first == ')'
  tokens.shift # drop the )
  args
end

#compile_ascend(tokens, cur_node, seq_head) ⇒ Object



323
324
325
326
# File 'lib/rubocop/node_pattern.rb', line 323

def compile_ascend(tokens, cur_node, seq_head)
  "(#{cur_node}.parent && " \
    "#{compile_expr(tokens, "#{cur_node}.parent", seq_head)})"
end

#compile_capt_ellip(tokens, cur_node, terms, index) ⇒ Object



230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/rubocop/node_pattern.rb', line 230

def compile_capt_ellip(tokens, cur_node, terms, index)
  capture = next_capture
  if (term = compile_seq_tail(tokens, "#{cur_node}.children.last"))
    terms << "(#{cur_node}.children.size > #{index})"
    terms << term
    terms << "(#{capture} = #{cur_node}.children[#{index}..-2])"
  else
    terms << "(#{cur_node}.children.size >= #{index})" if index > 0
    terms << "(#{capture} = #{cur_node}.children[#{index}..-1])"
  end
  terms
end

#compile_capture(tokens, cur_node, seq_head) ⇒ Object



314
315
316
317
# File 'lib/rubocop/node_pattern.rb', line 314

def compile_capture(tokens, cur_node, seq_head)
  "(#{next_capture} = #{cur_node}#{'.type' if seq_head}; " \
    "#{compile_expr(tokens, cur_node, seq_head)})"
end

#compile_ellipsis(tokens, cur_node, terms, index) ⇒ Object



220
221
222
223
224
225
226
227
228
# File 'lib/rubocop/node_pattern.rb', line 220

def compile_ellipsis(tokens, cur_node, terms, index)
  if (term = compile_seq_tail(tokens, "#{cur_node}.children.last"))
    terms << "(#{cur_node}.children.size > #{index})"
    terms << term
  elsif index > 0
    terms << "(#{cur_node}.children.size >= #{index})"
  end
  terms
end

#compile_expr(tokens, cur_node, seq_head) ⇒ Object

rubocop:disable Metrics/MethodLength, Metrics/AbcSize



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/rubocop/node_pattern.rb', line 138

def compile_expr(tokens, cur_node, seq_head)
  # read a single pattern-matching expression from the token stream,
  # return Ruby code which performs the corresponding matching operation
  # on 'cur_node' (which is Ruby code which evaluates to an AST node)
  #
  # the 'pattern-matching' expression may be a composite which
  # contains an arbitrary number of sub-expressions
  token = tokens.shift
  case token
  when '('       then compile_seq(tokens, cur_node, seq_head)
  when '{'       then compile_union(tokens, cur_node, seq_head)
  when '['       then compile_intersect(tokens, cur_node, seq_head)
  when '!'       then compile_negation(tokens, cur_node, seq_head)
  when '$'       then compile_capture(tokens, cur_node, seq_head)
  when '^'       then compile_ascend(tokens, cur_node, seq_head)
  when WILDCARD  then compile_wildcard(cur_node, token[1..-1], seq_head)
  when FUNCALL   then compile_funcall(tokens, cur_node, token, seq_head)
  when LITERAL   then compile_literal(cur_node, token, seq_head)
  when PREDICATE then compile_predicate(tokens, cur_node, token, seq_head)
  when NODE      then compile_nodetype(cur_node, token)
  when PARAM     then compile_param(cur_node, token[1..-1], seq_head)
  when CLOSING   then fail_due_to("#{token} in invalid position")
  when nil       then fail_due_to('pattern ended prematurely')
  else fail_due_to("invalid token #{token.inspect}")
  end
end

#compile_expr_with_capture_check(tokens, temp_node, seq_head, before, after) ⇒ Object



289
290
291
292
293
294
295
296
297
298
# File 'lib/rubocop/node_pattern.rb', line 289

def compile_expr_with_capture_check(tokens, temp_node, seq_head, before,
                                    after)
  @captures = before
  expr = compile_expr(tokens, temp_node, seq_head)
  if @captures != after
    fail_due_to('each branch of {} must have same # of captures')
  end

  expr
end

#compile_expr_with_captures(tokens, temp_node, seq_head) {|expr, captures_before, @captures| ... } ⇒ Object

Yields:

  • (expr, captures_before, @captures)


282
283
284
285
286
287
# File 'lib/rubocop/node_pattern.rb', line 282

def compile_expr_with_captures(tokens, temp_node, seq_head)
  captures_before = @captures
  expr = compile_expr(tokens, temp_node, seq_head)

  yield expr, captures_before, @captures
end

#compile_expr_with_index(tokens, cur_node, index) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
# File 'lib/rubocop/node_pattern.rb', line 208

def compile_expr_with_index(tokens, cur_node, index)
  if index.nil?
    # in 'sequence head' position; some expressions are compiled
    # differently at 'sequence head' (notably 'node type' expressions)
    # grep for seq_head to see where it makes a difference
    [compile_expr(tokens, cur_node, true), 0]
  else
    child_node = "#{cur_node}.children[#{index}]"
    [compile_expr(tokens, child_node, false), index + 1]
  end
end

#compile_funcall(tokens, cur_node, method, seq_head) ⇒ Object



357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/rubocop/node_pattern.rb', line 357

def compile_funcall(tokens, cur_node, method, seq_head)
  # call a method in the context which this pattern-matching
  # code is used in. pass target value as an argument
  method = method[1..-1] # drop the leading #
  if method.end_with?('(') # is there an arglist?
    args = compile_args(tokens)
    method = method[0..-2] # drop the trailing (
    "(#{method}(#{cur_node}#{'.type' if seq_head}),#{args.join(',')})"
  else
    "(#{method}(#{cur_node}#{'.type' if seq_head}))"
  end
end

#compile_intersect(tokens, cur_node, seq_head) ⇒ Object



300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/rubocop/node_pattern.rb', line 300

def compile_intersect(tokens, cur_node, seq_head)
  fail_due_to('empty intersection') if tokens.first == ']'

  with_temp_node(cur_node) do |init, temp_node|
    terms = []
    until tokens.first == ']'
      terms << compile_expr(tokens, temp_node, seq_head)
    end
    tokens.shift

    join_terms(init, terms, ' && ')
  end
end

#compile_literal(cur_node, literal, seq_head) ⇒ Object



343
344
345
# File 'lib/rubocop/node_pattern.rb', line 343

def compile_literal(cur_node, literal, seq_head)
  "(#{cur_node}#{'.type' if seq_head} == #{literal})"
end

#compile_negation(tokens, cur_node, seq_head) ⇒ Object



319
320
321
# File 'lib/rubocop/node_pattern.rb', line 319

def compile_negation(tokens, cur_node, seq_head)
  "(!#{compile_expr(tokens, cur_node, seq_head)})"
end

#compile_nodetype(cur_node, type) ⇒ Object



370
371
372
# File 'lib/rubocop/node_pattern.rb', line 370

def compile_nodetype(cur_node, type)
  "(#{cur_node} && #{cur_node}.#{type.tr('-', '_')}_type?)"
end

#compile_param(cur_node, number, seq_head) ⇒ Object



374
375
376
# File 'lib/rubocop/node_pattern.rb', line 374

def compile_param(cur_node, number, seq_head)
  "(#{cur_node}#{'.type' if seq_head} == #{get_param(number)})"
end

#compile_predicate(tokens, cur_node, predicate, seq_head) ⇒ Object



347
348
349
350
351
352
353
354
355
# File 'lib/rubocop/node_pattern.rb', line 347

def compile_predicate(tokens, cur_node, predicate, seq_head)
  if predicate.end_with?('(') # is there an arglist?
    args = compile_args(tokens)
    predicate = predicate[0..-2] # drop the trailing (
    "(#{cur_node}#{'.type' if seq_head}.#{predicate}(#{args.join(',')}))"
  else
    "(#{cur_node}#{'.type' if seq_head}.#{predicate})"
  end
end

#compile_seq(tokens, cur_node, seq_head) ⇒ Object

rubocop:enable Metrics/MethodLength, Metrics/AbcSize



166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/rubocop/node_pattern.rb', line 166

def compile_seq(tokens, cur_node, seq_head)
  fail_due_to('empty parentheses') if tokens.first == ')'
  fail_due_to('parentheses at sequence head') if seq_head

  # 'cur_node' is a Ruby expression which evaluates to an AST node,
  # but we don't know how expensive it is
  # to be safe, cache the node in a temp variable and then use the
  # temp variable as 'cur_node'
  with_temp_node(cur_node) do |init, temp_node|
    terms = compile_seq_terms(tokens, temp_node)

    join_terms(init, terms, ' && ')
  end
end

#compile_seq_tail(tokens, cur_node) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
# File 'lib/rubocop/node_pattern.rb', line 243

def compile_seq_tail(tokens, cur_node)
  tokens.shift
  if tokens.first == ')'
    tokens.shift
    nil
  else
    expr = compile_expr(tokens, cur_node, false)
    fail_due_to('missing )') unless tokens.shift == ')'
    expr
  end
end

#compile_seq_terms(tokens, cur_node) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/rubocop/node_pattern.rb', line 181

def compile_seq_terms(tokens, cur_node)
  ret, size =
    compile_seq_terms_with_size(tokens, cur_node) do |token, terms, index|
      case token
      when '...'.freeze
        return compile_ellipsis(tokens, cur_node, terms, index)
      when '$...'.freeze
        return compile_capt_ellip(tokens, cur_node, terms, index)
      end
    end

  ret << "(#{cur_node}.children.size == #{size})"
end

#compile_seq_terms_with_size(tokens, cur_node) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/rubocop/node_pattern.rb', line 195

def compile_seq_terms_with_size(tokens, cur_node)
  index = nil
  terms = []
  until tokens.first == ')'
    yield tokens.first, terms, index || 0
    term, index = compile_expr_with_index(tokens, cur_node, index)
    terms << term
  end

  tokens.shift # drop concluding )
  [terms, index]
end

#compile_union(tokens, cur_node, seq_head) ⇒ Object



255
256
257
258
259
260
261
262
# File 'lib/rubocop/node_pattern.rb', line 255

def compile_union(tokens, cur_node, seq_head)
  fail_due_to('empty union') if tokens.first == '}'

  with_temp_node(cur_node) do |init, temp_node|
    terms = union_terms(tokens, temp_node, seq_head)
    join_terms(init, terms, ' || ')
  end
end

#compile_wildcard(cur_node, name, seq_head) ⇒ Object



328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/rubocop/node_pattern.rb', line 328

def compile_wildcard(cur_node, name, seq_head)
  if name.empty?
    'true'
  elsif @unify.key?(name)
    # we have already seen a wildcard with this name before
    # so the value it matched the first time will already be stored
    # in a temp. check if this value matches the one stored in the temp
    "(#{cur_node}#{'.type' if seq_head} == temp#{@unify[name]})"
  else
    n = @unify[name] = next_temp_value
    # double assign to temp#{n} to avoid "assigned but unused variable"
    "(temp#{n} = temp#{n} = #{cur_node}#{'.type' if seq_head}; true)"
  end
end

#emit_capture_listObject



413
414
415
# File 'lib/rubocop/node_pattern.rb', line 413

def emit_capture_list
  (1..@captures).map { |n| "capture#{n}" }.join(',')
end

#emit_method_codeObject



436
437
438
439
440
441
# File 'lib/rubocop/node_pattern.rb', line 436

def emit_method_code
  <<-CODE
    return nil unless #{@match_code}
    block_given? ? yield(#{emit_capture_list}) : (return #{emit_retval})
  CODE
end

#emit_param_listObject



427
428
429
# File 'lib/rubocop/node_pattern.rb', line 427

def emit_param_list
  (1..@params).map { |n| "param#{n}" }.join(',')
end

#emit_retvalObject



417
418
419
420
421
422
423
424
425
# File 'lib/rubocop/node_pattern.rb', line 417

def emit_retval
  if @captures.zero?
    'true'
  elsif @captures == 1
    'capture1'
  else
    "[#{emit_capture_list}]"
  end
end

#emit_trailing_paramsObject



431
432
433
434
# File 'lib/rubocop/node_pattern.rb', line 431

def emit_trailing_params
  params = emit_param_list
  params.empty? ? '' : ",#{params}"
end

#fail_due_to(message) ⇒ Object

Raises:



443
444
445
# File 'lib/rubocop/node_pattern.rb', line 443

def fail_due_to(message)
  raise Invalid, "Couldn't compile due to #{message}. Pattern: #{@string}"
end

#get_param(number) ⇒ Object



403
404
405
406
407
# File 'lib/rubocop/node_pattern.rb', line 403

def get_param(number)
  number = number.empty? ? 1 : Integer(number)
  @params = number if number > @params
  number.zero? ? @root : "param#{number}"
end

#join_terms(init, terms, operator) ⇒ Object



409
410
411
# File 'lib/rubocop/node_pattern.rb', line 409

def join_terms(init, terms, operator)
  "(#{init};#{terms.join(operator)})"
end

#next_captureObject



399
400
401
# File 'lib/rubocop/node_pattern.rb', line 399

def next_capture
  "capture#{@captures += 1}"
end

#next_temp_valueObject



458
459
460
# File 'lib/rubocop/node_pattern.rb', line 458

def next_temp_value
  @temps += 1
end

#run(node_var) ⇒ Object



130
131
132
133
134
135
# File 'lib/rubocop/node_pattern.rb', line 130

def run(node_var)
  tokens = @string.scan(TOKEN)
  tokens.reject! { |token| token =~ /\A[\s,]+\Z/ } # drop whitespace
  @match_code = compile_expr(tokens, node_var, false)
  fail_due_to('unbalanced pattern') unless tokens.empty?
end

#union_terms(tokens, temp_node, seq_head) ⇒ Object



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/rubocop/node_pattern.rb', line 264

def union_terms(tokens, temp_node, seq_head)
  # we need to ensure that each branch of the {} contains the same
  # number of captures (since only one branch of the {} can actually
  # match, the same variables are used to hold the captures for each
  # branch)
  compile_expr_with_captures(tokens,
                             temp_node, seq_head) do |term, before, after|
    terms = [term]
    until tokens.first == '}'
      terms << compile_expr_with_capture_check(tokens, temp_node,
                                               seq_head, before, after)
    end
    tokens.shift

    terms
  end
end

#with_temp_node(cur_node) ⇒ Object



447
448
449
450
451
452
# File 'lib/rubocop/node_pattern.rb', line 447

def with_temp_node(cur_node)
  with_temp_variable do |temp_var|
    # double assign to temp#{n} to avoid "assigned but unused variable"
    yield "#{temp_var} = #{temp_var} = #{cur_node}", temp_var
  end
end

#with_temp_variable {|"temp#{next_temp_value}"| ... } ⇒ Object

Yields:



454
455
456
# File 'lib/rubocop/node_pattern.rb', line 454

def with_temp_variable
  yield "temp#{next_temp_value}"
end