# frozen_string_literal: true require 'delegate' module RuboCop module AST # This class performs a pattern-matching operation on an AST node. # # Detailed syntax: /docs/modules/ROOT/pages/node_pattern.adoc # # Initialize a new `NodePattern` with `NodePattern.new(pattern_string)`, then # pass an AST node to `NodePattern#match`. Alternatively, use one of the class # macros in `NodePattern::Macros` to define your own pattern-matching method. # # If the match fails, `nil` will be returned. If the match succeeds, the # return value depends on whether a block was provided to `#match`, and # whether the pattern contained any "captures" (values which are extracted # from a matching AST.) # # - With block: #match yields the captures (if any) and passes the return # value of the block through. # - With no block, but one capture: the capture is returned. # - With no block, but multiple captures: captures are returned as an array. # - With no block and no captures: #match returns `true`. # class NodePattern # Helpers for defining methods based on a pattern string module Macros # Define a method which applies a pattern to an AST node # # The new method will return nil if the node does not match. # If the node matches, and a block is provided, the new method will # yield to the block (passing any captures as block arguments). # If the node matches, and no block is provided, the new method will # return the captures, or `true` if there were none. def def_node_matcher(method_name, pattern_str, **keyword_defaults) NodePattern.new(pattern_str).def_node_matcher(self, method_name, **keyword_defaults) end # Define a method which recurses over the descendants of an AST node, # checking whether any of them match the provided pattern # # If the method name ends with '?', the new method will return `true` # as soon as it finds a descendant which matches. Otherwise, it will # yield all descendants which match. def def_node_search(method_name, pattern_str, **keyword_defaults) NodePattern.new(pattern_str).def_node_search(self, method_name, **keyword_defaults) end end extend Forwardable include MethodDefiner Invalid = Class.new(StandardError) VAR = 'node' attr_reader :pattern, :ast, :match_code def_delegators :@compiler, :captures, :named_parameters, :positional_parameters def initialize(str, compiler: Compiler.new) @pattern = str @ast = compiler.parser.parse(str) @compiler = compiler @match_code = @compiler.compile_as_node_pattern(@ast, var: VAR) @cache = {} end def match(*args, **rest, &block) @cache[:lambda] ||= as_lambda @cache[:lambda].call(*args, block: block, **rest) end def ==(other) other.is_a?(NodePattern) && other.ast == ast end alias eql? == def to_s "#<#{self.class} #{pattern}>" end def marshal_load(pattern) #:nodoc: initialize pattern end def marshal_dump #:nodoc: pattern end def as_json(_options = nil) #:nodoc: pattern end def encode_with(coder) #:nodoc: coder['pattern'] = pattern end def init_with(coder) #:nodoc: initialize(coder['pattern']) end # Yields its argument and any descendants, depth-first. # def self.descend(element, &block) return to_enum(__method__, element) unless block yield element if element.is_a?(::RuboCop::AST::Node) element.children.each do |child| descend(child, &block) end end nil end def freeze @match_code.freeze @compiler.freeze super end end end end