Module: Kettle::Dev::PrismUtils

Defined in:
lib/kettle/dev/prism_utils.rb

Overview

Shared utilities for working with Prism AST nodes.
Provides parsing, node inspection, and source generation helpers
used by both PrismMerger and AppraisalsAstMerger.

Uses Prism’s native methods for source generation (via .slice) to preserve
original formatting and comments. For normalized output (e.g., adding parentheses),
use normalize_call_node instead.

Class Method Summary collapse

Class Method Details

.block_call_to?(node, method_name) ⇒ Boolean

Check if a node is a block call to a specific method

Parameters:

  • node (Prism::Node)

    Node to check

  • method_name (Symbol)

    Method name to check for

Returns:

  • (Boolean)

    True if node is a block call to the specified method



196
197
198
# File 'lib/kettle/dev/prism_utils.rb', line 196

def block_call_to?(node, method_name)
  node.is_a?(Prism::CallNode) && node.name == method_name && !node.block.nil?
end

.call_to?(node, method_name) ⇒ Boolean

Check if a node is a specific method call

Parameters:

  • node (Prism::Node)

    Node to check

  • method_name (Symbol)

    Method name to check for

Returns:

  • (Boolean)

    True if node is a call to the specified method



188
189
190
# File 'lib/kettle/dev/prism_utils.rb', line 188

def call_to?(node, method_name)
  node.is_a?(Prism::CallNode) && node.name == method_name
end

.extract_const_name(node) ⇒ String?

Extract qualified constant name from a constant node

Parameters:

  • node (Prism::Node, nil)

    Constant node

Returns:

  • (String, nil)

    Qualified name like “Gem::Specification” or nil



78
79
80
81
82
83
84
85
86
87
# File 'lib/kettle/dev/prism_utils.rb', line 78

def extract_const_name(node)
  case node
  when Prism::ConstantReadNode
    node.name.to_s
  when Prism::ConstantPathNode
    parent = extract_const_name(node.parent)
    child = node.name || node.child&.name
    (parent && child) ? "#{parent}::#{child}" : child.to_s
  end
end

.extract_literal_value(node) ⇒ String, ...

Extract literal value from string or symbol nodes

Parameters:

  • node (Prism::Node, nil)

    Node to extract from

Returns:

  • (String, Symbol, nil)

    Literal value or nil



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/kettle/dev/prism_utils.rb', line 55

def extract_literal_value(node)
  return unless node
  case node
  when Prism::StringNode then node.unescaped
  when Prism::SymbolNode then node.unescaped
  else
    # Attempt to handle array literals
    if node.respond_to?(:elements) && node.elements
      arr = node.elements.map do |el|
        case el
        when Prism::StringNode then el.unescaped
        when Prism::SymbolNode then el.unescaped
        end
      end
      return arr if arr.all?
    end
    nil
  end
end

.extract_statements(body_node) ⇒ Array<Prism::Node>

Extract statements from a Prism body node

Parameters:

  • body_node (Prism::Node, nil)

    Body node (typically StatementsNode)

Returns:

  • (Array<Prism::Node>)

    Array of statement nodes



27
28
29
30
31
32
33
34
35
# File 'lib/kettle/dev/prism_utils.rb', line 27

def extract_statements(body_node)
  return [] unless body_node

  if body_node.is_a?(Prism::StatementsNode)
    body_node.body.compact
  else
    [body_node].compact
  end
end

.find_leading_comments(parse_result, current_stmt, prev_stmt, body_node) ⇒ Array<Prism::Comment>

Find leading comments for a statement node
Leading comments are those that appear after the previous statement
and before the current statement

Parameters:

  • parse_result (Prism::ParseResult)

    Parse result with comments

  • current_stmt (Prism::Node)

    Current statement node

  • prev_stmt (Prism::Node, nil)

    Previous statement node

  • body_node (Prism::Node)

    Body containing the statements

Returns:

  • (Array<Prism::Comment>)

    Leading comments



97
98
99
100
101
102
103
104
105
# File 'lib/kettle/dev/prism_utils.rb', line 97

def find_leading_comments(parse_result, current_stmt, prev_stmt, body_node)
  start_line = prev_stmt ? prev_stmt.location.end_line : body_node.location.start_line
  end_line = current_stmt.location.start_line

  parse_result.comments.select do |comment|
    comment.location.start_line > start_line &&
      comment.location.start_line < end_line
  end
end

.inline_comments_for_node(parse_result, stmt) ⇒ Array<Prism::Comment>

