Module: RailsCodeHealth::ASTHelpers

Included in:
RailsAnalyzer, RubyAnalyzer
Defined in:
lib/rails_code_health/ast_helpers.rb

Constant Summary collapse

SCOPE_BOUNDARY_TYPES =
%i[class module def defs sclass].freeze
VISIBILITY_MODIFIERS =
%i[public private protected].freeze

Instance Method Summary collapse

Instance Method Details

#class_body_sends(class_node, method_name) ⇒ Object

Returns direct ‘:send` nodes in the class body matching the given method name. Does not descend into nested classes, modules, or method bodies.

NOTE: calls wrapped in a block (e.g., ‘included do … end` in concerns) are NOT matched — the wrapping :block node is not a :send. Use class_body_children directly and inspect :block nodes if you need to find macro calls inside such wrappers.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rails_code_health/ast_helpers.rb', line 61

def class_body_sends(class_node, method_name)
  matches = []
  return matches unless class_node.is_a?(Parser::AST::Node)
  return matches unless %i[class module].include?(class_node.type)

  class_body_children(class_node).each do |child|
    next unless child.is_a?(Parser::AST::Node)
    next unless child.type == :send
    # Receiver-less send only (e.g., `has_many :foo`, not `MyMod.has_many :foo`)
    next unless child.children[0].nil?
    matches << child if child.children[1] == method_name
  end

  matches
end

#defs_by_visibility(class_node) ⇒ Object

Returns a hash { public: [def_nodes], private: [def_nodes], protected: [def_nodes] } for the given class node. Handles bare modifier blocks and inline ‘private def foo`. Defs inside `class << self` are excluded (the :sclass node is not descended into).

NOTE: ‘private :symbol_name` / `private :a, :b` forms are NOT handled —methods so marked stay classified as :public. Only bare modifier blocks (`private` on its own line) and inline `private def foo` are recognized.



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
52
# File 'lib/rails_code_health/ast_helpers.rb', line 22

def defs_by_visibility(class_node)
  result = { public: [], private: [], protected: [] }
  return result unless class_node.is_a?(Parser::AST::Node)
  return result unless %i[class module].include?(class_node.type)

  current = :public
  body = class_body_children(class_node)

  body.each do |child|
    next unless child.is_a?(Parser::AST::Node)

    if child.type == :send && child.children[0].nil? &&
       VISIBILITY_MODIFIERS.include?(child.children[1])
      modifier = child.children[1]
      if child.children.length == 2
        # bare modifier — changes default for subsequent defs
        current = modifier
      else
        # inline form: `private def foo` or `private :foo`
        inline_target = child.children[2]
        if inline_target.is_a?(Parser::AST::Node) && inline_target.type == :def
          result[modifier] << inline_target
        end
      end
    elsif child.type == :def
      result[current] << child
    end
  end

  result
end

#erb_ruby_fragments(source) ⇒ Object

Yields each Ruby code fragment inside <% %> / <%= %> / <%- %> tags. Multi-line tags are handled. ERB comment tags (<%# … %>) are skipped. Returns an Enumerator if no block given.



80
81
82
83
84
85
86
# File 'lib/rails_code_health/ast_helpers.rb', line 80

def erb_ruby_fragments(source)
  return enum_for(:erb_ruby_fragments, source) unless block_given?

  source.scan(/<%(?!#)=?-?(.*?)-?%>/m) do |match|
    yield match[0]
  end
end

#find_nodes(node, type) {|node| ... } ⇒ Object

Yields every descendant (and the node itself) of the given type. Descends into all children.

Yields:

  • (node)


8
9
10
11
12
13
# File 'lib/rails_code_health/ast_helpers.rb', line 8

def find_nodes(node, type, &block)
  return unless node.is_a?(Parser::AST::Node)

  yield(node) if node.type == type
  node.children.each { |child| find_nodes(child, type, &block) }
end

#find_nodes_in_scope(node, type) {|node| ... } ⇒ Object

Like find_nodes, but does not descend into nested class/module/def/defs/sclass nodes. The starting node itself is inspected; all children are inspected, but nested scope-introducing constructs are not recursed into.

Yields:

  • (node)


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rails_code_health/ast_helpers.rb', line 91

def find_nodes_in_scope(node, type, &block)
  return unless node.is_a?(Parser::AST::Node)

  yield(node) if node.type == type

  node.children.each do |child|
    next unless child.is_a?(Parser::AST::Node)

    # Scope-boundary children get yielded if they match the target type,
    # but we do not recurse into them.
    if SCOPE_BOUNDARY_TYPES.include?(child.type)
      yield(child) if child.type == type
      next
    end

    find_nodes_in_scope(child, type, &block)
  end
end