|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module SyntaxTree |
| 4 | + # Provides an interface for searching for a pattern of nodes against a |
| 5 | + # subtree of an AST. |
| 6 | + class Search |
| 7 | + class UncompilableError < StandardError |
| 8 | + end |
| 9 | + |
| 10 | + attr_reader :matcher |
| 11 | + |
| 12 | + def initialize(query) |
| 13 | + root = SyntaxTree.parse("case nil\nin #{query}\nend") |
| 14 | + @matcher = compile(root.statements.body.first.consequent.pattern) |
| 15 | + end |
| 16 | + |
| 17 | + def scan(root) |
| 18 | + return to_enum(__method__, root) unless block_given? |
| 19 | + queue = [root] |
| 20 | + |
| 21 | + until queue.empty? |
| 22 | + node = queue.shift |
| 23 | + next unless node |
| 24 | + |
| 25 | + yield node if matcher.call(node) |
| 26 | + queue += node.child_nodes |
| 27 | + end |
| 28 | + end |
| 29 | + |
| 30 | + private |
| 31 | + |
| 32 | + def compile(pattern) |
| 33 | + case pattern |
| 34 | + in Binary[left:, operator: :|, right:] |
| 35 | + compiled_left = compile(left) |
| 36 | + compiled_right = compile(right) |
| 37 | + |
| 38 | + ->(node) { compiled_left.call(node) || compiled_right.call(node) } |
| 39 | + in Const[value:] if SyntaxTree.const_defined?(value) |
| 40 | + clazz = SyntaxTree.const_get(value) |
| 41 | + |
| 42 | + ->(node) { node.is_a?(clazz) } |
| 43 | + in Const[value:] if Object.const_defined?(value) |
| 44 | + clazz = Object.const_get(value) |
| 45 | + |
| 46 | + ->(node) { node.is_a?(clazz) } |
| 47 | + in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]]] |
| 48 | + compile(pattern.constant) |
| 49 | + in HshPtn[constant:, keywords:, keyword_rest: nil] |
| 50 | + compiled_constant = compile(constant) |
| 51 | + |
| 52 | + preprocessed_keywords = |
| 53 | + keywords.to_h do |keyword, value| |
| 54 | + raise NoMatchingPatternError unless keyword.is_a?(Label) |
| 55 | + [keyword.value.chomp(":").to_sym, compile(value)] |
| 56 | + end |
| 57 | + |
| 58 | + compiled_keywords = ->(node) do |
| 59 | + deconstructed = node.deconstruct_keys(preprocessed_keywords.keys) |
| 60 | + preprocessed_keywords.all? do |keyword, matcher| |
| 61 | + matcher.call(deconstructed[keyword]) |
| 62 | + end |
| 63 | + end |
| 64 | + |
| 65 | + ->(node) do |
| 66 | + compiled_constant.call(node) && compiled_keywords.call(node) |
| 67 | + end |
| 68 | + in RegexpLiteral[parts: [TStringContent[value:]]] |
| 69 | + regexp = /#{value}/ |
| 70 | + |
| 71 | + ->(attribute) { regexp.match?(attribute) } |
| 72 | + in StringLiteral[parts: [TStringContent[value:]]] |
| 73 | + ->(attribute) { attribute == value } |
| 74 | + in VarRef[value: Const => value] |
| 75 | + compile(value) |
| 76 | + end |
| 77 | + rescue NoMatchingPatternError |
| 78 | + raise UncompilableError, <<~ERROR |
| 79 | + Syntax Tree was unable to compile the pattern you provided to search |
| 80 | + into a usable expression. It failed on the node within the pattern |
| 81 | + matching expression represented by: |
| 82 | +
|
| 83 | + #{PP.pp(pattern, +"").chomp} |
| 84 | +
|
| 85 | + Note that not all syntax supported by Ruby's pattern matching syntax is |
| 86 | + also supported by Syntax Tree's code search. If you're using some syntax |
| 87 | + that you believe should be supported, please open an issue on the GitHub |
| 88 | + repository at https://github.com/ruby-syntax-tree/syntax_tree. |
| 89 | + ERROR |
| 90 | + end |
| 91 | + end |
| 92 | +end |
0 commit comments