Class: Ibex::Frontend::ActionScanner

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/frontend/action_scanner.rb,
sig/ibex/frontend/action_scanner.rbs

Overview

Extracts a balanced Ruby action while ignoring braces inside literals.

Constant Summary collapse

PAIRED_DELIMITERS =

RBS:

  • type heredoc = {
      identifier: String,
      indented: bool,
      length: Integer,
      location: Location
    }

Returns:

  • (Hash[String, String])
{ "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze
REGEX_PREFIXES =

Signature:

  • Hash[String, String]

Returns:

  • (String)
"=([{!,:;?&|+-*%^~<>"

Instance Method Summary collapse

Constructor Details

#initialize(cursor) ⇒ ActionScanner

Returns a new instance of ActionScanner.

RBS:

  • (SourceCursor cursor) -> void

Parameters:



22
23
24
25
# File 'lib/ibex/frontend/action_scanner.rb', line 22

def initialize(cursor)
  @cursor = cursor
  @pending_heredocs = [] #: Array[heredoc]
end

Instance Method Details

#bare_heredoc_identifier(prefix) ⇒ [ String?, Integer ]

RBS:

  • (String prefix) -> [String?, Integer]

Parameters:

  • prefix (String)

Returns:

  • ([ String?, Integer ])


239
240
241
242
243
# File 'lib/ibex/frontend/action_scanner.rb', line 239

def bare_heredoc_identifier(prefix)
  suffix = @cursor.rest[prefix.length..] || ""
  identifier = suffix.match(/\A[A-Za-z_]\w*/)&.[](0)
  [identifier, prefix.length + identifier.to_s.length]
end

#heredoc_openerheredoc?

RBS:

  • () -> heredoc?

Returns:

  • (heredoc, nil)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/ibex/frontend/action_scanner.rb', line 205

def heredoc_opener
  prefix = @cursor.rest.match(/\A<<([~-]?)/)
  return unless prefix

  marker = prefix[0]
  return unless marker

  quote = @cursor.rest[marker.length]
  identifier, length = if quote && ["'", '"', "`"].include?(quote)
                         quoted_heredoc_identifier(marker, quote)
                       else
                         bare_heredoc_identifier(marker)
                       end
  return unless identifier

  modifier = prefix[1] || ""
  { identifier: identifier, indented: !modifier.empty?, length: length,
    location: @cursor.location } #: heredoc
end

#quoted_heredoc_identifier(prefix, quote) ⇒ [ String, Integer ]?

RBS:

  • (String prefix, String quote) -> [String, Integer]?

Parameters:

  • prefix (String)
  • quote (String)

Returns:

  • ([ String, Integer ], nil)


226
227
228
229
230
231
232
233
234
235
236
# File 'lib/ibex/frontend/action_scanner.rb', line 226

def quoted_heredoc_identifier(prefix, quote)
  start = prefix.length + 1
  finish = @cursor.rest.index(quote, start)
  return unless finish

  identifier = @cursor.rest[start...finish]
  return unless identifier
  return if identifier.match?(/[\r\n]/)

  [identifier, finish + 1]
end

#regexp_start?Boolean

RBS:

  • () -> bool

Returns:

  • (Boolean)


156
157
158
159
# File 'lib/ibex/frontend/action_scanner.rb', line 156

def regexp_start?
  prefix = @cursor.source.chars.take(@cursor.index).join.rstrip[-1]
  prefix.nil? || REGEX_PREFIXES.include?(prefix)
end

#scanToken

RBS:

  • () -> Token

Returns:



28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/ibex/frontend/action_scanner.rb', line 28

def scan
  location = @cursor.location
  @cursor.advance
  start = @cursor.index
  scan_code(1)
  finish = @cursor.index
  @cursor.advance
  Token.new(type: :action, value: @cursor.source[start...finish], location: location)
rescue Ibex::Error
  raise
rescue StandardError => e
  raise Ibex::Error, "#{location}: unable to scan action: #{e.message}"
end

#scan_character_literalvoid

This method returns an undefined value.

RBS:

  • () -> void



189
190
191
192
193
# File 'lib/ibex/frontend/action_scanner.rb', line 189

def scan_character_literal
  @cursor.advance
  @cursor.advance if @cursor.peek == "\\"
  @cursor.advance unless @cursor.eof?
end

#scan_code(depth) ⇒ void

This method returns an undefined value.

RBS:

  • (Integer depth) -> void

Parameters:

  • depth (Integer)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/ibex/frontend/action_scanner.rb', line 45

def scan_code(depth)
  until @cursor.eof?
    if @cursor.peek == "\n" && @pending_heredocs.any?
      @cursor.advance
      scan_pending_heredocs
      next
    end

    index = @cursor.index
    scan_special_character
    next if @cursor.index != index

    depth += 1 if @cursor.peek == "{"
    depth -= 1 if @cursor.peek == "}"
    return if depth.zero?

    @cursor.advance
  end
  raise Ibex::Error, "#{@cursor.location}: unterminated action"
end

#scan_commentvoid

This method returns an undefined value.

RBS:

  • () -> void



184
185
186
# File 'lib/ibex/frontend/action_scanner.rb', line 184

def scan_comment
  @cursor.advance until @cursor.eof? || @cursor.peek == "\n"
end

#scan_delimited(opener, closer) ⇒ void

This method returns an undefined value.

RBS:

  • (String opener, String closer) -> void

Parameters:

  • opener (String)
  • closer (String)


139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/ibex/frontend/action_scanner.rb', line 139

def scan_delimited(opener, closer)
  start = @cursor.location
  depth = 1
  until @cursor.eof?
    if @cursor.peek == "\\"
      @cursor.advance(2)
      next
    end
    depth += 1 if opener != closer && @cursor.peek == opener
    depth -= 1 if @cursor.peek == closer
    @cursor.advance
    return if depth.zero?
  end
  raise Ibex::Error, "#{start}: unterminated percent literal"
end

#scan_heredocvoid

This method returns an undefined value.

RBS:

  • () -> void



196
197
198
199
200
201
202
# File 'lib/ibex/frontend/action_scanner.rb', line 196

def scan_heredoc
  opener = heredoc_opener
  return unless opener

  @cursor.advance(opener[:length])
  @pending_heredocs << opener
end

#scan_heredoc_body(heredoc) ⇒ void

This method returns an undefined value.

RBS:

  • (heredoc heredoc) -> void

Parameters:

  • heredoc (heredoc)


251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/ibex/frontend/action_scanner.rb', line 251

def scan_heredoc_body(heredoc)
  identifier = heredoc[:identifier]
  escaped_identifier = Regexp.escape(identifier)
  prefix = heredoc[:indented] ? "[ \\t]*" : ""
  terminator = /\A#{prefix}#{escaped_identifier}\r?\z/
  until @cursor.eof?
    line = @cursor.rest[/\A[^\n]*(?:\n|\z)/] || ""
    content = line.delete_suffix("\n")
    @cursor.advance(line.length)
    return if content.match?(terminator)
  end
  raise Ibex::Error, "#{heredoc[:location]}: unterminated heredoc #{identifier}"
end

#scan_interpolationvoid

This method returns an undefined value.

RBS:

  • () -> void



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/ibex/frontend/action_scanner.rb', line 100

def scan_interpolation
  depth = 1
  until @cursor.eof?
    character = @cursor.peek
    raise Ibex::Error, "#{@cursor.location}: unterminated string interpolation" unless character

    if ["'", '"', "`"].include?(character)
      scan_quoted(character)
      next
    end
    if character == "{"
      depth += 1
    elsif character == "}"
      depth -= 1
      @cursor.advance
      return if depth.zero?

      next
    end
    @cursor.advance
  end
  raise Ibex::Error, "#{@cursor.location}: unterminated string interpolation"
end

#scan_pending_heredocsvoid

This method returns an undefined value.

RBS:

  • () -> void



246
247
248
# File 'lib/ibex/frontend/action_scanner.rb', line 246

def scan_pending_heredocs
  @pending_heredocs.shift.then { |heredoc| scan_heredoc_body(heredoc) } until @pending_heredocs.empty?
end

#scan_percent_literalvoid

This method returns an undefined value.

RBS:

  • () -> void



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ibex/frontend/action_scanner.rb', line 125

def scan_percent_literal
  match = @cursor.rest.match(/\A%(?:[qQwWiIxrs])?([^\w\s])/)
  return unless match

  literal_prefix = match[0]
  opener = match[1]
  return unless literal_prefix && opener

  closer = PAIRED_DELIMITERS.fetch(opener, opener)
  @cursor.advance(literal_prefix.length)
  scan_delimited(opener, closer)
end

#scan_quoted(quote) ⇒ void

This method returns an undefined value.

RBS:

  • (String quote) -> void

Parameters:

  • quote (String)


80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ibex/frontend/action_scanner.rb', line 80

def scan_quoted(quote)
  start = @cursor.location
  @cursor.advance
  until @cursor.eof?
    if @cursor.peek == "\\"
      @cursor.advance(2)
    elsif quote != "'" && @cursor.rest.start_with?("\#{")
      @cursor.advance(2)
      scan_interpolation
    elsif @cursor.peek == quote
      @cursor.advance
      return
    else
      @cursor.advance
    end
  end
  raise Ibex::Error, "#{start}: unterminated #{quote} string in action"
end

#scan_regexpvoid

This method returns an undefined value.

RBS:

  • () -> void



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/ibex/frontend/action_scanner.rb', line 162

def scan_regexp
  start = @cursor.location
  @cursor.advance
  in_class = false
  until @cursor.eof?
    if @cursor.peek == "\\"
      @cursor.advance(2)
      next
    end
    in_class = true if @cursor.peek == "["
    in_class = false if @cursor.peek == "]"
    if @cursor.peek == "/" && !in_class
      @cursor.advance
      @cursor.advance while @cursor.peek&.match?(/[a-z]/i)
      return
    end
    @cursor.advance
  end
  raise Ibex::Error, "#{start}: unterminated regular expression"
end

#scan_special_charactervoid

This method returns an undefined value.

RBS:

  • () -> void



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/ibex/frontend/action_scanner.rb', line 67

def scan_special_character
  character = @cursor.peek
  case character
  when "'", '"', "`" then scan_quoted(character)
  when "%" then scan_percent_literal
  when "/" then scan_regexp if regexp_start?
  when "#" then scan_comment
  when "?" then scan_character_literal
  when "<" then scan_heredoc if @cursor.peek(1) == "<"
  end
end