Class: SqlBeautifier::Scanner

Inherits:
Object
  • Object
show all
Defined in:
lib/sql_beautifier/scanner.rb

Constant Summary collapse

IDENTIFIER_CHARACTER =
%r{[[:alnum:]_$]}
SENTINEL_MAX_LOOKBACK =
20

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, position: 0) ⇒ Scanner

Returns a new instance of Scanner.



12
13
14
15
16
17
18
# File 'lib/sql_beautifier/scanner.rb', line 12

def initialize(source, position: 0)
  @source = source
  @position = position
  @in_single_quote = false
  @in_double_quote = false
  @parenthesis_depth = 0
end

Instance Attribute Details

#parenthesis_depth ⇒ Object (readonly)

Returns the value of attribute parenthesis_depth.



10
11
12
# File 'lib/sql_beautifier/scanner.rb', line 10

def parenthesis_depth
  @parenthesis_depth
end

#position ⇒ Object (readonly)

Returns the value of attribute position.



9
10
11
# File 'lib/sql_beautifier/scanner.rb', line 9

def position
  @position
end

#source ⇒ Object (readonly)

Returns the value of attribute source.



8
9
10
# File 'lib/sql_beautifier/scanner.rb', line 8

def source
  @source
end

Instance Method Details

#advance!(count = 1) ⇒ Object



48
49
50
# File 'lib/sql_beautifier/scanner.rb', line 48

def advance!(count = 1)
  @position += count
end

#advance_through_double_quote! ⇒ Object



227
228
229
230
231
232
233
234
235
236
# File 'lib/sql_beautifier/scanner.rb', line 227

def advance_through_double_quote!
  if @source[@position] == Constants::DOUBLE_QUOTE && @source[@position + 1] == Constants::DOUBLE_QUOTE
    @position += 2
  elsif @source[@position] == Constants::DOUBLE_QUOTE
    @in_double_quote = false
    @position += 1
  else
    @position += 1
  end
end

#advance_through_single_quote! ⇒ Object



216
217
218
219
220
221
222
223
224
225
# File 'lib/sql_beautifier/scanner.rb', line 216

def advance_through_single_quote!
  if @source[@position] == Constants::SINGLE_QUOTE && @source[@position + 1] == Constants::SINGLE_QUOTE
    @position += 2
  elsif @source[@position] == Constants::SINGLE_QUOTE
    @in_single_quote = false
    @position += 1
  else
    @position += 1
  end
end

#character_after(at_position = @position, offset = 1) ⇒ Object



290
291
292
293
294
295
# File 'lib/sql_beautifier/scanner.rb', line 290

def character_after(at_position = @position, offset = 1)
  target = at_position + offset
  return nil if target >= @source.length

  @source[target]
end

#character_before(at_position = @position) ⇒ Object



284
285
286
287
288
# File 'lib/sql_beautifier/scanner.rb', line 284

def character_before(at_position = @position)
  return nil if at_position.zero?

  @source[at_position - 1]
end

#consume_dollar_quoted_string!(delimiter) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/sql_beautifier/scanner.rb', line 114

def consume_dollar_quoted_string!(delimiter)
  start = @position
  @position += delimiter.length

  while @position < @source.length
    if @source[@position, delimiter.length] == delimiter
      @position += delimiter.length
      return @source[start...@position]
    end

    @position += 1
  end

  @source[start...@position]
end

#consume_double_quoted_identifier! ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/sql_beautifier/scanner.rb', line 88

def consume_double_quoted_identifier!
  start = @position
  @position += 1

  while @position < @source.length
    if @source[@position] == Constants::DOUBLE_QUOTE && @source[@position + 1] == Constants::DOUBLE_QUOTE
      @position += 2
    elsif @source[@position] == Constants::DOUBLE_QUOTE
      @position += 1
      return @source[start...@position]
    else
      @position += 1
    end
  end

  @source[start...@position]
end

#consume_sentinel! ⇒ Object



106
107
108
109
110
111
112
# File 'lib/sql_beautifier/scanner.rb', line 106

def consume_sentinel!
  start = @position
  end_position = sentinel_end_position
  @position = end_position

  @source[start...end_position]
end

#consume_single_quoted_string! ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/sql_beautifier/scanner.rb', line 70

def consume_single_quoted_string!
  start = @position
  @position += 1

  while @position < @source.length
    if @source[@position] == Constants::SINGLE_QUOTE && @source[@position + 1] == Constants::SINGLE_QUOTE
      @position += 2
    elsif @source[@position] == Constants::SINGLE_QUOTE
      @position += 1
      return @source[start...@position]
    else
      @position += 1
    end
  end

  @source[start...@position]
end

#current_char ⇒ Object



24
25
26
# File 'lib/sql_beautifier/scanner.rb', line 24

def current_char
  @source[@position]
end

#decrement_depth! ⇒ Object



66
67
68
# File 'lib/sql_beautifier/scanner.rb', line 66

def decrement_depth!
  @parenthesis_depth = [@parenthesis_depth - 1, 0].max
end

#detect_conjunction_at(at_position = @position) ⇒ Object