Find inline comments for a statement node
Inline comments are those that appear on the same line as the statement’s end

Parameters:

  • parse_result (Prism::ParseResult)

    Parse result with comments

  • stmt (Prism::Node)

    Statement node

Returns:

  • (Array<Prism::Comment>)

    Inline comments



112
113
114
115
116
117
# File 'lib/kettle/dev/prism_utils.rb', line 112

def inline_comments_for_node(parse_result, stmt)
  parse_result.comments.select do |comment|
    comment.location.start_line == stmt.location.end_line &&
      comment.location.start_offset > stmt.location.end_offset
  end
end

.node_to_source(node) ⇒ String

Convert a Prism AST node to Ruby source code
Uses Prism’s native slice method which preserves the original source exactly.
This is preferable to Unparser for Prism nodes as it maintains original formatting
and comments without requiring transformation.

Parameters:

  • node (Prism::Node)

    AST node

Returns:

  • (String)

    Ruby source code



125
126
127
128
129
# File 'lib/kettle/dev/prism_utils.rb', line 125

def node_to_source(node)
  return "" unless node
  # Prism nodes have a slice method that returns the original source
  node.slice
end

.normalize_argument(arg) ⇒ String

Normalize an argument node to canonical format

Parameters:

  • arg (Prism::Node)

    Argument node

Returns:

  • (String)

    Normalized argument source



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
# File 'lib/kettle/dev/prism_utils.rb', line 152

def normalize_argument(arg)
  case arg
  when Prism::StringNode
    "\"#{arg.unescaped}\""
  when Prism::SymbolNode
    ":#{arg.unescaped}"
  when Prism::KeywordHashNode
    # Handle hash arguments like {key: value}
    pairs = arg.elements.map do |assoc|
      key = case assoc.key
      when Prism::SymbolNode then "#{assoc.key.unescaped}:"
      when Prism::StringNode then "\"#{assoc.key.unescaped}\" =>"
      else "#{assoc.key.slice} =>"
      end
      value = normalize_argument(assoc.value)
      "#{key} #{value}"
    end.join(", ")
    pairs
  when Prism::HashNode
    # Handle explicit hash syntax
    pairs = arg.elements.map do |assoc|
      key_part = normalize_argument(assoc.key)
      value_part = normalize_argument(assoc.value)
      "#{key_part} => #{value_part}"
    end.join(", ")
    "{#{pairs}}"
  else
    # For other types (numbers, arrays, etc.), use the original source
    arg.slice.strip
  end
end

.normalize_call_node(node) ⇒ String

Normalize a call node to use parentheses format
Converts gem "foo" to gem("foo") style

Parameters:

  • node (Prism::CallNode)

    Call node

Returns:

  • (String)

    Normalized source code



135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/kettle/dev/prism_utils.rb', line 135

def normalize_call_node(node)
  return node.slice.strip unless node.is_a?(Prism::CallNode)

  method_name = node.name
  args = node.arguments&.arguments || []

  if args.empty?
    "#{method_name}()"
  else
    arg_strings = args.map { |arg| normalize_argument(arg) }
    "#{method_name}(#{arg_strings.join(", ")})"
  end
end

.parse_with_comments(source) ⇒ Prism::ParseResult

Parse Ruby source code and return Prism parse result with comments

Parameters:

  • source (String)

    Ruby source code

Returns:

  • (Prism::ParseResult)

    Parse result containing AST and comments



20
21
22
# File 'lib/kettle/dev/prism_utils.rb', line 20

def parse_with_comments(source)
  Prism.parse(source)
end

.statement_key(node, tracked_methods: %i[gem source eval_gemfile git_source])) ⇒ Array?

Generate a unique key for a statement node to identify equivalent statements
Used for merge/append operations to detect duplicates

Parameters:

  • node (Prism::Node)

    Statement node

  • tracked_methods (Array<Symbol>) (defaults to: %i[gem source eval_gemfile git_source]))

    Methods to track (default: gem, source, eval_gemfile, git_source)

Returns:

  • (Array, nil)

    Key array like [:gem, “foo”] or nil if not trackable



42
43
44
45
46
47
48
49
50
# File 'lib/kettle/dev/prism_utils.rb', line 42

def statement_key(node, tracked_methods: %i[gem source eval_gemfile git_source])
  return unless node.is_a?(Prism::CallNode)
  return unless tracked_methods.include?(node.name)

  first_arg = node.arguments&.arguments&.first
  arg_value = extract_literal_value(first_arg)

  [node.name, arg_value] if arg_value
end