Class: Bullematic::AST::Finder
- Inherits:
-
Object
- Object
- Bullematic::AST::Finder
- Defined in:
- lib/bullematic/ast/finder.rb,
sig/generated/bullematic/ast/finder.rbs
Defined Under Namespace
Classes: QueryLocation
Constant Summary collapse
- QUERY_METHODS =
%i[all where find find_by first last order limit offset].freeze
Instance Attribute Summary collapse
- #parse_result ⇒ Object readonly
Instance Method Summary collapse
- #add_block_receiver_names(node, line_number, names) ⇒ void
- #association_receiver_names_at_line(node, line_number, associations) ⇒ Array[Symbol]
- #block_parameter_at_line?(node, name, line_number) ⇒ Boolean
- #call_on_variable_at_line?(node, method_name, names, line_number) ⇒ Boolean
- #check_assignment_node(node, queries, model_class_name, target_line) ⇒ Boolean
- #check_call_node(node, queries, model_class_name, target_line) ⇒ void
- #find_model_queries(model_class_name, line_number: nil) ⇒ Array[QueryLocation]
- #find_model_queries_for_variables_at_line(model_class_name, line_number, associations) ⇒ Array[QueryLocation]
- #find_root_receiver(node) ⇒ Object
-
#initialize(parse_result) ⇒ Finder
constructor
A new instance of Finder.
- #mark_descendant_calls(node, skip_nodes) ⇒ void
- #method_scope_at_line(node, line_number) ⇒ Object
- #model_constant?(node, model_class_name) ⇒ Boolean
- #nested_association?(parent_association, child_associations, line_number) ⇒ Boolean
- #nested_association_in_node?(node, parent_association, child_associations, line_number) ⇒ Boolean
- #query_method?(method_name) ⇒ Boolean
- #receiver_calls?(node, method_name) ⇒ Boolean
- #variable_written?(node, names) ⇒ Boolean
- #variable_written_between?(node, name, start_line, end_line, scope) ⇒ Boolean
- #visit_node(node, queries, model_class_name, target_line, skip_nodes = Set.new) ⇒ void
Constructor Details
#initialize(parse_result) ⇒ Finder
Returns a new instance of Finder.
24 25 26 |
# File 'lib/bullematic/ast/finder.rb', line 24 def initialize(parse_result) @parse_result = parse_result end |
Instance Attribute Details
#parse_result ⇒ Object (readonly)
20 21 22 |
# File 'lib/bullematic/ast/finder.rb', line 20 def parse_result @parse_result end |
Instance Method Details
#add_block_receiver_names(node, line_number, names) ⇒ void
This method returns an undefined value.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/bullematic/ast/finder.rb', line 173 def add_block_receiver_names(node, line_number, names) return unless node.respond_to?(:child_nodes) if node.is_a?(Prism::CallNode) && node.block && line_number.between?(node.block.location.start_line, node.block.location.end_line) parameters = node.block.parameters&.parameters&.requireds || [] receiver = node.receiver if parameters.any? { |parameter| names.include?(parameter.name) } && (receiver.is_a?(Prism::InstanceVariableReadNode) || receiver.is_a?(Prism::LocalVariableReadNode)) names << receiver.name end end node.child_nodes.compact.each { |child| add_block_receiver_names(child, line_number, names) } names.uniq! end |
#association_receiver_names_at_line(node, line_number, associations) ⇒ Array[Symbol]
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/bullematic/ast/finder.rb', line 153 def association_receiver_names_at_line(node, line_number, associations) return [] unless node.respond_to?(:child_nodes) names = [] #: Array[Symbol] if node.is_a?(Prism::CallNode) && associations.include?(node.name) && node.location.start_line == line_number receiver = find_root_receiver(node) if receiver.is_a?(Prism::InstanceVariableReadNode) || receiver.is_a?(Prism::LocalVariableReadNode) names << receiver.name end end node.child_nodes.compact.each do |child| names.concat(association_receiver_names_at_line(child, line_number, associations)) end names.uniq end |
#block_parameter_at_line?(node, name, line_number) ⇒ Boolean
194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/bullematic/ast/finder.rb', line 194 def block_parameter_at_line?(node, name, line_number) return false unless node.respond_to?(:child_nodes) if node.is_a?(Prism::CallNode) && node.block && line_number.between?(node.block.location.start_line, node.block.location.end_line) parameters = node.block.parameters&.parameters&.requireds || [] return true if parameters.any? { |parameter| parameter.name == name } end node.child_nodes.compact.any? { |child| block_parameter_at_line?(child, name, line_number) } end |
#call_on_variable_at_line?(node, method_name, names, line_number) ⇒ Boolean
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/bullematic/ast/finder.rb', line 288 def call_on_variable_at_line?(node, method_name, names, line_number) return false unless node.respond_to?(:child_nodes) if node.is_a?(Prism::CallNode) && node.name == method_name && node.location.start_line == line_number receiver = find_root_receiver(node) return true if receiver.is_a?(Prism::LocalVariableReadNode) && names.include?(receiver.name) end if node.is_a?(Prism::CallNode) && node.block parameters = node.block.parameters&.parameters&.requireds || [] return false if parameters.any? { |parameter| parameter.respond_to?(:name) && names.include?(parameter.name) } end node.child_nodes.compact.any? do |child| call_on_variable_at_line?(child, method_name, names, line_number) end end |
#check_assignment_node(node, queries, model_class_name, target_line) ⇒ Boolean
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/bullematic/ast/finder.rb', line 129 def check_assignment_node(node, queries, model_class_name, target_line) return false if target_line && node.location.start_line != target_line value = node.value return false unless value.is_a?(Prism::CallNode) return false unless query_method?(value.name) root_receiver = find_root_receiver(value) return false unless model_constant?(root_receiver, model_class_name) queries << QueryLocation.new( node: value, location: value.location, receiver: root_receiver, method_name: value.name, target_name: node.name ) true end |
#check_call_node(node, queries, model_class_name, target_line) ⇒ void
This method returns an undefined value.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/bullematic/ast/finder.rb', line 109 def check_call_node(node, queries, model_class_name, target_line) return unless query_method?(node.name) return if target_line && node.location.start_line != target_line root_receiver = find_root_receiver(node) return unless model_constant?(root_receiver, model_class_name) queries << QueryLocation.new( node: node, location: node.location, receiver: root_receiver, method_name: node.name ) end |
#find_model_queries(model_class_name, line_number: nil) ⇒ Array[QueryLocation]
31 32 33 34 35 |
# File 'lib/bullematic/ast/finder.rb', line 31 def find_model_queries(model_class_name, line_number: nil) queries = [] #: Array[QueryLocation] visit_node(parse_result.value, queries, model_class_name, line_number) queries end |
#find_model_queries_for_variables_at_line(model_class_name, line_number, associations) ⇒ Array[QueryLocation]
41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/bullematic/ast/finder.rb', line 41 def find_model_queries_for_variables_at_line(model_class_name, line_number, associations) names = association_receiver_names_at_line(parse_result.value, line_number, associations) add_block_receiver_names(parse_result.value, line_number, names) scope = method_scope_at_line(parse_result.value, line_number) find_model_queries(model_class_name).select do |query| query.target_name && names.include?(query.target_name) && !block_parameter_at_line?(parse_result.value, query.target_name, line_number) && query.location.end_line <= line_number && method_scope_at_line(parse_result.value, query.location.start_line).equal?(scope) && !variable_written_between?(scope || parse_result.value, query.target_name, query.location.end_line, line_number, scope) end end |
#find_root_receiver(node) ⇒ Object
240 241 242 243 244 |
# File 'lib/bullematic/ast/finder.rb', line 240 def find_root_receiver(node) current = node current = current.receiver while current.is_a?(Prism::CallNode) && current.receiver current end |
#mark_descendant_calls(node, skip_nodes) ⇒ void
This method returns an undefined value.
94 95 96 97 98 99 100 101 102 |
# File 'lib/bullematic/ast/finder.rb', line 94 def mark_descendant_calls(node, skip_nodes) return unless node.respond_to?(:child_nodes) skip_nodes.add(node.object_id) if node.is_a?(Prism::CallNode) node.child_nodes.compact.each do |child| mark_descendant_calls(child, skip_nodes) end end |
#method_scope_at_line(node, line_number) ⇒ Object
209 210 211 212 213 214 215 216 217 218 |
# File 'lib/bullematic/ast/finder.rb', line 209 def method_scope_at_line(node, line_number) return unless node.respond_to?(:child_nodes) return unless line_number.between?(node.location.start_line, node.location.end_line) node.child_nodes.compact.each do |child| scope = method_scope_at_line(child, line_number) return scope if scope end node if node.is_a?(Prism::DefNode) end |
#model_constant?(node, model_class_name) ⇒ Boolean
249 250 251 252 253 |
# File 'lib/bullematic/ast/finder.rb', line 249 def model_constant?(node, model_class_name) return false unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) node.full_name.delete_prefix("::") == model_class_name.delete_prefix("::") end |
#nested_association?(parent_association, child_associations, line_number) ⇒ Boolean
59 60 61 62 63 |
# File 'lib/bullematic/ast/finder.rb', line 59 def nested_association?(parent_association, child_associations, line_number) return false unless line_number nested_association_in_node?(parse_result.value, parent_association, child_associations, line_number) end |
#nested_association_in_node?(node, parent_association, child_associations, line_number) ⇒ Boolean
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/bullematic/ast/finder.rb', line 266 def nested_association_in_node?(node, parent_association, child_associations, line_number) return false unless node.respond_to?(:child_nodes) if node.is_a?(Prism::CallNode) && %i[each find_each].include?(node.name) && node.block && receiver_calls?(node.receiver, parent_association) parameters = node.block.parameters&.parameters&.requireds || [] names = parameters.map(&:name) return true if !variable_written?(node.block.body, names) && child_associations.all? do |association| call_on_variable_at_line?(node.block.body, association, names, line_number) end end node.child_nodes.compact.any? do |child| nested_association_in_node?(child, parent_association, child_associations, line_number) end end |
#query_method?(method_name) ⇒ Boolean
257 258 259 |
# File 'lib/bullematic/ast/finder.rb', line 257 def query_method?(method_name) QUERY_METHODS.include?(method_name) end |
#receiver_calls?(node, method_name) ⇒ Boolean
309 310 311 312 313 314 315 316 317 |
# File 'lib/bullematic/ast/finder.rb', line 309 def receiver_calls?(node, method_name) current = node while current.is_a?(Prism::CallNode) return true if current.name == method_name current = current.receiver end false end |
#variable_written?(node, names) ⇒ Boolean
322 323 324 325 326 327 |
# File 'lib/bullematic/ast/finder.rb', line 322 def variable_written?(node, names) return false unless node.respond_to?(:child_nodes) return true if node.is_a?(Prism::LocalVariableWriteNode) && names.include?(node.name) node.child_nodes.compact.any? { |child| variable_written?(child, names) } end |
#variable_written_between?(node, name, start_line, end_line, scope) ⇒ Boolean
226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/bullematic/ast/finder.rb', line 226 def variable_written_between?(node, name, start_line, end_line, scope) return false unless node.respond_to?(:child_nodes) return false if node.is_a?(Prism::DefNode) && !node.equal?(scope) written = node.respond_to?(:name) && node.name == name && node.class.name.match?(/\APrism::(?:Instance|Local)Variable(?:And|Operator|Or)?WriteNode\z/) && node.location.start_line > start_line && node.location.start_line <= end_line written || node.child_nodes.compact.any? do |child| variable_written_between?(child, name, start_line, end_line, scope) end end |
#visit_node(node, queries, model_class_name, target_line, skip_nodes = Set.new) ⇒ void
This method returns an undefined value.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/bullematic/ast/finder.rb', line 73 def visit_node(node, queries, model_class_name, target_line, skip_nodes = Set.new) return unless node.respond_to?(:child_nodes) return if skip_nodes.include?(node.object_id) case node when Prism::InstanceVariableWriteNode, Prism::LocalVariableWriteNode if check_assignment_node(node, queries, model_class_name, target_line) mark_descendant_calls(node.value, skip_nodes) end when Prism::CallNode check_call_node(node, queries, model_class_name, target_line) end node.child_nodes.compact.each do |child| visit_node(child, queries, model_class_name, target_line, skip_nodes) end end |