Module: ScopeHunter::ASTUtils

Extended by:
ASTUtils
Included in:
ASTUtils
Defined in:
lib/scope_hunter/ast_utils.rb

Constant Summary collapse

AR_METHODS =
%i[where rewhere joins order limit offset select distinct group having references includes preload].freeze

Instance Method Summary collapse

Instance Method Details

#const_name(node) ⇒ Object



64
65
66
67
68
# File 'lib/scope_hunter/ast_utils.rb', line 64

def const_name(node)
  node.const_name
rescue NoMethodError
  nil
end

#hash_to_ruby(node) ⇒ Object



74
75
76
# File 'lib/scope_hunter/ast_utils.rb', line 74

def hash_to_ruby(node)
  node.pairs.to_h { |p| [literal(p.key), literal(p.value)] }
end

#literal(node) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/scope_hunter/ast_utils.rb', line 78

def literal(node)
  case
  when node.nil? then nil
  when node.sym_type? then node.value
  when node.str_type? then node.value
  when node.int_type? then node.value
  when node.float_type? then node.value
  when node.true_type? then true
  when node.false_type? then false
  when node.const_type? then node.const_name.to_sym
  else :__dynamic__ # lvar, ivar, send, etc. — normalized to ? by Canonicalizer,
                    # enabling parameterized scope matching
  end
end

#model_const?(node) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/scope_hunter/ast_utils.rb', line 60

def model_const?(node)
  node&.const_type?
end

#relation_chain(node, require_model: true) ⇒ Object

Returns array of steps: ["User", msg: :where, args: [{status: :active]}, ...] Handles the compound where.not(...) pattern as a single :where_not step.



13
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
# File 'lib/scope_hunter/ast_utils.rb', line 13

def relation_chain(node, require_model: true)
  return nil unless node&.send_type?

  chain = []
  cur = node
  seen_ar = false

  while cur&.send_type?
    msg  = cur.method_name
    recv = cur.receiver
    args = cur.arguments

    if where_not_node?(cur)
      # `recv` is the bare `.where` call; its receiver is the model (or nil in scope body)
      model_recv = recv.receiver
      chain.unshift({
        recv: model_recv&.const_type? ? const_name(model_recv) : nil,
        msg:  :where_not,
        args: unwrap_args(args)
      })
      seen_ar = true
      cur = model_recv
    elsif AR_METHODS.include?(msg) || model_const?(recv)
      chain.unshift({
        recv: recv&.const_type? ? const_name(recv) : nil,
        msg:  msg,
        args: unwrap_args(args)
      })
      seen_ar ||= AR_METHODS.include?(msg)
      cur = recv
    else
      break
    end
  end

  return nil unless seen_ar
  return nil if require_model && chain.first[:recv].nil? # must start from Model constant
  chain
end

#unwrap_args(args) ⇒ Object



70
71
72
# File 'lib/scope_hunter/ast_utils.rb', line 70

def unwrap_args(args)
  args.map { |a| a.hash_type? ? hash_to_ruby(a) : literal(a) }
end

#where_not_node?(node) ⇒ Boolean

Returns:

  • (Boolean)


53
54
55
56
57
58
# File 'lib/scope_hunter/ast_utils.rb', line 53

def where_not_node?(node)
  node.method_name == :not &&
    node.receiver&.send_type? &&
    node.receiver.method_name == :where &&
    node.receiver.arguments.empty?
end