class RuboCop::AST::Node
rubocop:disable Metrics/ClassLength
lvar_node = node.each_descendant.find(&:lvar_type?)
# Find the first lvar node under the receiver node.
node.defined_type? # Equivalent to: ‘node.type == :defined?`
# Non-word characters (other than a-zA-Z0-9_) in type names are omitted.
node.op_asgn_type? # Equivalent to: `node.type == :op_asgn`
node.send_type? # Equivalent to: `node.type == :send`
@example
It has predicate methods for every node type, like this:
the power of `Enumerable`.
access to parent nodes and an object-oriented way to traverse an AST with
`RuboCop::AST::Node` is a subclass of `Parser::AST::Node`. It provides
def ancestors
-
(Array- an array of ancestor nodes)
def ancestors each_ancestor.to_a end
def argument?
def argument? parent&.send_type? && parent.arguments.include?(self) end
def assignment?
def assignment? ASSIGNMENTS.include?(type) end
def basic_conditional?
def basic_conditional? BASIC_CONDITIONALS.include?(type) end
def basic_literal?
def basic_literal? BASIC_LITERALS.include?(type) end
def begin_value_used?
def begin_value_used? # the last child node determines the value of the parent sibling_index == parent.children.size - 1 ? parent.value_used? : false end
def boolean_type?
def boolean_type? true_type? || false_type? end
def call_type?
def call_type? send_type? || csend_type? end
def case_if_value_used?
def case_if_value_used? # (case <condition> <when...>) # (if <condition> <truebranch> <falsebranch>) sibling_index.zero? ? true : parent.value_used? end
def chained?
def chained? parent&.call_type? && eql?(parent.receiver) end
def child_nodes
-
(Array- an array of child nodes)
def child_nodes each_child_node.to_a end
def complete!
def complete! @mutable_attributes.freeze each_child_node(&:complete!) end
def complete?
def complete? @mutable_attributes.frozen? end
def conditional?
def conditional? CONDITIONALS.include?(type) end
def const_name
def const_name return unless const_type? namespace, name = *self if namespace && !namespace.cbase_type? "#{namespace.const_name}::#{name}" else name.to_s end end
def defined_module
def defined_module namespace, name = *defined_module0 s(:const, namespace, name) if name end
def defined_module_name
def defined_module_name (const = defined_module) && const.const_name end
def descendants
-
(Array- an array of descendant nodes)
def descendants each_descendant.to_a end
def each_ancestor(*types, &block)
-
(Enumerator)- if no block is given -
(self)- if a block is given
Other tags:
- Yieldparam: node - each ancestor node
Parameters:
-
type_b(Symbol) -- a node type -
type_a(Symbol) -- a node type -
type(Symbol) -- a node type
Overloads:
-
each_ancestor(type_a, type_b, ...) -
each_ancestor(type) -
each_ancestor
def each_ancestor(*types, &block) return to_enum(__method__, *types) unless block_given? visit_ancestors(types, &block) self end
def each_child_node(*types)
-
(Enumerator)- if no block is given -
(self)- if a block is given
Other tags:
- Yieldparam: node - each child node
Parameters:
-
type_b(Symbol) -- a node type -
type_a(Symbol) -- a node type -
type(Symbol) -- a node type
Overloads:
-
each_child_node(type_a, type_b, ...) -
each_child_node(type) -
each_child_node
def each_child_node(*types) return to_enum(__method__, *types) unless block_given? children.each do |child| next unless child.is_a?(Node) yield child if types.empty? || types.include?(child.type) end self end
def each_descendant(*types, &block)
-
(Enumerator)- if no block is given -
(self)- if a block is given
Other tags:
- Yieldparam: node - each descendant node
Parameters:
-
type_b(Symbol) -- a node type -
type_a(Symbol) -- a node type -
type(Symbol) -- a node type
Overloads:
-
each_descendant(type_a, type_b, ...) -
each_descendant(type) -
each_descendant
def each_descendant(*types, &block) return to_enum(__method__, *types) unless block_given? visit_descendants(types, &block) self end
def each_node(*types, &block)
-
(Enumerator)- if no block is given -
(self)- if a block is given
Other tags:
- Yieldparam: node - each node
Parameters:
-
type_b(Symbol) -- a node type -
type_a(Symbol) -- a node type -
type(Symbol) -- a node type
Overloads:
-
each_node(type_a, type_b, ...) -
each_node(type) -
each_node
def each_node(*types, &block) return to_enum(__method__, *types) unless block_given? yield self if types.empty? || types.include?(type) visit_descendants(types, &block) self end
def empty_source?
def empty_source? source_length.zero? end
def equals_asgn?
def equals_asgn? EQUALS_ASSIGNMENTS.include?(type) end
def falsey_literal?
def falsey_literal? FALSEY_LITERALS.include?(type) end
def first_line
def first_line loc.line end
def for_value_used?
def for_value_used? # `for var in enum; body; end` # (for <var> <enum> <body>) sibling_index == 2 ? parent.value_used? : true end
def guard_clause?
def guard_clause? node = and_type? || or_type? ? rhs : self node.match_guard_clause? end
def immutable_literal?
def immutable_literal? IMMUTABLE_LITERALS.include?(type) end
def initialize(type, children = [], properties = {})
- See: https://www.rubydoc.info/gems/ast/AST/Node:initialize -
def initialize(type, children = [], properties = {}) @mutable_attributes = {} # ::AST::Node#initialize freezes itself. super # #parent= may be invoked multiple times for a node because there are # pending nodes while constructing AST and they are replaced later. # For example, `lvar` and `send` type nodes are initially created as an # `ident` type node and fixed to the appropriate type later. # So, the #parent attribute needs to be mutable. each_child_node do |child_node| child_node.parent = self unless child_node.complete? end end
def keyword?
def keyword? return true if special_keyword? || send_type? && prefix_not? return false unless KEYWORDS.include?(type) !OPERATOR_KEYWORDS.include?(type) || loc.operator.is?(type.to_s) end
def last_line
def last_line loc.last_line end
def line_count
def line_count return 0 unless source_range source_range.last_line - source_range.first_line + 1 end
def literal?
def literal? LITERALS.include?(type) end
def multiline?
def multiline? line_count > 1 end
def mutable_literal?
def mutable_literal? MUTABLE_LITERALS.include?(type) end
def node_parts
-
(Array- the different parts of the ndde)
def node_parts to_a end
def nonempty_line_count
def nonempty_line_count source.lines.grep(/\S/).size end
def numeric_type?
def numeric_type? int_type? || float_type? end
def operator_keyword?
def operator_keyword? OPERATOR_KEYWORDS.include?(type) end
def parent
-
(Node, nil)- the parent node or `nil`
def parent @mutable_attributes[:parent] end
def parent=(node)
def parent=(node) @mutable_attributes[:parent] = node end
def parent_module_name
def parent_module_name # what class or module is this method/constant/etc definition in? # returns nil if answer cannot be determined ancestors = each_ancestor(:class, :module, :sclass, :casgn, :block) result = ancestors.map do |ancestor| parent_module_name_part(ancestor) { |full_name| return full_name } end.compact.reverse.join('::') result.empty? ? 'Object' : result end
def parent_module_name_for_block(ancestor)
def parent_module_name_for_block(ancestor) if ancestor.method?(:class_eval) # `class_eval` with no receiver applies to whatever module or class # we are currently in return unless (receiver = ancestor.receiver) yield unless receiver.const_type? receiver.const_name elsif !new_class_or_module_block?(ancestor) yield end end
def parent_module_name_for_sclass(sclass_node)
def parent_module_name_for_sclass(sclass_node) # TODO: look for constant definition and see if it is nested # inside a class or module subject = sclass_node.children[0] if subject.const_type? "#<Class:#{subject.const_name}>" elsif subject.self_type? "#<Class:#{sclass_node.parent_module_name}>" end end
def parent_module_name_part(node)
def parent_module_name_part(node) case node.type when :class, :module, :casgn # TODO: if constant name has cbase (leading ::), then we don't need # to keep traversing up through nested classes/modules node.defined_module_name when :sclass yield parent_module_name_for_sclass(node) else # block parent_module_name_for_block(node) { yield nil } end end
def parenthesized_call?
def parenthesized_call? loc.respond_to?(:begin) && loc.begin && loc.begin.is?('(') end
def pure?
So, is evaluation of this node free of side effects?
expressions which are equivalent in value.
number of times they are evaluated, or replace them with other
and have no side effects, that means we can reorder them, change the
If we know that expressions are useful only for their return values,
effects, and some for both.
Some expressions are evaluated for their value, some for their side
def pure? # Be conservative and return false if we're not sure case type when :__FILE__, :__LINE__, :const, :cvar, :defined?, :false, :float, :gvar, :int, :ivar, :lvar, :nil, :str, :sym, :true, :regopt true when :and, :array, :begin, :case, :dstr, :dsym, :eflipflop, :ensure, :erange, :for, :hash, :if, :iflipflop, :irange, :kwbegin, :not, :or, :pair, :regexp, :until, :until_post, :when, :while, :while_post child_nodes.all?(&:pure?) else false end end
def range_type?
def range_type? irange_type? || erange_type? end
def reference?
def reference? REFERENCES.include?(type) end
def shorthand_asgn?
def shorthand_asgn? SHORTHAND_ASSIGNMENTS.include?(type) end
def sibling_index
-
(Integer)- the index of the receiver node in its siblings
def sibling_index parent&.children&.index { |sibling| sibling.equal?(self) } end
def single_line?
def single_line? line_count == 1 end
def source
def source loc.expression.source end
def source_length
def source_length source_range ? source_range.size : 0 end
def source_range
def source_range loc.expression end
def special_keyword?
def special_keyword? SPECIAL_KEYWORDS.include?(source) end
def truthy_literal?
def truthy_literal? TRUTHY_LITERALS.include?(type) end
def updated(type = nil, children = nil, properties = {})
identical subtrees. Rather, the entire AST must be copied any time any
not just the other way around, we cannot update an AST and share
mutate our ASTs. Since we keep references from children to parents and
Override `AST::Node#updated` so that `AST::Processor` does not try to
def updated(type = nil, children = nil, properties = {}) properties[:location] ||= @location klass = RuboCop::AST::Builder::NODE_MAP[type || @type] || Node klass.new(type || @type, children || @children, properties) end
def value_used?
`(...; nil)`, might that affect anything?
So, does the return value of this node matter? If we changed it to
change the return value
means we can transform it in ways which preserve the side effects, but
If we know that an expression is useful only for its side effects, that
effects, and some for both
Some expressions are evaluated for their value, some for their side
def value_used? # Be conservative and return true if we're not sure. return false if parent.nil? case parent.type when :array, :defined?, :dstr, :dsym, :eflipflop, :erange, :float, :hash, :iflipflop, :irange, :not, :pair, :regexp, :str, :sym, :when, :xstr parent.value_used? when :begin, :kwbegin begin_value_used? when :for for_value_used? when :case, :if case_if_value_used? when :while, :until, :while_post, :until_post while_until_value_used? else true end end
def variable?
def variable? VARIABLES.include?(type) end
def visit_ancestors(types)
def visit_ancestors(types) last_node = self while (current_node = last_node.parent) yield current_node if types.empty? || types.include?(current_node.type) last_node = current_node end end
def visit_descendants(types, &block)
def visit_descendants(types, &block) each_child_node do |child| yield child if types.empty? || types.include?(child.type) child.visit_descendants(types, &block) end end
def while_until_value_used?
def while_until_value_used? # (while <condition> <body>) -> always evaluates to `nil` sibling_index.zero? end