Module: Kettle::Dev::PrismGemfile

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

Overview

Prism helpers for Gemfile-like merging.

Class Method Summary collapse

Class Method Details

.merge_gem_calls(src_content, dest_content) ⇒ Object

Merge gem calls from src_content into dest_content.

  • Replaces dest source call with src’s if present.
  • Replaces or inserts non-comment git_source definitions.
  • Appends missing gem calls (by name) from src to dest preserving dest content and newlines.
    This is a conservative, comment-preserving approach using Prism to detect call nodes.


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

def merge_gem_calls(src_content, dest_content)
  src_res = PrismUtils.parse_with_comments(src_content)
  dest_res = PrismUtils.parse_with_comments(dest_content)

  src_stmts = PrismUtils.extract_statements(src_res.value.statements)
  dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)

  # Find source nodes
  src_source_node = src_stmts.find { |n| PrismUtils.call_to?(n, :source) }
  dest_source_node = dest_stmts.find { |n| PrismUtils.call_to?(n, :source) }

  out = dest_content.dup
  dest_lines = out.lines

  # Replace or insert source line
  if src_source_node
    src_src = src_source_node.slice
    if dest_source_node
      out = out.sub(dest_source_node.slice, src_src)
      dest_lines = out.lines
    else
      # insert after any leading comment/blank block
      insert_idx = 0
      while insert_idx < dest_lines.length && (dest_lines[insert_idx].strip.empty? || dest_lines[insert_idx].lstrip.start_with?("#"))
        insert_idx += 1
      end
      dest_lines.insert(insert_idx, src_src.rstrip + "\n")
      out = dest_lines.join
      dest_lines = out.lines
    end
  end

  # --- Handle git_source replacement/insertion ---
  src_git_nodes = src_stmts.select { |n| PrismUtils.call_to?(n, :git_source) }
  if src_git_nodes.any?
    # We'll operate on dest_lines for insertion; recompute dest_stmts if we changed out
    dest_res = PrismUtils.parse_with_comments(out)
    dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)

    # Iterate in reverse when inserting so that inserting at the same index
    # preserves the original order from the source (we insert at a fixed index).
    src_git_nodes.reverse_each do |gnode|
      key = PrismUtils.statement_key(gnode) # => [:git_source, name]
      name = key && key[1]
      replaced = false

      if name
        dest_same_idx = dest_stmts.index { |d| PrismUtils.statement_key(d) && PrismUtils.statement_key(d)[0] == :git_source && PrismUtils.statement_key(d)[1] == name }
        if dest_same_idx
          # Replace the matching dest node slice
          out = out.sub(dest_stmts[dest_same_idx].slice, gnode.slice)
          replaced = true
        end
      end

      # If not replaced, prefer to replace an existing github entry in destination
      # (this mirrors previous behavior in template_helpers which favored replacing
      # a github git_source when inserting others).
      unless replaced
        dest_github_idx = dest_stmts.index { |d| PrismUtils.statement_key(d) && PrismUtils.statement_key(d)[0] == :git_source && PrismUtils.statement_key(d)[1] == "github" }
        if dest_github_idx
          out = out.sub(dest_stmts[dest_github_idx].slice, gnode.slice)
          replaced = true
        end
      end

      unless replaced
        # Insert below source line if present, else at top after comments
        dest_lines = out.lines
        insert_idx = dest_lines.index { |ln| !ln.strip.start_with?("#") && ln =~ /^\s*source\s+/ } || 0
        insert_idx += 1 if insert_idx
        dest_lines.insert(insert_idx, gnode.slice.rstrip + "\n")
        out = dest_lines.join
      end

      # Recompute dest_stmts for subsequent iterations
      dest_res = PrismUtils.parse_with_comments(out)
      dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)
    end
  end

  # Collect gem names present in dest (top-level only)
  dest_res = PrismUtils.parse_with_comments(out)
  dest_stmts = PrismUtils.extract_statements(dest_res.value.statements)
  dest_gem_names = dest_stmts.map { |n| PrismUtils.statement_key(n) }.compact.select { |k| k[0] == :gem }.map { |k| k[1] }.to_set

  # Find gem call nodes in src and append missing ones (top-level only)
  missing_nodes = src_stmts.select do |n|
    k = PrismUtils.statement_key(n)
    k && k.first == :gem && !dest_gem_names.include?(k[1])
  end
  if missing_nodes.any?
    out << "\n" unless out.end_with?("\n") || out.empty?
    missing_nodes.each do |n|
      # Preserve inline comments for the source node when appending
      inline = begin
        PrismUtils.inline_comments_for_node(src_res, n)
      rescue
        []
      end
      line = n.slice.rstrip
      if inline && inline.any?
        inline_text = inline.map { |c| c.slice.strip }.join(" ")
        # Only append the inline text if it's not already part of the slice
        line = line + " " + inline_text unless line.include?(inline_text)
      end
      out << line + "\n"
    end
  end

  out
rescue StandardError => e
  # Use debug_log if available, otherwise Kettle::Dev.debug_error
  if defined?(Kettle::Dev) && Kettle::Dev.respond_to?(:debug_error)
    Kettle::Dev.debug_error(e, __method__)
  else
    Kernel.warn("[#{__method__}] #{e.class}: #{e.message}")
  end
  dest_content
end

.remove_gem_dependency(content, gem_name) ⇒ String

Remove gem calls that reference the given gem name (to prevent self-dependency).
Works by locating gem() call nodes where the first argument matches gem_name.

Parameters:

  • content (String)

    Gemfile-like content

  • gem_name (String)

    the gem name to remove

Returns:

  • (String)

    modified content with self-referential gem calls removed



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

def remove_gem_dependency(content, gem_name)
  return content if gem_name.to_s.strip.empty?

  result = PrismUtils.parse_with_comments(content)
  stmts = PrismUtils.extract_statements(result.value.statements)

  # Find gem call nodes where first argument matches gem_name
  gem_nodes = stmts.select do |n|
    next false unless n.is_a?(Prism::CallNode) && n.name == :gem

    first_arg = n.arguments&.arguments&.first
    arg_val = begin
      PrismUtils.extract_literal_value(first_arg)
    rescue StandardError
      nil
    end
    arg_val && arg_val.to_s == gem_name.to_s
  end

  # Remove each matching gem call from content
  out = content.dup
  gem_nodes.each do |gn|
    # Remove the entire line(s) containing this node
    out = out.sub(gn.slice, "")
  end

  out
rescue StandardError => e
  if defined?(Kettle::Dev) && Kettle::Dev.respond_to?(:debug_error)
    Kettle::Dev.debug_error(e, __method__)
  else
    Kernel.warn("[#{__method__}] #{e.class}: #{e.message}")
  end
  content
end