|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module SyntaxTree |
| 4 | + # A pattern is an object that wraps a Ruby pattern matching expression. The |
| 5 | + # expression would normally be passed to an `in` clause within a `case` |
| 6 | + # expression or a rightward assignment expression. For example, in the |
| 7 | + # following snippet: |
| 8 | + # |
| 9 | + # case node |
| 10 | + # in Const[value: "SyntaxTree"] |
| 11 | + # end |
| 12 | + # |
| 13 | + # the pattern is the `Const[value: "SyntaxTree"]` expression. Within Syntax |
| 14 | + # Tree, every node generates these kinds of expressions using the |
| 15 | + # #construct_keys method. |
| 16 | + # |
| 17 | + # The pattern gets compiled into an object that responds to call by running |
| 18 | + # the #compile method. This method itself will run back through Syntax Tree to |
| 19 | + # parse the expression into a tree, then walk the tree to generate the |
| 20 | + # necessary callable objects. For example, if you wanted to compile the |
| 21 | + # expression above into a callable, you would: |
| 22 | + # |
| 23 | + # callable = SyntaxTree::Pattern.new("Const[value: 'SyntaxTree']").compile |
| 24 | + # callable.call(node) |
| 25 | + # |
| 26 | + # The callable object returned by #compile is guaranteed to respond to #call |
| 27 | + # with a single argument, which is the node to match against. It also is |
| 28 | + # guaranteed to respond to #===, which means it itself can be used in a `case` |
| 29 | + # expression, as in: |
| 30 | + # |
| 31 | + # case node |
| 32 | + # when callable |
| 33 | + # end |
| 34 | + # |
| 35 | + # If the query given to the initializer cannot be compiled into a valid |
| 36 | + # matcher (either because of a syntax error or because it is using syntax we |
| 37 | + # do not yet support) then a SyntaxTree::Pattern::CompilationError will be |
| 38 | + # raised. |
| 39 | + class Pattern |
| 40 | + # Raised when the query given to a pattern is either invalid Ruby syntax or |
| 41 | + # is using syntax that we don't yet support. |
| 42 | + class CompilationError < StandardError |
| 43 | + def initialize(repr) |
| 44 | + super(<<~ERROR) |
| 45 | + Syntax Tree was unable to compile the pattern you provided to search |
| 46 | + into a usable expression. It failed on to understand the node |
| 47 | + represented by: |
| 48 | +
|
| 49 | + #{repr} |
| 50 | +
|
| 51 | + Note that not all syntax supported by Ruby's pattern matching syntax |
| 52 | + is also supported by Syntax Tree's code search. If you're using some |
| 53 | + syntax that you believe should be supported, please open an issue on |
| 54 | + GitHub at https://github.com/ruby-syntax-tree/syntax_tree/issues/new. |
| 55 | + ERROR |
| 56 | + end |
| 57 | + end |
| 58 | + |
| 59 | + attr_reader :query |
| 60 | + |
| 61 | + def initialize(query) |
| 62 | + @query = query |
| 63 | + end |
| 64 | + |
| 65 | + def compile |
| 66 | + program = |
| 67 | + begin |
| 68 | + SyntaxTree.parse("case nil\nin #{query}\nend") |
| 69 | + rescue Parser::ParseError |
| 70 | + raise CompilationError, query |
| 71 | + end |
| 72 | + |
| 73 | + compile_node(program.statements.body.first.consequent.pattern) |
| 74 | + end |
| 75 | + |
| 76 | + private |
| 77 | + |
| 78 | + def combine_and(left, right) |
| 79 | + ->(node) { left.call(node) && right.call(node) } |
| 80 | + end |
| 81 | + |
| 82 | + def combine_or(left, right) |
| 83 | + ->(node) { left.call(node) || right.call(node) } |
| 84 | + end |
| 85 | + |
| 86 | + def compile_node(root) |
| 87 | + case root |
| 88 | + in AryPtn[constant:, requireds:, rest: nil, posts: []] |
| 89 | + compiled_constant = compile_node(constant) if constant |
| 90 | + |
| 91 | + preprocessed = requireds.map { |required| compile_node(required) } |
| 92 | + |
| 93 | + compiled_requireds = ->(node) do |
| 94 | + deconstructed = node.deconstruct |
| 95 | + |
| 96 | + deconstructed.length == preprocessed.length && |
| 97 | + preprocessed |
| 98 | + .zip(deconstructed) |
| 99 | + .all? { |(matcher, value)| matcher.call(value) } |
| 100 | + end |
| 101 | + |
| 102 | + if compiled_constant |
| 103 | + combine_and(compiled_constant, compiled_requireds) |
| 104 | + else |
| 105 | + compiled_requireds |
| 106 | + end |
| 107 | + in Binary[left:, operator: :|, right:] |
| 108 | + combine_or(compile_node(left), compile_node(right)) |
| 109 | + in Const[value:] if SyntaxTree.const_defined?(value) |
| 110 | + clazz = SyntaxTree.const_get(value) |
| 111 | + |
| 112 | + ->(node) { node.is_a?(clazz) } |
| 113 | + in Const[value:] if Object.const_defined?(value) |
| 114 | + clazz = Object.const_get(value) |
| 115 | + |
| 116 | + ->(node) { node.is_a?(clazz) } |
| 117 | + in ConstPathRef[ |
| 118 | + parent: VarRef[value: Const[value: "SyntaxTree"]], constant: |
| 119 | + ] |
| 120 | + compile_node(constant) |
| 121 | + in DynaSymbol[parts: []] |
| 122 | + symbol = :"" |
| 123 | + |
| 124 | + ->(node) { node == symbol } |
| 125 | + in DynaSymbol[parts: [TStringContent[value:]]] |
| 126 | + symbol = value.to_sym |
| 127 | + |
| 128 | + ->(attribute) { attribute == value } |
| 129 | + in HshPtn[constant:, keywords:, keyword_rest: nil] |
| 130 | + compiled_constant = compile_node(constant) |
| 131 | + |
| 132 | + preprocessed = |
| 133 | + keywords.to_h do |keyword, value| |
| 134 | + raise NoMatchingPatternError unless keyword.is_a?(Label) |
| 135 | + [keyword.value.chomp(":").to_sym, compile_node(value)] |
| 136 | + end |
| 137 | + |
| 138 | + compiled_keywords = ->(node) do |
| 139 | + deconstructed = node.deconstruct_keys(preprocessed.keys) |
| 140 | + |
| 141 | + preprocessed.all? do |keyword, matcher| |
| 142 | + matcher.call(deconstructed[keyword]) |
| 143 | + end |
| 144 | + end |
| 145 | + |
| 146 | + if compiled_constant |
| 147 | + combine_and(compiled_constant, compiled_keywords) |
| 148 | + else |
| 149 | + compiled_keywords |
| 150 | + end |
| 151 | + in RegexpLiteral[parts: [TStringContent[value:]]] |
| 152 | + regexp = /#{value}/ |
| 153 | + |
| 154 | + ->(attribute) { regexp.match?(attribute) } |
| 155 | + in StringLiteral[parts: []] |
| 156 | + ->(attribute) { attribute == "" } |
| 157 | + in StringLiteral[parts: [TStringContent[value:]]] |
| 158 | + ->(attribute) { attribute == value } |
| 159 | + in SymbolLiteral[value:] |
| 160 | + symbol = value.value.to_sym |
| 161 | + |
| 162 | + ->(attribute) { attribute == symbol } |
| 163 | + in VarRef[value: Const => value] |
| 164 | + compile_node(value) |
| 165 | + in VarRef[value: Kw[value: "nil"]] |
| 166 | + ->(attribute) { attribute.nil? } |
| 167 | + end |
| 168 | + rescue NoMatchingPatternError |
| 169 | + raise CompilationError, PP.pp(root, +"").chomp |
| 170 | + end |
| 171 | + end |
| 172 | +end |
0 commit comments