Class: Bullematic::AST::Rewriter
- Inherits:
-
Object
- Object
- Bullematic::AST::Rewriter
- Defined in:
- lib/bullematic/ast/rewriter.rb,
sig/generated/bullematic/ast/rewriter.rbs
Defined Under Namespace
Classes: Modification
Instance Attribute Summary collapse
- #modifications ⇒ Object readonly
- #source ⇒ Object readonly
Instance Method Summary collapse
- #add_includes(query_location, associations) ⇒ Symbol
- #already_has_includes?(query_location, associations) ⇒ Boolean
- #collect_literal_associations(node, associations) ⇒ void
- #extract_associations_from_call(call_node) ⇒ Array[untyped]
- #find_chain_insert_point(node) ⇒ Integer
- #find_existing_includes(node) ⇒ Array[Symbol]
- #find_insert_point(query_location) ⇒ Integer
- #format_association(association, braces = true) ⇒ String
- #format_associations(associations) ⇒ String
-
#initialize(source) ⇒ Rewriter
constructor
A new instance of Rewriter.
- #literal_association(node) ⇒ Object
-
#rewrite ⇒ String
: () -> String.
-
#validate_modifications! ⇒ void
: () -> void.
Constructor Details
#initialize(source) ⇒ Rewriter
Returns a new instance of Rewriter.
22 23 24 25 |
# File 'lib/bullematic/ast/rewriter.rb', line 22 def initialize(source) @source = source.dup @modifications = [] end |
Instance Attribute Details
#modifications ⇒ Object (readonly)
18 19 20 |
# File 'lib/bullematic/ast/rewriter.rb', line 18 def modifications @modifications end |
#source ⇒ Object (readonly)
18 19 20 |
# File 'lib/bullematic/ast/rewriter.rb', line 18 def source @source end |
Instance Method Details
#add_includes(query_location, associations) ⇒ 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
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.
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]
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
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]
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
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
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
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
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 |
#rewrite ⇒ String
: () -> 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 |