Class: Bullematic::AST::Finder

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

Defined Under Namespace

Classes: QueryLocation

Constant Summary collapse

QUERY_METHODS =

Signature:

  • Array[Symbol]

Returns:

  • (Array[Symbol])
%i[all where find find_by first last order limit offset].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parse_result) ⇒ Finder

Returns a new instance of Finder.

RBS:

  • parse_result: untyped

  • return: void

Parameters:

  • parse_result (Object)


24
25
26
# File 'lib/bullematic/ast/finder.rb', line 24

def initialize(parse_result)
  @parse_result = parse_result
end

Instance Attribute Details

#parse_resultObject (readonly)

RBS:

  • attr_reader parse_result: untyped

Returns:

  • (Object)


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.

RBS:

  • node: untyped

  • line_number: Integer

  • names: Array[Symbol]

  • return: void

Parameters:

  • node (Object)
  • line_number (Integer)
  • names (Array[Symbol])


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]

RBS:

  • node: untyped

  • line_number: Integer

  • associations: Array[Symbol]

  • return: Array[Symbol]

Parameters:

  • node (Object)
  • line_number (Integer)
  • associations (Array[Symbol])

Returns:

  • (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

RBS:

  • node: untyped

  • name: Symbol

  • line_number: Integer

  • return: bool

Parameters:

  • node (Object)
  • name (Symbol)
  • line_number (Integer)

Returns:

  • (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

RBS:

  • node: untyped

  • method_name: Symbol

  • names: Array[Symbol]

  • line_number: Integer

  • return: bool

Parameters:

  • node (Object)
  • method_name (Symbol)
  • names (Array[Symbol])
  • line_number (Integer)

Returns:

  • (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

RBS:

  • node: untyped

  • queries: Array[QueryLocation]

  • model_class_name: String

  • target_line: Integer?

  • return: bool

Parameters:

  • node (Object)
  • queries (Array[QueryLocation])
  • model_class_name (String)
  • target_line (Integer, nil)

Returns:

  • (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.

RBS:

  • node: untyped

  • queries: Array[QueryLocation]

  • model_class_name: String

  • target_line: Integer?

  • return: void

Parameters:

  • node (Object)
  • queries (Array[QueryLocation])
  • model_class_name (String)
  • target_line (Integer, nil)


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]

RBS:

  • model_class_name: String

  • line_number: Integer?

  • return: Array[QueryLocation]

Parameters:

  • model_class_name (String)
  • line_number: (Integer, nil) (defaults to: nil)

Returns:



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]

RBS:

  • model_class_name: String

  • line_number: Integer

  • associations: Array[Symbol]

  • return: Array[QueryLocation]

Parameters:

  • model_class_name (String)
  • line_number (Integer)
  • associations (Array[Symbol])

Returns:



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

RBS:

  • node: untyped

  • return: untyped

Parameters:

  • node (Object)

Returns:

  • (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.

RBS:

  • node: untyped

  • skip_nodes: Set[Integer]

  • return: void

Parameters:

  • node (Object)
  • skip_nodes (Set[Integer])


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

RBS:

  • node: untyped

  • line_number: Integer

  • return: untyped

Parameters:

  • node (Object)
  • line_number (Integer)

Returns:

  • (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

RBS:

  • node: untyped

  • model_class_name: String

  • return: bool

Parameters:

  • node (Object)
  • model_class_name (String)

Returns:

  • (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

RBS:

  • parent_association: Symbol

  • child_associations: Array[Symbol]

  • line_number: Integer?

  • return: bool

Parameters:

  • parent_association (Symbol)
  • child_associations (Array[Symbol])
  • line_number (Integer, nil)

Returns:

  • (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

RBS:

  • node: untyped

  • parent_association: Symbol

  • child_associations: Array[Symbol]

  • line_number: Integer

  • return: bool

Parameters:

  • node (Object)
  • parent_association (Symbol)
  • child_associations (Array[Symbol])
  • line_number (Integer)

Returns:

  • (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

RBS:

  • method_name: Symbol

  • return: bool

Parameters:

  • method_name (Symbol)

Returns:

  • (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

RBS:

  • node: untyped

  • method_name: Symbol

  • return: bool

Parameters:

  • node (Object)
  • method_name (Symbol)

Returns:

  • (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

RBS:

  • node: untyped

  • names: Array[Symbol]

  • return: bool

Parameters:

  • node (Object)
  • names (Array[Symbol])

Returns:

  • (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

RBS:

  • node: untyped

  • name: Symbol

  • start_line: Integer

  • end_line: Integer

  • scope: untyped

  • return: bool

Parameters:

  • node (Object)
  • name (Symbol)
  • start_line (Integer)
  • end_line (Integer)
  • scope (Object)

Returns:

  • (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.

RBS:

  • node: untyped

  • queries: Array[QueryLocation]

  • model_class_name: String

  • target_line: Integer?

  • skip_nodes: Set[Integer]

  • return: void

Parameters:

  • node (Object)
  • queries (Array[QueryLocation])
  • model_class_name (String)
  • target_line (Integer, nil)
  • skip_nodes (Set[Integer]) (defaults to: Set.new)


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