1 2 3 4 5 6 7 8 9 10 11 12 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
|
# 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
|