Module: RailsAiContext::SourceLine

Defined in:
lib/rails_ai_context/source_line.rb

Overview

Source-scanning helpers for the tools that read Ruby without a full parse (Turbo broadcast scans, retry_on option scans).

Constant Summary collapse

PERCENT_TYPES =

%-literal type letters that open a string-like literal (%w, %i(), %q{}, %r//, %x``, ...). A bare % followed by a non-alphanumeric delimiter (%(...)) opens one too.

"wWiIqQrsx"
PERCENT_PAIRS =

Paired delimiters nest; anything else closes on the same character.

{ "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze
INTERPOLATING =

Literals whose bodies interpolate (so "#..." must be tracked).

[ '"', "`" ].freeze

Class Method Summary collapse

Class Method Details

.executable_part(line) ⇒ Object

The executable portion of one source line for call-site scanning. A def line - including visibility-prefixed forms like private def or private_class_method def self.x - contributes only its body: nothing for a plain signature (names and parameter defaults are declarations, not calls: def notify(via: :broadcast_x_to)), the part after a top-level = for an endless method, and the part after a top-level ; for a classic one-liner (def x; call; end).



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
# File 'lib/rails_ai_context/source_line.rb', line 133

def executable_part(line)
  m = line.match(/\A\s*(?:[a-z_]\w*\s+)*def\b/)
  return line unless m

  depth = 0
  quote = nil
  chars = line.chars
  i = m.end(0)
  while i < chars.length
    ch = chars[i]
    if ch == "\\"
      i += 2
      next
    end
    if quote
      if ch == "#" && quote == '"' && chars[i + 1] == "{"
        # Interpolation inside a string default: skip to its matching
        # close brace, copying nested strings whole, so quotes and
        # delimiters inside `#{...}` can't flip the walk's state.
        depth_braces = 1
        i += 2
        while i < chars.length && depth_braces.positive?
          case chars[i]
          when "\\" then i += 1
          when "{" then depth_braces += 1
          when "}" then depth_braces -= 1
          when '"', "'"
            inner = chars[i]
            i += 1
            while i < chars.length && chars[i] != inner
              i += chars[i] == "\\" ? 2 : 1
            end
          end
          i += 1
        end
        next
      end
      quote = nil if ch == quote
      i += 1
      next
    end
    case ch
    when '"', "'" then quote = ch
    when "(" then depth += 1
    when ")" then depth -= 1
    when ";"
      # A `;` inside a string default (`sep: "; "`) is content, handled
      # by the quote branch above; a bare top-level `;` ends the
      # signature of a classic one-liner.
      return chars[(i + 1)..].join if depth.zero?
    when "="
      if depth.zero? && chars[i + 1] != "=" && chars[i - 1] != "=" && chars[i - 1] != "!" &&
         chars[i - 1] != "<" && chars[i - 1] != ">"
        return chars[(i + 1)..].join
      end
    end
    i += 1
  end
  ""
end

.percent_delimiter(chars, i) ⇒ Object

The delimiter of a %-literal starting at chars ("%"), or nil when this % is arithmetic/format (e.g. n % 2, "%s" % x).



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

def percent_delimiter(chars, i)
  nxt = chars[i + 1]
  return nil if nxt.nil?

  if PERCENT_TYPES.include?(nxt)
    delim = chars[i + 2]
    delim if delim && delim !~ /[[:alnum:][:space:]]/
  elsif nxt !~ /[[:alnum:][:space:]=]/
    nxt
  end
end

.strip_comment(line) ⇒ Object

Single-line form, kept for call sites that scan line by line.



122
123
124
# File 'lib/rails_ai_context/source_line.rb', line 122

def strip_comment(line)
  strip_comments(line)
end

.strip_comments(text) ⇒ Object

Remove Ruby comments from a source fragment (a single line or several joined lines), respecting string, backtick, and %-literals: a '#' inside "..."/'...'/.../%w is content, and interpolation braces are tracked so a '#' after a closed interpolation still counts as inside its literal. Literal state carries across newlines, so a multi-line string does not lose its tail, and =begin/=end block comments are removed up front. Heuristic, not a lexer - /regex/ literals, ?# char literals, and heredoc bodies are not modeled. Newlines and any trailing-newline shape are preserved.



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
# File 'lib/rails_ai_context/source_line.rb', line 29

def strip_comments(text)
  # =begin/=end block comments first: their prose ("doesn't") would
  # otherwise open phantom string literals that poison the state for
  # the rest of the fragment. Newlines are kept so line numbers hold.
  if text.include?("=begin")
    text = text.gsub(/^=begin\b.*?(?:^=end\b[^\n]*(?:\n|\z)|\z)/m) { |block| "\n" * block.count("\n") }
  end

  # Fast path: no '#' at all (the overwhelmingly common case).
  return text unless text.include?("#")

  chars = text.chars
  out = []
  i = 0
  quote = nil       # the character that closes the current literal
  quote_open = nil  # the opener, when it differs (paired % delimiters)
  interp = 0
  while i < chars.length
    ch = chars[i]
    if ch == "\\"
      out << ch << chars[i + 1].to_s
      i += 2
      next
    end
    if quote
      if interp.positive?
        # A string nested inside the interpolation may contain braces
        # ("#{ "}" }") - copy it whole so they don't skew the count.
        if ch == '"' || ch == "'" || ch == "`"
          inner = ch
          out << ch
          i += 1
          while i < chars.length && chars[i] != inner
            if chars[i] == "\\"
              out << chars[i] << chars[i + 1].to_s
              i += 2
            else
              out << chars[i]
              i += 1
            end
          end
          out << chars[i].to_s
          i += 1
          next
        end
        interp += 1 if ch == "{"
        interp -= 1 if ch == "}"
      elsif ch == "#" && INTERPOLATING.include?(quote) && chars[i + 1] == "{"
        interp = 1
        out << ch << "{"
        i += 2
        next
      elsif ch == quote_open && quote_open != quote
        # nested paired delimiter inside a %-literal: copy to its close
        depth = 1
        out << ch
        i += 1
        while i < chars.length && depth.positive?
          depth += 1 if chars[i] == quote_open
          depth -= 1 if chars[i] == quote
          out << chars[i]
          i += 1
        end
        next
      elsif ch == quote
        quote = nil
        quote_open = nil
      end
    elsif ch == '"' || ch == "'" || ch == "`"
      quote = ch
    elsif ch == "%" && (delim = percent_delimiter(chars, i))
      quote_open = delim
      quote = PERCENT_PAIRS[delim] || delim
      if PERCENT_TYPES.include?(chars[i + 1].to_s)
        out << ch << chars[i + 1] << chars[i + 2]
        i += 3
      else
        out << ch << chars[i + 1]
        i += 2
      end
      next
    elsif ch == "#"
      # comment: skip to (not past) the next newline
      i += 1 until i >= chars.length || chars[i] == "\n"
      next
    end
    out << ch
    i += 1
  end
  out.join
end