Module: WhyClasses::AST::NodeHelpers
- Defined in:
- lib/why_classes/ast/node_helpers.rb
Overview
Predicates over the parser-gem AST. Every helper operates on the direct body of a class/module node and never recurses into nested classes, so a rule handling an outer class does not accidentally analyse an inner one.
Constant Summary collapse
- Node =
::Parser::AST::Node
- METAPROGRAMMING_SENDS =
%i[ define_method define_singleton_method instance_variable_set instance_variable_get method_missing respond_to_missing? class_eval instance_eval module_eval const_set attr define_attribute_methods ].freeze
- NESTED_SCOPE_TYPES =
%i[class module].freeze
Class Method Summary collapse
-
.attr_fields(class_node) ⇒ Object
All field symbols exposed by any attr_* macro.
-
.attr_macros(class_node) ⇒ Object
attr_reader/attr_writer/attr_accessor macro calls -> flat list of field symbols keyed by macro name.
-
.body_statements(body) ⇒ Object
The list of top-level statements in a class/module/def body.
- .class_body(class_node) ⇒ Object
- .class_name(class_node) ⇒ Object
-
.collect(node, cross_scopes: false, &block) ⇒ Object
Recursively collect nodes matching the block.
-
.const_path(const_node) ⇒ Object
"Foo::Bar" for a const node, else nil.
-
.initialize_method(class_node) ⇒ Object
Names of
def initializein the body. -
.initialize_only_assigns_params?(class_node) ⇒ Boolean
True when initialize only assigns instance variables straight from its own parameters (no logic, no defaults, no computed values).
-
.instance_methods(class_node) ⇒ Object
Instance method definitions declared directly in the body (:def).
-
.ivars_assigned(node) ⇒ Object
Instance variables assigned anywhere in the subtree.
-
.ivars_read(node) ⇒ Object
Instance variables read anywhere in the subtree.
-
.keyword_params?(args_node) ⇒ Boolean
True when all parameters are keyword arguments.
- .node?(obj) ⇒ Boolean
-
.param_names(args_node) ⇒ Object
Parameter names of an :args node.
-
.simple_params?(args_node) ⇒ Boolean
True when every parameter is a plain required positional (:arg) or required keyword (:kwarg) -- no defaults, splats, or blocks.
-
.singleton_methods(class_node) ⇒ Object
Singleton methods:
def self.foo(:defs) plus methods insideclass << self(:sclass) blocks. -
.superclass_path(class_node) ⇒ Object
Superclass const path for a :class node, or nil.
-
.uses_metaprogramming?(class_node) ⇒ Boolean
Static analysis is unreliable in the presence of metaprogramming, so any rule that would rewrite code must bail to advice-only when this is true.
Class Method Details
.attr_fields(class_node) ⇒ Object
All field symbols exposed by any attr_* macro.
82 83 84 |
# File 'lib/why_classes/ast/node_helpers.rb', line 82 def attr_fields(class_node) attr_macros(class_node).values.flatten.uniq end |
.attr_macros(class_node) ⇒ Object
attr_reader/attr_writer/attr_accessor macro calls -> flat list of field symbols keyed by macro name.
67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/why_classes/ast/node_helpers.rb', line 67 def attr_macros(class_node) result = { attr_reader: [], attr_writer: [], attr_accessor: [] } body_statements(class_body(class_node)).each do |stmt| next unless node?(stmt) && stmt.type == :send && stmt.children[0].nil? macro = stmt.children[1] next unless result.key?(macro) fields = stmt.children[2..].select { |a| node?(a) && a.type == :sym }.map { |a| a.children[0] } result[macro].concat(fields) end result end |
.body_statements(body) ⇒ Object
The list of top-level statements in a class/module/def body.
18 19 20 21 22 23 |
# File 'lib/why_classes/ast/node_helpers.rb', line 18 def body_statements(body) return [] if body.nil? return body.children if body.type == :begin [body] end |
.class_body(class_node) ⇒ Object
26 27 28 |
# File 'lib/why_classes/ast/node_helpers.rb', line 26 def class_body(class_node) class_node.type == :module ? class_node.children[1] : class_node.children[2] end |
.class_name(class_node) ⇒ Object
38 39 40 |
# File 'lib/why_classes/ast/node_helpers.rb', line 38 def class_name(class_node) const_path(class_node.children[0]) end |
.collect(node, cross_scopes: false, &block) ⇒ Object
Recursively collect nodes matching the block. By default the walk stays
within the current type definition: it descends into method bodies
(def/defs) and class << self (sclass) but not into nested class or
module definitions, so a class's analysis never bleeds into an inner one.
164 165 166 167 168 |
# File 'lib/why_classes/ast/node_helpers.rb', line 164 def collect(node, cross_scopes: false, &block) found = [] walk_local(node, cross_scopes, found, &block) found end |
.const_path(const_node) ⇒ Object
"Foo::Bar" for a const node, else nil.
31 32 33 34 35 36 |
# File 'lib/why_classes/ast/node_helpers.rb', line 31 def const_path(const_node) return nil unless node?(const_node) && const_node.type == :const scope, name = const_node.children scope.nil? ? name.to_s : "#{const_path(scope)}::#{name}" end |
.initialize_method(class_node) ⇒ Object
Names of def initialize in the body.
97 98 99 |
# File 'lib/why_classes/ast/node_helpers.rb', line 97 def initialize_method(class_node) instance_methods(class_node).find { |m| m.children[0] == :initialize } end |
.initialize_only_assigns_params?(class_node) ⇒ Boolean
True when initialize only assigns instance variables straight from its own parameters (no logic, no defaults, no computed values).
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/why_classes/ast/node_helpers.rb', line 103 def initialize_only_assigns_params?(class_node) init = initialize_method(class_node) return false unless init params = param_names(init.children[1]) # No default values / splats / blocks allowed. return false unless simple_params?(init.children[1]) assignments = body_statements(init.children[2]) return false if assignments.empty? assignments.all? do |stmt| node?(stmt) && stmt.type == :ivasgn && (value = stmt.children[1]) && value.type == :lvar && params.include?(value.children[0]) end end |
.instance_methods(class_node) ⇒ Object
Instance method definitions declared directly in the body (:def).
50 51 52 |
# File 'lib/why_classes/ast/node_helpers.rb', line 50 def instance_methods(class_node) body_statements(class_body(class_node)).select { |n| node?(n) && n.type == :def } end |
.ivars_assigned(node) ⇒ Object
Instance variables assigned anywhere in the subtree.
87 88 89 |
# File 'lib/why_classes/ast/node_helpers.rb', line 87 def ivars_assigned(node) collect(node) { |n| n.type == :ivasgn }.map { |n| n.children[0] }.uniq end |
.ivars_read(node) ⇒ Object
Instance variables read anywhere in the subtree.
92 93 94 |
# File 'lib/why_classes/ast/node_helpers.rb', line 92 def ivars_read(node) collect(node) { |n| n.type == :ivar }.map { |n| n.children[0] }.uniq end |
.keyword_params?(args_node) ⇒ Boolean
True when all parameters are keyword arguments.
138 139 140 141 142 |
# File 'lib/why_classes/ast/node_helpers.rb', line 138 def keyword_params?(args_node) return false if args_node.nil? || args_node.children.empty? args_node.children.all? { |a| node?(a) && %i[kwarg kwoptarg].include?(a.type) } end |
.node?(obj) ⇒ Boolean
13 14 15 |
# File 'lib/why_classes/ast/node_helpers.rb', line 13 def node?(obj) obj.is_a?(Node) end |
.param_names(args_node) ⇒ Object
Parameter names of an :args node.
123 124 125 126 127 |
# File 'lib/why_classes/ast/node_helpers.rb', line 123 def param_names(args_node) return [] unless node?(args_node) args_node.children.map { |a| a.children[0] } end |
.simple_params?(args_node) ⇒ Boolean
True when every parameter is a plain required positional (:arg) or required keyword (:kwarg) -- no defaults, splats, or blocks.
131 132 133 134 135 |
# File 'lib/why_classes/ast/node_helpers.rb', line 131 def simple_params?(args_node) return true if args_node.nil? args_node.children.all? { |a| node?(a) && %i[arg kwarg].include?(a.type) } end |
.singleton_methods(class_node) ⇒ Object
Singleton methods: def self.foo (:defs) plus methods inside
class << self (:sclass) blocks.
56 57 58 59 60 61 62 63 |
# File 'lib/why_classes/ast/node_helpers.rb', line 56 def singleton_methods(class_node) stmts = body_statements(class_body(class_node)) defs = stmts.select { |n| node?(n) && n.type == :defs } sclass_defs = stmts.select { |n| node?(n) && n.type == :sclass } .flat_map { |sc| body_statements(sc.children[1]) } .select { |n| node?(n) && n.type == :def } defs + sclass_defs end |
.superclass_path(class_node) ⇒ Object
Superclass const path for a :class node, or nil.
43 44 45 46 47 |
# File 'lib/why_classes/ast/node_helpers.rb', line 43 def superclass_path(class_node) return nil unless class_node.type == :class const_path(class_node.children[1]) end |
.uses_metaprogramming?(class_node) ⇒ Boolean
Static analysis is unreliable in the presence of metaprogramming, so any rule that would rewrite code must bail to advice-only when this is true.
153 154 155 156 157 158 |
# File 'lib/why_classes/ast/node_helpers.rb', line 153 def (class_node) collect(class_node) do |n| (n.type == :send && METAPROGRAMMING_SENDS.include?(n.children[1])) || n.type == :def && %i[method_missing respond_to_missing?].include?(n.children[0]) end.any? end |