Class: Bullematic::AST::Rewriter

Inherits:
Object
  • Object
show all
Defined in:
lib/bullematic/ast/rewriter.rb,
sig/generated/bullematic/ast/rewriter.rbs

Defined Under Namespace

Classes: Modification

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source) ⇒ Rewriter

Returns a new instance of Rewriter.

RBS:

  • source: String

  • return: void

Parameters:

  • source (String)


22
23
24
25
# File 'lib/bullematic/ast/rewriter.rb', line 22

def initialize(source)
  @source = source.dup
  @modifications = []
end

Instance Attribute Details

#modificationsObject (readonly)

RBS:

  • attr_reader source: String
    attr_reader modifications: Array[Modification]

Returns:

  • (Object)


18
19
20
# File 'lib/bullematic/ast/rewriter.rb', line 18

def modifications
  @modifications
end

#sourceObject (readonly)

RBS:

  • attr_reader source: String
    attr_reader modifications: Array[Modification]

Returns:

  • (Object)


18
19
20
# File 'lib/bullematic/ast/rewriter.rb', line 18

def source
  @source
end

Instance Method Details

#add_includes(query_location, associations) ⇒ Symbol

RBS:

  • query_location: Finder::QueryLocation

  • associations: Array[untyped]

  • return: Symbol

Parameters:

Returns:

  • (Symbol)


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
# File 'lib/bullematic/ast/rewriter.rb', line 30

def add_includes(query_location, associations)
  associations = associations.uniq
  return :unsupported if associations.empty?

  return :already_present if already_has_includes?(query_location, associations)

  strategy = Bullematic.configuration&.fix_strategy || :includes
  insert_point = find_insert_point(query_location)
  pending = @modifications.find { |mod| mod.type == :insert && mod.offset == insert_point }
  if pending
    pending.associations = (pending.associations + associations).uniq
    pending.new_text = ".#{strategy}(#{format_associations(pending.associations)})"
    return :changed
  end

  assoc_string = format_associations(associations)
  new_text = ".#{strategy}(#{assoc_string})"

  @modifications << Modification.new(
    type: :insert,
    offset: insert_point,
    byte_length: 0,
    new_text: new_text,
    associations: associations
  )
  :changed
end

#already_has_includes?(query_location, associations) ⇒ Boolean

RBS:

  • query_location: Finder::QueryLocation

  • associations: Array[untyped]

  • return: bool

Parameters:

Returns:

  • (Boolean)


107
108
109
110
111
112
# File 'lib/bullematic/ast/rewriter.rb', line 107

def already_has_includes?(query_location, associations)
  existing = find_existing_includes(query_location.node)
  return false if existing.empty?

  associations.all? { |assoc| existing.include?(assoc) }
end

#collect_literal_associations(node, associations) ⇒ void

This method returns an undefined value.

RBS:

  • node: untyped

  • associations: Array[untyped]

  • return: void

Parameters:

  • node (Object)
  • associations (Array[untyped])


143
144
145
146
147
148
149
150
151
152
153
# File 'lib/bullematic/ast/rewriter.rb', line 143

def collect_literal_associations(node, associations)
  case node
  when Prism::SymbolNode
    associations << node.value.to_sym
  when Prism::ArrayNode
    node.elements.each { |element| collect_literal_associations(element, associations) }
  when Prism::KeywordHashNode, Prism::HashNode
    association = literal_association(node)
    associations << association if association
  end
end

#extract_associations_from_call(call_node) ⇒ Array[untyped]

RBS:

  • call_node: untyped

  • return: Array[untyped]

Parameters:

  • call_node (Object)

Returns:

  • (Array[untyped])


132
133
134
135
136
137
138
# File 'lib/bullematic/ast/rewriter.rb', line 132

def extract_associations_from_call(call_node)
  return [] unless call_node.arguments

  associations = [] #: Array[untyped]
  call_node.arguments.arguments.each { |arg| collect_literal_associations(arg, associations) }
  associations
end

#find_chain_insert_point(node) ⇒ Integer

RBS:

  • node: untyped

  • return: Integer

Parameters:

  • node (Object)

Returns:

  • (Integer)


93
94
95
96
97
98
99
100
101
102
# File 'lib/bullematic/ast/rewriter.rb', line 93