381
382
383
384
385
# File 'lib/sql_beautifier/scanner.rb', line 381

def detect_conjunction_at(at_position = @position)
  Constants::CONJUNCTIONS.detect do |conjunction|
    keyword_at?(conjunction, at_position)
  end
end

#dollar_quote_delimiter_at(at_position = @position) ⇒ Object



257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/sql_beautifier/scanner.rb', line 257

def dollar_quote_delimiter_at(at_position = @position)
  return "$$" if @source[at_position, 2] == "$$"
  return unless @source[at_position] == "$"

  closing_dollar_position = @source.index("$", at_position + 1)
  return unless closing_dollar_position

  delimiter = @source[at_position..closing_dollar_position]
  tag = delimiter[1..-2]
  return unless tag.match?(%r{\A[[:alpha:]_][[:alnum:]_]*\z})

  delimiter
end

#enter_double_quote! ⇒ Object



57
58
59
60
# File 'lib/sql_beautifier/scanner.rb', line 57

def enter_double_quote!
  @in_double_quote = true
  @position += 1
end

#enter_single_quote! ⇒ Object



52
53
54
55
# File 'lib/sql_beautifier/scanner.rb', line 52

def enter_single_quote!
  @in_single_quote = true
  @position += 1
end

#escaped_double_quote?(at_position = @position) ⇒ Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/sql_beautifier/scanner.rb', line 301

def escaped_double_quote?(at_position = @position)
  @source[at_position] == Constants::DOUBLE_QUOTE && @source[at_position + 1] == Constants::DOUBLE_QUOTE
end

#escaped_single_quote?(at_position = @position) ⇒ Boolean

Returns:

  • (Boolean)


297
298
299
# File 'lib/sql_beautifier/scanner.rb', line 297

def escaped_single_quote?(at_position = @position)
  @source[at_position] == Constants::SINGLE_QUOTE && @source[at_position + 1] == Constants::SINGLE_QUOTE
end

#find_matching_parenthesis(opening_position) ⇒ Object



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
# File 'lib/sql_beautifier/scanner.rb', line 325

def find_matching_parenthesis(opening_position)
  local_depth = 0
  scan_position = opening_position

  while scan_position < @source.length
    character = @source[scan_position]

    if @source[scan_position] == Constants::SINGLE_QUOTE
      scan_position += 1
      while scan_position < @source.length
        if @source[scan_position] == Constants::SINGLE_QUOTE && @source[scan_position + 1] == Constants::SINGLE_QUOTE
          scan_position += 2
        elsif @source[scan_position] == Constants::SINGLE_QUOTE
          scan_position += 1
          break
        else
          scan_position += 1
        end
      end
      next
    end

    if character == Constants::DOUBLE_QUOTE
      scan_position += 1
      while scan_position < @source.length
        if @source[scan_position] == Constants::DOUBLE_QUOTE && @source[scan_position + 1] == Constants::DOUBLE_QUOTE
          scan_position += 2
        elsif @source[scan_position] == Constants::DOUBLE_QUOTE
          scan_position += 1
          break
        else
          scan_position += 1
        end
      end
      next
    end

    if sentinel_at?(scan_position)
      scan_position = sentinel_end_position(scan_position)
      next
    end

    case character
    when Constants::OPEN_PARENTHESIS
      local_depth += 1
    when Constants::CLOSE_PARENTHESIS
      local_depth -= 1
      return scan_position if local_depth.zero?
    end

    scan_position += 1
  end

  nil
end

#finished? ⇒ Boolean

Returns:

  • (Boolean)


20
21
22
# File 'lib/sql_beautifier/scanner.rb', line 20

def finished?
  @position >= @source.length
end

#in_double_quote? ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
# File 'lib/sql_beautifier/scanner.rb', line 40

def in_double_quote?
  @in_double_quote
end

#in_quoted_context? ⇒ Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/sql_beautifier/scanner.rb', line 44

def in_quoted_context?
  @in_single_quote || @in_double_quote
end

#in_single_quote? ⇒ Boolean

Returns:

  • (Boolean)


36
37
38
# File 'lib/sql_beautifier/scanner.rb', line 36

def in_single_quote?
  @in_single_quote
end

#increment_depth! ⇒ Object



62
63
64
# File 'lib/sql_beautifier/scanner.rb', line 62

def increment_depth!
  @parenthesis_depth += 1
end

#inside_sentinel?(at_position) ⇒ Boolean

Returns:

  • (Boolean)


249
250
251
252
253
254
255
# File 'lib/sql_beautifier/scanner.rb', line 249

def inside_sentinel?(at_position)
  search_start = [at_position - SENTINEL_MAX_LOOKBACK, 0].max
  prefix_position = @source.rindex(CommentParser::SENTINEL_PREFIX, at_position)
  return false unless prefix_position && prefix_position >= search_start

  at_position < sentinel_end_position(prefix_position)
end

#keyword_at?(keyword, at_position = @position) ⇒ Boolean

Returns:

  • (Boolean)


271
272
273
274
275
276
277
278
# File 'lib/sql_beautifier/scanner.rb', line 271

