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{:(?:[\[email protected]_*/?!<>~|%^]+|==|\[\]=?)}
ID_CHAR =
/[a-zA-Z_]/
META_CHAR =
/\(|\)|\{|\}|\[|\]|\$\.\.\.|\$|!|\^|\.\.\./
TOKEN =
/\G(?:\s+|#{META_CHAR}|#{ID_CHAR}+\??|%\d*|\d+|#{RSYM}|.)/
NODE =
/\A#{ID_CHAR}+\Z/
PREDICATE =
/\A#{ID_CHAR}+\?\Z/
LITERAL =
/\A(?:#{RSYM}|\d+|nil)\Z/
WILDCARD =
/\A_#{ID_CHAR}*\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.


100
101
102
103
104
105
106
107
108
109
# File 'lib/rubocop/node_pattern.rb', line 100

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

  @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


98
99
100
# File 'lib/rubocop/node_pattern.rb', line 98

def match_code
  @match_code
end

Instance Method Details

#compile_ascend(tokens, cur_node, seq_head) ⇒ Object


254
255
256
257
258
# File 'lib/rubocop/node_pattern.rb', line 254

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


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

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


244
245
246
247
248
# File 'lib/rubocop/node_pattern.rb', line 244

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


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

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


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/rubocop/node_pattern.rb', line 118

def compile_expr(tokens, cur_node, seq_head)
  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 LITERAL   then compile_literal(cur_node, token, seq_head)
  when PREDICATE then compile_predicate(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_intersect(tokens, cur_node, seq_head) ⇒ Object


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

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

  init = "temp#{@temps += 1} = #{cur_node}"
  cur_node = "temp#{@temps}"

  terms = []
  until tokens.first == ']'
    terms << compile_expr(tokens, cur_node, seq_head)
  end
  tokens.shift

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

#compile_literal(cur_node, literal, seq_head) ⇒ Object


274
275
276
# File 'lib/rubocop/node_pattern.rb', line 274

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

#compile_negation(tokens, cur_node, seq_head) ⇒ Object


250
251
252
# File 'lib/rubocop/node_pattern.rb', line 250

def compile_negation(tokens, cur_node, seq_head)
  '(!' << compile_expr(tokens, cur_node, seq_head) << ')'
end

#compile_nodetype(cur_node, type) ⇒ Object


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

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

#compile_param(cur_node, number, seq_head) ⇒ Object


286
287
288
289
290
# File 'lib/rubocop/node_pattern.rb', line 286

def compile_param(cur_node, number, seq_head)
  number = number.empty? ? 1 : Integer(number)
  @params = number if number > @params
  "(#{cur_node}#{'.type' if seq_head} == param#{number})"
end

#compile_predicate(cur_node, predicate, seq_head) ⇒ Object


278
279
280
# File 'lib/rubocop/node_pattern.rb', line 278

def compile_predicate(cur_node, predicate, seq_head)
  "(#{cur_node}#{'.type' if seq_head}.#{predicate})"
end

#compile_seq(tokens, cur_node, seq_head) ⇒ Object


138
139
140
141
142
143
144
145
146
147
# File 'lib/rubocop/node_pattern.rb', line 138

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

  init = "temp#{@temps += 1} = #{cur_node}"
  cur_node = "temp#{@temps}"
  terms = compile_seq_terms(tokens, cur_node)

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

#compile_seq_tail(tokens, cur_node) ⇒ Object


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

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


149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/rubocop/node_pattern.rb', line 149

def compile_seq_terms(tokens, cur_node)
  terms = []
  index = nil
  until tokens.first == ')'
    if tokens.first == '...'
      return compile_ellipsis(tokens, cur_node, terms, index || 0)
    elsif tokens.first == '$...'
      return compile_capt_ellip(tokens, cur_node, terms, index || 0)
    elsif index.nil?
      terms << compile_expr(tokens, cur_node, true)
      index = 0
    else
      child_node = "#{cur_node}.children[#{index}]"
      terms << compile_expr(tokens, child_node, false)
      index += 1
    end
  end
  terms << "(#{cur_node}.children.size == #{index})"
  tokens.shift # drop concluding )
  terms
end

#compile_union(tokens, cur_node, seq_head) ⇒ Object


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/rubocop/node_pattern.rb', line 206

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

  init = "temp#{@temps += 1} = #{cur_node}"
  cur_node = "temp#{@temps}"

  terms = []
  captures_before = @captures
  terms << compile_expr(tokens, cur_node, seq_head)
  captures_after = @captures

  until tokens.first == '}'
    @captures = captures_before
    terms << compile_expr(tokens, cur_node, seq_head)
    if @captures != captures_after
      fail_due_to('each branch of {} must have same # of captures')
    end
  end
  tokens.shift

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

#compile_wildcard(cur_node, name, seq_head) ⇒ Object


260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/rubocop/node_pattern.rb', line 260

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] = (@temps += 1)
    "(temp#{n} = #{cur_node}#{'.type' if seq_head}; true)"
  end
end

#emit_capture_listObject


300
301
302
# File 'lib/rubocop/node_pattern.rb', line 300

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

#emit_method_codeObject


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

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

#emit_param_listObject


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

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

#emit_retvalObject


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

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

#emit_trailing_param_listObject


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

def emit_trailing_param_list
  params = emit_param_list
  params.empty? ? '' : ',' << params
end

#fail_due_to(message) ⇒ Object


330
331
332
# File 'lib/rubocop/node_pattern.rb', line 330

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

#join_terms(init, terms, operator) ⇒ Object


296
297
298
# File 'lib/rubocop/node_pattern.rb', line 296

def join_terms(init, terms, operator)
  '(' << init << ';' << terms.join(operator) << ')'
end

#next_captureObject


292
293
294
# File 'lib/rubocop/node_pattern.rb', line 292

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

#run(node_var) ⇒ Object


111
112
113
114
115
116
# File 'lib/rubocop/node_pattern.rb', line 111

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