def find_chain_insert_point(node)
  current = node
  current = current.receiver while current.is_a?(Prism::CallNode) && current.receiver.is_a?(Prism::CallNode)

  if current.receiver
    current.receiver.location.end_offset
  else
    current.location.start_offset
  end
end

#find_existing_includes(node) ⇒ Array[Symbol]

RBS:

  • node: untyped

  • return: Array[Symbol]

Parameters:

  • node (Object)

Returns:

  • (Array[Symbol])


116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/bullematic/ast/rewriter.rb', line 116

def find_existing_includes(node)
  includes = [] #: Array[Symbol]
  current = node

  while current.is_a?(Prism::CallNode)
    if %i[includes preload eager_load].include?(current.name)
      includes.concat(extract_associations_from_call(current))
    end
    current = current.receiver
  end

  includes
end

#find_insert_point(query_location) ⇒ Integer

RBS:

  • query_location: Finder::QueryLocation

  • return: Integer

Parameters:

Returns:

  • (Integer)


80
81
82
83
84
85
86
87
88
89
# File 'lib/bullematic/ast/rewriter.rb', line 80

def find_insert_point(query_location)
  node = query_location.node
  receiver = query_location.receiver

  if receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
    receiver.location.end_offset
  else
    find_chain_insert_point(node)
  end
end

#format_association(association, braces = true) ⇒ String

RBS:

  • association: untyped

  • braces: bool

  • return: String

Parameters:

  • association (Object)
  • braces (Boolean) (defaults to: true)

Returns:

  • (String)


185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/bullematic/ast/rewriter.rb', line 185

def format_association(association, braces = true)
  case association
  when Hash
    contents = association.map do |key, value|
      key_source = key.to_s.match?(/\A[a-z_]\w*\z/) ? "#{key}:" : "#{key.inspect} =>"
      "#{key_source} #{format_association(value)}"
    end.join(", ")
    braces ? "{ #{contents} }" : contents
  when Array
    "[#{association.map { |nested| format_association(nested) }.join(', ')}]"
  else
    association.inspect
  end
end

#format_associations(associations) ⇒ String

RBS:

  • associations: Array[untyped]

  • return: String

Parameters:

  • associations (Array[untyped])

Returns:

  • (String)


178
179
180
# File 'lib/bullematic/ast/rewriter.rb', line 178

def format_associations(associations)
  associations.map { |association| format_association(association, false) }.join(", ")
end

#literal_association(node) ⇒ Object

RBS:

  • node: untyped

  • return: untyped

Parameters:

  • node (Object)

Returns:

  • (Object)


157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/bullematic/ast/rewriter.rb', line 157

def literal_association(node)
  case node
  when Prism::SymbolNode
    node.value.to_sym
  when Prism::ArrayNode
    values = node.elements.map { |element| literal_association(element) }
    values unless values.any?(&:nil?)
  when Prism::KeywordHashNode, Prism::HashNode
    pairs = node.elements.map do |element|
      next unless element.is_a?(Prism::AssocNode)

      key = literal_association(element.key)
      value = literal_association(element.value)
      [key, value] if key && value
    end
    pairs.to_h if pairs.none?(&:nil?)
  end
end

#rewriteString

: () -> String

Returns:

  • (String)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/bullematic/ast/rewriter.rb', line 59

def rewrite
  return @source if @modifications.empty?

  validate_modifications!
  sorted = @modifications.sort_by { |m| -m.offset }

  result = @source.dup
  sorted.each do |mod|
    before = result.byteslice(0, mod.offset)
    after = result.byteslice(mod.offset + mod.byte_length..)
    replacement = mod.type == :delete ? "" : mod.new_text
    result = before + replacement + after
  end

  result
end

#validate_modifications!void

This method returns an undefined value.

: () -> void



201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/bullematic/ast/rewriter.rb', line 201

def validate_modifications!
  @modifications.each do |modification|
    limit = modification.offset + modification.byte_length
    raise FixError, "edit is outside the source" if modification.offset.negative? || limit > @source.bytesize
  end

  ranges = @modifications.reject { |modification| modification.byte_length.zero? }
  ranges.combination(2) do |left, right|
    overlap = left.offset < right.offset + right.byte_length && right.offset < left.offset + left.byte_length
    raise FixError, "overlapping edits" if overlap
  end
end