def keyword_at?(keyword, at_position = @position)
  return false unless @source[at_position, keyword.length]&.downcase == keyword

  previous_character = character_before(at_position)
  next_character = character_after(at_position, keyword.length)

  word_boundary?(previous_character) && word_boundary?(next_character)
end

#peek(offset = 1) ⇒ Object



28
29
30
# File 'lib/sql_beautifier/scanner.rb', line 28

def peek(offset = 1)
  @source[@position + offset]
end

#read_identifier! ⇒ Object



314
315
316
317
318
319
320
321
322
323
# File 'lib/sql_beautifier/scanner.rb', line 314

def read_identifier!
  skip_whitespace!
  return nil if finished?

  if current_char == Constants::DOUBLE_QUOTE
    read_quoted_identifier!
  else
    read_unquoted_identifier!
  end
end

#scan_quoted_or_sentinel! ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/sql_beautifier/scanner.rb', line 177

def scan_quoted_or_sentinel!
  return consume_sentinel! if sentinel_at?

  delimiter = dollar_quote_delimiter_at
  return consume_dollar_quoted_string!(delimiter) if delimiter

  case current_char
  when Constants::SINGLE_QUOTE
    consume_single_quoted_string!
  when Constants::DOUBLE_QUOTE
    consume_double_quoted_identifier!
  end
end

#sentinel_at?(at_position = @position) ⇒ Boolean

Returns:

  • (Boolean)


238
239
240
# File 'lib/sql_beautifier/scanner.rb', line 238

def sentinel_at?(at_position = @position)
  @source[at_position, CommentParser::SENTINEL_PREFIX.length] == CommentParser::SENTINEL_PREFIX
end

#sentinel_end_position(from_position = @position) ⇒ Object



242
243
244
245
246
247
# File 'lib/sql_beautifier/scanner.rb', line 242

def sentinel_end_position(from_position = @position)
  closing = @source.index(CommentParser::SENTINEL_SUFFIX, from_position + CommentParser::SENTINEL_PREFIX.length)
  return from_position + 1 unless closing

  closing + CommentParser::SENTINEL_SUFFIX.length
end

#skip_dollar_quoted_string!(delimiter) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/sql_beautifier/scanner.rb', line 164

def skip_dollar_quoted_string!(delimiter)
  @position += delimiter.length

  while @position < @source.length
    if @source[@position, delimiter.length] == delimiter
      @position += delimiter.length
      return
    end

    @position += 1
  end
end

#skip_double_quoted_identifier! ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/sql_beautifier/scanner.rb', line 145

def skip_double_quoted_identifier!
  @position += 1

  while @position < @source.length
    if @source[@position] == Constants::DOUBLE_QUOTE && @source[@position + 1] == Constants::DOUBLE_QUOTE
      @position += 2
    elsif @source[@position] == Constants::DOUBLE_QUOTE
      @position += 1
      return
    else
      @position += 1
    end
  end
end

#skip_past_keyword!(keyword) ⇒ Object



309
310
311
312
# File 'lib/sql_beautifier/scanner.rb', line 309

def skip_past_keyword!(keyword)
  @position += keyword.length
  skip_whitespace!
end

#skip_quoted_or_sentinel! ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/sql_beautifier/scanner.rb', line 191

def skip_quoted_or_sentinel!
  if @in_single_quote
    advance_through_single_quote!
    return true
  end

  if @in_double_quote
    advance_through_double_quote!
    return true
  end

  if sentinel_at?
    skip_sentinel!
    return true
  end

  delimiter = dollar_quote_delimiter_at
  if delimiter
    skip_dollar_quoted_string!(delimiter)
    return true
  end

  false
end

#skip_sentinel! ⇒ Object



160
161
162
# File 'lib/sql_beautifier/scanner.rb', line 160

def skip_sentinel!
  @position = sentinel_end_position
end

#skip_single_quoted_string! ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/sql_beautifier/scanner.rb', line 130

def skip_single_quoted_string!
  @position += 1

  while @position < @source.length
    if @source[@position] == Constants::SINGLE_QUOTE && @source[@position + 1] == Constants::SINGLE_QUOTE
      @position += 2
    elsif @source[@position] == Constants::SINGLE_QUOTE
      @position += 1
      return
    else
      @position += 1
    end
  end
end

#skip_whitespace! ⇒ Object



305
306
307
# File 'lib/sql_beautifier/scanner.rb', line 305

def skip_whitespace!
  @position += 1 while @position < @source.length && @source[@position] =~ Constants::WHITESPACE_CHARACTER_REGEX
end

#top_level? ⇒ Boolean

Returns:

  • (Boolean)


32
33
34
# File 'lib/sql_beautifier/scanner.rb', line 32

def top_level?
  @parenthesis_depth.zero? && !@in_single_quote && !@in_double_quote
end

#word_boundary?(character) ⇒ Boolean

Returns:

  • (Boolean)


280
281
282
# File 'lib/sql_beautifier/scanner.rb', line 280

def word_boundary?(character)
  character.nil? || character !~ IDENTIFIER_CHARACTER
end