summaryrefslogtreecommitdiff
path: root/lib/rdoc/parser/ripper_state_lex.rb
blob: f6cefd03058f40892ca5771d1b5da3ab50b5dc24 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
# frozen_string_literal: true
require 'ripper'

##
# Wrapper for Ripper lex states

class RDoc::Parser::RipperStateLex
  # :stopdoc:

  # TODO: Remove this constants after Ruby 2.4 EOL
  RIPPER_HAS_LEX_STATE = Ripper::Filter.method_defined?(:state)

  Token = Struct.new(:line_no, :char_no, :kind, :text, :state)

  EXPR_NONE = 0
  EXPR_BEG = 1
  EXPR_END = 2
  EXPR_ENDARG = 4
  EXPR_ENDFN = 8
  EXPR_ARG = 16
  EXPR_CMDARG = 32
  EXPR_MID = 64
  EXPR_FNAME = 128
  EXPR_DOT = 256
  EXPR_CLASS = 512
  EXPR_LABEL = 1024
  EXPR_LABELED = 2048
  EXPR_FITEM = 4096
  EXPR_VALUE = EXPR_BEG
  EXPR_BEG_ANY  =  (EXPR_BEG | EXPR_MID | EXPR_CLASS)
  EXPR_ARG_ANY  =  (EXPR_ARG | EXPR_CMDARG)
  EXPR_END_ANY  =  (EXPR_END | EXPR_ENDARG | EXPR_ENDFN)

  class InnerStateLex < Ripper::Filter
    attr_accessor :lex_state

    def initialize(code)
      @lex_state = EXPR_BEG
      @in_fname = false
      @continue = false
      reset
      super(code)
    end

    def reset
      @command_start = false
      @cmd_state = @command_start
    end

    def on_nl(tok, data)
      case @lex_state
      when EXPR_FNAME, EXPR_DOT
        @continue = true
      else
        @continue = false
        @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
      end
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_ignored_nl(tok, data)
      case @lex_state
      when EXPR_FNAME, EXPR_DOT
        @continue = true
      else
        @continue = false
        @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
      end
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_op(tok, data)
      case tok
      when '&', '|', '!', '!=', '!~'
        case @lex_state
        when EXPR_FNAME, EXPR_DOT
          @lex_state = EXPR_ARG
        else
          @lex_state = EXPR_BEG
        end
      when '<<'
        # TODO next token?
        case @lex_state
        when EXPR_FNAME, EXPR_DOT
          @lex_state = EXPR_ARG
        else
          @lex_state = EXPR_BEG
        end
      when '?'
        @lex_state = EXPR_BEG
      when '&&', '||', '+=', '-=', '*=', '**=',
           '&=', '|=', '^=', '<<=', '>>=', '||=', '&&='
        @lex_state = EXPR_BEG
      when '::'
        case @lex_state
        when EXPR_ARG, EXPR_CMDARG
          @lex_state = EXPR_DOT
        when EXPR_FNAME, EXPR_DOT
          @lex_state = EXPR_ARG
        else
          @lex_state = EXPR_BEG
        end
      else
        case @lex_state
        when EXPR_FNAME, EXPR_DOT
          @lex_state = EXPR_ARG
        else
          @lex_state = EXPR_BEG
        end
      end
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_kw(tok, data)
      case tok
      when 'class'
        @lex_state = EXPR_CLASS
        @in_fname = true
      when 'def'
        @lex_state = EXPR_FNAME
        @continue = true
        @in_fname = true
      when 'if', 'unless', 'while', 'until'
        if ((EXPR_MID | EXPR_END | EXPR_ENDARG | EXPR_ENDFN | EXPR_ARG | EXPR_CMDARG) & @lex_state) != 0 # postfix if
          @lex_state = EXPR_BEG | EXPR_LABEL
        else
          @lex_state = EXPR_BEG
        end
      when 'begin', 'case', 'when'
        @lex_state = EXPR_BEG
      when 'return', 'break'
        @lex_state = EXPR_MID
      else
        if @lex_state == EXPR_FNAME
          @lex_state = EXPR_END
        else
          @lex_state = EXPR_END
        end
      end
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_tstring_beg(tok, data)
      @lex_state = EXPR_BEG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_tstring_end(tok, data)
      @lex_state = EXPR_END | EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_CHAR(tok, data)
      @lex_state = EXPR_END
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_period(tok, data)
      @lex_state = EXPR_DOT
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_int(tok, data)
      @lex_state = EXPR_END | EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_float(tok, data)
      @lex_state = EXPR_END | EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_rational(tok, data)
      @lex_state = EXPR_END | EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_imaginary(tok, data)
      @lex_state = EXPR_END | EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_symbeg(tok, data)
      @lex_state = EXPR_FNAME
      @continue = true
      @in_fname = true
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    private def on_variables(event, tok, data)
      if @in_fname
        @lex_state = EXPR_ENDFN
        @in_fname = false
        @continue = false
      elsif @continue
        case @lex_state
        when EXPR_DOT
          @lex_state = EXPR_ARG
        else
          @lex_state = EXPR_ENDFN
          @continue = false
        end
      else
        @lex_state = EXPR_CMDARG
      end
      data << Token.new(lineno, column, event, tok, @lex_state)
    end

    def on_ident(tok, data)
      on_variables(__method__, tok, data)
    end

    def on_ivar(tok, data)
      @lex_state = EXPR_END
      on_variables(__method__, tok, data)
    end

    def on_cvar(tok, data)
      @lex_state = EXPR_END
      on_variables(__method__, tok, data)
    end

    def on_gvar(tok, data)
      @lex_state = EXPR_END
      on_variables(__method__, tok, data)
    end

    def on_backref(tok, data)
      @lex_state = EXPR_END
      on_variables(__method__, tok, data)
    end

    def on_lparen(tok, data)
      @lex_state = EXPR_LABEL | EXPR_BEG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_rparen(tok, data)
      @lex_state = EXPR_ENDFN
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_lbrace(tok, data)
      @lex_state = EXPR_LABEL | EXPR_BEG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_rbrace(tok, data)
      @lex_state = EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_lbracket(tok, data)
      @lex_state = EXPR_LABEL | EXPR_BEG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_rbracket(tok, data)
      @lex_state = EXPR_ENDARG
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_const(tok, data)
      case @lex_state
      when EXPR_FNAME
        @lex_state = EXPR_ENDFN
      when EXPR_CLASS, EXPR_CMDARG, EXPR_MID
        @lex_state = EXPR_ARG
      else
        @lex_state = EXPR_CMDARG
      end
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_sp(tok, data)
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_comma(tok, data)
      @lex_state = EXPR_BEG | EXPR_LABEL if (EXPR_ARG_ANY & @lex_state) != 0
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_comment(tok, data)
      @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_ignored_sp(tok, data)
      @lex_state = EXPR_BEG unless (EXPR_LABEL & @lex_state) != 0
      data << Token.new(lineno, column, __method__, tok, @lex_state)
    end

    def on_heredoc_beg(tok, data)
      data << Token.new(lineno, column, __method__, tok, @lex_state)
      @lex_state = EXPR_END
      data
    end

    def on_heredoc_end(tok, data)
      data << Token.new(lineno, column, __method__, tok, @lex_state)
      @lex_state = EXPR_BEG
      data
    end

    def on_default(event, tok, data)
      reset
      data << Token.new(lineno, column, event, tok, @lex_state)
    end
  end unless RIPPER_HAS_LEX_STATE

  class InnerStateLex < Ripper::Filter
    def initialize(code)
      super(code)
    end

    def on_default(event, tok, data)
      data << Token.new(lineno, column, event, tok, state)
    end
  end if RIPPER_HAS_LEX_STATE

  def get_squashed_tk
    if @buf.empty?
      tk = @tokens.shift
    else
      tk = @buf.shift
    end
    return nil if tk.nil?
    case tk[:kind]
    when :on_symbeg then
      tk = get_symbol_tk(tk)
    when :on_tstring_beg then
      tk = get_string_tk(tk)
    when :on_backtick then
      if (tk[:state] & (EXPR_FNAME | EXPR_ENDFN)) != 0
        @inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE
        tk[:kind] = :on_ident
        tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG
      else
        tk = get_string_tk(tk)
      end
    when :on_regexp_beg then
      tk = get_regexp_tk(tk)
    when :on_embdoc_beg then
      tk = get_embdoc_tk(tk)
    when :on_heredoc_beg then
      @heredoc_queue << retrieve_heredoc_info(tk)
      @inner_lex.lex_state = EXPR_END unless RIPPER_HAS_LEX_STATE
    when :on_nl, :on_ignored_nl, :on_comment, :on_heredoc_end then
      if !@heredoc_queue.empty?
        get_heredoc_tk(*@heredoc_queue.shift)
      elsif tk[:text].nil? # :on_ignored_nl sometimes gives nil
        tk[:text] = ''
      end
    when :on_words_beg then
      tk = get_words_tk(tk)
    when :on_qwords_beg then
      tk = get_words_tk(tk)
    when :on_symbols_beg then
      tk = get_words_tk(tk)
    when :on_qsymbols_beg then
      tk = get_words_tk(tk)
    when :on_op then
      if '&.' == tk[:text]
        tk[:kind] = :on_period
      else
        tk = get_op_tk(tk)
      end
    end
    tk
  end

  private def get_symbol_tk(tk)
    is_symbol = true
    symbol_tk = Token.new(tk.line_no, tk.char_no, :on_symbol)
    if ":'" == tk[:text] or ':"' == tk[:text] or tk[:text].start_with?('%s')
      tk1 = get_string_tk(tk)
      symbol_tk[:text] = tk1[:text]
      symbol_tk[:state] = tk1[:state]
    else
      case (tk1 = get_squashed_tk)[:kind]
      when :on_ident
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_tstring_content
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = get_squashed_tk[:state] # skip :on_tstring_end
      when :on_tstring_end
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_op
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_ivar
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_cvar
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_gvar
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_const
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      when :on_kw
        symbol_tk[:text] = ":#{tk1[:text]}"
        symbol_tk[:state] = tk1[:state]
      else
        is_symbol = false
        tk = tk1
      end
    end
    if is_symbol
      tk = symbol_tk
    end
    tk
  end

  private def get_string_tk(tk)
    string = tk[:text]
    state = nil
    kind = :on_tstring
    loop do
      inner_str_tk = get_squashed_tk
      if inner_str_tk.nil?
        break
      elsif :on_tstring_end == inner_str_tk[:kind]
        string = string + inner_str_tk[:text]
        state = inner_str_tk[:state]
        break
      elsif :on_label_end == inner_str_tk[:kind]
        string = string + inner_str_tk[:text]
        state = inner_str_tk[:state]
        kind = :on_symbol
        break
      else
        string = string + inner_str_tk[:text]
        if :on_embexpr_beg == inner_str_tk[:kind] then
          kind = :on_dstring if :on_tstring == kind
        end
      end
    end
    Token.new(tk.line_no, tk.char_no, kind, string, state)
  end

  private def get_regexp_tk(tk)
    string = tk[:text]
    state = nil
    loop do
      inner_str_tk = get_squashed_tk
      if inner_str_tk.nil?
        break
      elsif :on_regexp_end == inner_str_tk[:kind]
        string = string + inner_str_tk[:text]
        state = inner_str_tk[:state]
        break
      else
        string = string + inner_str_tk[:text]
      end
    end
    Token.new(tk.line_no, tk.char_no, :on_regexp, string, state)
  end

  private def get_embdoc_tk(tk)
    string = tk[:text]
    until :on_embdoc_end == (embdoc_tk = get_squashed_tk)[:kind] do
      string = string + embdoc_tk[:text]
    end
    string = string + embdoc_tk[:text]
    Token.new(tk.line_no, tk.char_no, :on_embdoc, string, embdoc_tk.state)
  end

  private def get_heredoc_tk(heredoc_name, indent)
    string = ''
    start_tk = nil
    prev_tk = nil
    until heredoc_end?(heredoc_name, indent, tk = @tokens.shift) do
      start_tk = tk unless start_tk
      if (prev_tk.nil? or "\n" == prev_tk[:text][-1]) and 0 != tk[:char_no]
        string = string + (' ' * tk[:char_no])
      end
      string = string + tk[:text]
      prev_tk = tk
    end
    start_tk = tk unless start_tk
    prev_tk = tk unless prev_tk
    @buf.unshift tk # closing heredoc
    heredoc_tk = Token.new(start_tk.line_no, start_tk.char_no, :on_heredoc, string, prev_tk.state)
    @buf.unshift heredoc_tk
  end

  private def retrieve_heredoc_info(tk)
    name = tk[:text].gsub(/\A<<[-~]?(['"`]?)(.+)\1\z/, '\2')
    indent = tk[:text] =~ /\A<<[-~]/
    [name, indent]
  end

  private def heredoc_end?(name, indent, tk)
    result = false
    if :on_heredoc_end == tk[:kind] then
      tk_name = tk[:text].chomp
      tk_name.lstrip! if indent
      if name == tk_name
        result = true
      end
    end
    result
  end

  private def get_words_tk(tk)
    string = ''
    start_token = tk[:text]
    start_quote = tk[:text].rstrip[-1]
    line_no = tk[:line_no]
    char_no = tk[:char_no]
    state = tk[:state]
    end_quote =
      case start_quote
      when ?( then ?)
      when ?[ then ?]
      when ?{ then ?}
      when ?< then ?>
      else start_quote
      end
    end_token = nil
    loop do
      tk = get_squashed_tk
      if tk.nil?
        end_token = end_quote
        break
      elsif :on_tstring_content == tk[:kind] then
        string += tk[:text]
      elsif :on_words_sep == tk[:kind] or :on_tstring_end == tk[:kind] then
        if end_quote == tk[:text].strip then
          end_token = tk[:text]
          break
        else
          string += tk[:text]
        end
      else
        string += tk[:text]
      end
    end
    text = "#{start_token}#{string}#{end_token}"
    Token.new(line_no, char_no, :on_dstring, text, state)
  end

  private def get_op_tk(tk)
    redefinable_operators = %w[! != !~ % & * ** + +@ - -@ / < << <= <=> == === =~ > >= >> [] []= ^ ` | ~]
    if redefinable_operators.include?(tk[:text]) and tk[:state] == EXPR_ARG then
      @inner_lex.lex_state = EXPR_ARG unless RIPPER_HAS_LEX_STATE
      tk[:state] = Ripper::Lexer.const_defined?(:State) ? Ripper::Lexer::State.new(EXPR_ARG) : EXPR_ARG
      tk[:kind] = :on_ident
    elsif tk[:text] =~ /^[-+]$/ then
      tk_ahead = get_squashed_tk
      case tk_ahead[:kind]
      when :on_int, :on_float, :on_rational, :on_imaginary then
        tk[:text] += tk_ahead[:text]
        tk[:kind] = tk_ahead[:kind]
        tk[:state] = tk_ahead[:state]
      when :on_heredoc_beg, :on_tstring, :on_dstring # frozen/non-frozen string literal
        tk[:text] += tk_ahead[:text]
        tk[:kind] = tk_ahead[:kind]
        tk[:state] = tk_ahead[:state]
      else
        @buf.unshift tk_ahead
      end
    end
    tk
  end

  # :startdoc:

  # New lexer for +code+.
  def initialize(code)
    @buf = []
    @heredoc_queue = []
    @inner_lex = InnerStateLex.new(code)
    @tokens = @inner_lex.parse([])
  end

  # Returns tokens parsed from +code+.
  def self.parse(code)
    lex = self.new(code)
    tokens = []
    begin
      while tk = lex.get_squashed_tk
        tokens.push tk
      end
    rescue StopIteration
    end
    tokens
  end

  # Returns +true+ if lex state will be +END+ after +token+.
  def self.end?(token)
    (token[:state] & EXPR_END)
  end
end