From 78eea51cc15eabfc41ea719cb8ad7ef794a839e4 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 24 Oct 2022 17:00:15 -0400 Subject: [PATCH 1/5] Fix README syntax highlighting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c8c51445..9b1e681d 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ stree search VarRef path/to/file.rb For a file that contains `Foo + Bar` you will receive: -```ruby +``` path/to/file.rb:1:0: Foo + Bar path/to/file.rb:1:6: Foo + Bar ``` From 0c5ebad2511938bb346472adcf5c14c9a5fd6f9f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 25 Oct 2022 10:28:16 -0400 Subject: [PATCH 2/5] Support even more syntax --- .gitignore | 1 + lib/syntax_tree/search.rb | 57 ++++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 2838e82b..69755243 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /vendor/ test.rb +query.txt diff --git a/lib/syntax_tree/search.rb b/lib/syntax_tree/search.rb index 13378c4e..7326272b 100644 --- a/lib/syntax_tree/search.rb +++ b/lib/syntax_tree/search.rb @@ -29,13 +29,37 @@ def scan(root) private + def combine_and(left, right) + ->(node) { left.call(node) && right.call(node) } + end + + def combine_or(left, right) + ->(node) { left.call(node) || right.call(node) } + end + def compile(pattern) case pattern - in Binary[left:, operator: :|, right:] - compiled_left = compile(left) - compiled_right = compile(right) + in AryPtn[constant:, requireds:, rest: nil, posts: []] + compiled_constant = compile(constant) if constant + + preprocessed = requireds.map { |required| compile(required) } - ->(node) { compiled_left.call(node) || compiled_right.call(node) } + compiled_requireds = ->(node) do + deconstructed = node.deconstruct + + deconstructed.length == preprocessed.length && + preprocessed.zip(deconstructed).all? do |(matcher, value)| + matcher.call(value) + end + end + + if compiled_constant + combine_and(compiled_constant, compiled_requireds) + else + compiled_requireds + end + in Binary[left:, operator: :|, right:] + combine_or(compile(left), compile_right) in Const[value:] if SyntaxTree.const_defined?(value) clazz = SyntaxTree.const_get(value) @@ -46,33 +70,48 @@ def compile(pattern) ->(node) { node.is_a?(clazz) } in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]]] compile(pattern.constant) + in DynaSymbol[parts: [TStringContent[value:]]] + symbol = value.to_sym + + ->(attribute) { attribute == value } in HshPtn[constant:, keywords:, keyword_rest: nil] compiled_constant = compile(constant) - preprocessed_keywords = + preprocessed = keywords.to_h do |keyword, value| raise NoMatchingPatternError unless keyword.is_a?(Label) [keyword.value.chomp(":").to_sym, compile(value)] end compiled_keywords = ->(node) do - deconstructed = node.deconstruct_keys(preprocessed_keywords.keys) - preprocessed_keywords.all? do |keyword, matcher| + deconstructed = node.deconstruct_keys(preprocessed.keys) + + preprocessed.all? do |keyword, matcher| matcher.call(deconstructed[keyword]) end end - ->(node) do - compiled_constant.call(node) && compiled_keywords.call(node) + if compiled_constant + combine_and(compiled_constant, compiled_keywords) + else + compiled_keywords end in RegexpLiteral[parts: [TStringContent[value:]]] regexp = /#{value}/ ->(attribute) { regexp.match?(attribute) } + in StringLiteral[parts: []] + ->(attribute) { attribute == "" } in StringLiteral[parts: [TStringContent[value:]]] ->(attribute) { attribute == value } + in SymbolLiteral[value:] + symbol = value.value.to_sym + + ->(attribute) { attribute == symbol } in VarRef[value: Const => value] compile(value) + in VarRef[value: Kw[value: "nil"]] + ->(attribute) { attribute.nil? } end rescue NoMatchingPatternError raise UncompilableError, <<~ERROR From bfa8c399cce95fa72406d9f54e672950f336bfea Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 25 Oct 2022 10:58:21 -0400 Subject: [PATCH 3/5] Add an expr CLI command --- lib/syntax_tree.rb | 1 + lib/syntax_tree/cli.rb | 33 +++++++- lib/syntax_tree/pattern.rb | 168 +++++++++++++++++++++++++++++++++++++ lib/syntax_tree/search.rb | 113 +------------------------ test/cli_test.rb | 5 ++ test/search_test.rb | 57 ++++++++----- 6 files changed, 241 insertions(+), 136 deletions(-) create mode 100644 lib/syntax_tree/pattern.rb diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index eef142ff..3979a976 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -21,6 +21,7 @@ require_relative "syntax_tree/visitor/with_environment" require_relative "syntax_tree/parser" +require_relative "syntax_tree/pattern" require_relative "syntax_tree/search" # Syntax Tree is a suite of tools built on top of the internal CRuby parser. It diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index c5eae1bc..b847e059 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -188,6 +188,20 @@ def run(item) end end + # An action of the CLI that outputs a pattern-matching Ruby expression that + # would match the first expression of the input given. + class Expr < Action + def run(item) + case item.handler.parse(item.source) + in Program[statements: Statements[body: [expression]]] + puts expression.construct_keys + else + warn("The input to `stree expr` must be a single expression.") + exit(1) + end + end + end + # An action of the CLI that formats the input source and prints it out. class Format < Action def run(item) @@ -219,10 +233,15 @@ class Search < Action def initialize(query) query = File.read(query) if File.readable?(query) - @search = SyntaxTree::Search.new(query) - rescue SyntaxTree::Search::UncompilableError => error - warn(error.message) - exit(1) + pattern = + begin + Pattern.new(query).compile + rescue Pattern::CompilationError => error + warn(error.message) + exit(1) + end + + @search = SyntaxTree::Search.new(pattern) end def run(item) @@ -281,6 +300,10 @@ def run(item) #{Color.bold("stree doc [--plugins=...] [-e SCRIPT] FILE")} Print out the doc tree that would be used to format the given files + #{Color.bold("stree expr [-e SCRIPT] FILE")} + Print out a pattern-matching Ruby expression that would match the first + expression of the given files + #{Color.bold("stree format [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Print out the formatted version of the given files @@ -436,6 +459,8 @@ def run(argv) Debug.new(options) when "doc" Doc.new(options) + when "e", "expr" + Expr.new(options) when "f", "format" Format.new(options) when "help" diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb new file mode 100644 index 00000000..5ae78a2e --- /dev/null +++ b/lib/syntax_tree/pattern.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +module SyntaxTree + # A pattern is an object that wraps a Ruby pattern matching expression. The + # expression would normally be passed to an `in` clause within a `case` + # expression or a rightward assignment expression. For example, in the + # following snippet: + # + # case node + # in Const[value: "SyntaxTree"] + # end + # + # the pattern is the `Const[value: "SyntaxTree"]` expression. Within Syntax + # Tree, every node generates these kinds of expressions using the + # #construct_keys method. + # + # The pattern gets compiled into an object that responds to call by running + # the #compile method. This method itself will run back through Syntax Tree to + # parse the expression into a tree, then walk the tree to generate the + # necessary callable objects. For example, if you wanted to compile the + # expression above into a callable, you would: + # + # callable = SyntaxTree::Pattern.new("Const[value: 'SyntaxTree']").compile + # callable.call(node) + # + # The callable object returned by #compile is guaranteed to respond to #call + # with a single argument, which is the node to match against. It also is + # guaranteed to respond to #===, which means it itself can be used in a `case` + # expression, as in: + # + # case node + # when callable + # end + # + # If the query given to the initializer cannot be compiled into a valid + # matcher (either because of a syntax error or because it is using syntax we + # do not yet support) then a SyntaxTree::Pattern::CompilationError will be + # raised. + class Pattern + class CompilationError < StandardError + def initialize(repr) + super(<<~ERROR) + Syntax Tree was unable to compile the pattern you provided to search + into a usable expression. It failed on to understand the node + represented by: + + #{repr} + + Note that not all syntax supported by Ruby's pattern matching syntax + is also supported by Syntax Tree's code search. If you're using some + syntax that you believe should be supported, please open an issue on + GitHub at https://github.com/ruby-syntax-tree/syntax_tree/issues/new. + ERROR + end + end + + attr_reader :query + + def initialize(query) + @query = query + end + + def compile + program = + begin + SyntaxTree.parse("case nil\nin #{query}\nend") + rescue Parser::ParseError + raise CompilationError, query + end + + compile_node(program.statements.body.first.consequent.pattern) + end + + private + + def combine_and(left, right) + ->(node) { left.call(node) && right.call(node) } + end + + def combine_or(left, right) + ->(node) { left.call(node) || right.call(node) } + end + + def compile_node(node) + case node + in AryPtn[constant:, requireds:, rest: nil, posts: []] + compiled_constant = compile_node(constant) if constant + + preprocessed = requireds.map { |required| compile_node(required) } + + compiled_requireds = ->(node) do + deconstructed = node.deconstruct + + deconstructed.length == preprocessed.length && + preprocessed.zip(deconstructed).all? do |(matcher, value)| + matcher.call(value) + end + end + + if compiled_constant + combine_and(compiled_constant, compiled_requireds) + else + compiled_requireds + end + in Binary[left:, operator: :|, right:] + combine_or(compile_node(left), compile_node(right)) + in Const[value:] if SyntaxTree.const_defined?(value) + clazz = SyntaxTree.const_get(value) + + ->(node) { node.is_a?(clazz) } + in Const[value:] if Object.const_defined?(value) + clazz = Object.const_get(value) + + ->(node) { node.is_a?(clazz) } + in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]], constant:] + compile_node(constant) + in DynaSymbol[parts: []] + symbol = "".to_sym + + ->(node) { node == symbol } + in DynaSymbol[parts: [TStringContent[value:]]] + symbol = value.to_sym + + ->(attribute) { attribute == value } + in HshPtn[constant:, keywords:, keyword_rest: nil] + compiled_constant = compile_node(constant) + + preprocessed = + keywords.to_h do |keyword, value| + raise NoMatchingPatternError unless keyword.is_a?(Label) + [keyword.value.chomp(":").to_sym, compile_node(value)] + end + + compiled_keywords = ->(node) do + deconstructed = node.deconstruct_keys(preprocessed.keys) + + preprocessed.all? do |keyword, matcher| + matcher.call(deconstructed[keyword]) + end + end + + if compiled_constant + combine_and(compiled_constant, compiled_keywords) + else + compiled_keywords + end + in RegexpLiteral[parts: [TStringContent[value:]]] + regexp = /#{value}/ + + ->(attribute) { regexp.match?(attribute) } + in StringLiteral[parts: []] + ->(attribute) { attribute == "" } + in StringLiteral[parts: [TStringContent[value:]]] + ->(attribute) { attribute == value } + in SymbolLiteral[value:] + symbol = value.value.to_sym + + ->(attribute) { attribute == symbol } + in VarRef[value: Const => value] + compile_node(value) + in VarRef[value: Kw[value: "nil"]] + ->(attribute) { attribute.nil? } + end + rescue NoMatchingPatternError + raise CompilationError, PP.pp(node, +"").chomp + end + end +end diff --git a/lib/syntax_tree/search.rb b/lib/syntax_tree/search.rb index 7326272b..9fd52ba1 100644 --- a/lib/syntax_tree/search.rb +++ b/lib/syntax_tree/search.rb @@ -4,14 +4,10 @@ module SyntaxTree # Provides an interface for searching for a pattern of nodes against a # subtree of an AST. class Search - class UncompilableError < StandardError - end - - attr_reader :matcher + attr_reader :pattern - def initialize(query) - root = SyntaxTree.parse("case nil\nin #{query}\nend") - @matcher = compile(root.statements.body.first.consequent.pattern) + def initialize(pattern) + @pattern = pattern end def scan(root) @@ -22,110 +18,9 @@ def scan(root) node = queue.shift next unless node - yield node if matcher.call(node) + yield node if pattern.call(node) queue += node.child_nodes end end - - private - - def combine_and(left, right) - ->(node) { left.call(node) && right.call(node) } - end - - def combine_or(left, right) - ->(node) { left.call(node) || right.call(node) } - end - - def compile(pattern) - case pattern - in AryPtn[constant:, requireds:, rest: nil, posts: []] - compiled_constant = compile(constant) if constant - - preprocessed = requireds.map { |required| compile(required) } - - compiled_requireds = ->(node) do - deconstructed = node.deconstruct - - deconstructed.length == preprocessed.length && - preprocessed.zip(deconstructed).all? do |(matcher, value)| - matcher.call(value) - end - end - - if compiled_constant - combine_and(compiled_constant, compiled_requireds) - else - compiled_requireds - end - in Binary[left:, operator: :|, right:] - combine_or(compile(left), compile_right) - in Const[value:] if SyntaxTree.const_defined?(value) - clazz = SyntaxTree.const_get(value) - - ->(node) { node.is_a?(clazz) } - in Const[value:] if Object.const_defined?(value) - clazz = Object.const_get(value) - - ->(node) { node.is_a?(clazz) } - in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]]] - compile(pattern.constant) - in DynaSymbol[parts: [TStringContent[value:]]] - symbol = value.to_sym - - ->(attribute) { attribute == value } - in HshPtn[constant:, keywords:, keyword_rest: nil] - compiled_constant = compile(constant) - - preprocessed = - keywords.to_h do |keyword, value| - raise NoMatchingPatternError unless keyword.is_a?(Label) - [keyword.value.chomp(":").to_sym, compile(value)] - end - - compiled_keywords = ->(node) do - deconstructed = node.deconstruct_keys(preprocessed.keys) - - preprocessed.all? do |keyword, matcher| - matcher.call(deconstructed[keyword]) - end - end - - if compiled_constant - combine_and(compiled_constant, compiled_keywords) - else - compiled_keywords - end - in RegexpLiteral[parts: [TStringContent[value:]]] - regexp = /#{value}/ - - ->(attribute) { regexp.match?(attribute) } - in StringLiteral[parts: []] - ->(attribute) { attribute == "" } - in StringLiteral[parts: [TStringContent[value:]]] - ->(attribute) { attribute == value } - in SymbolLiteral[value:] - symbol = value.value.to_sym - - ->(attribute) { attribute == symbol } - in VarRef[value: Const => value] - compile(value) - in VarRef[value: Kw[value: "nil"]] - ->(attribute) { attribute.nil? } - end - rescue NoMatchingPatternError - raise UncompilableError, <<~ERROR - Syntax Tree was unable to compile the pattern you provided to search - into a usable expression. It failed on the node within the pattern - matching expression represented by: - - #{PP.pp(pattern, +"").chomp} - - Note that not all syntax supported by Ruby's pattern matching syntax is - also supported by Syntax Tree's code search. If you're using some syntax - that you believe should be supported, please open an issue on the GitHub - repository at https://github.com/ruby-syntax-tree/syntax_tree. - ERROR - end end end diff --git a/test/cli_test.rb b/test/cli_test.rb index 1a037918..b4ef0afc 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -79,6 +79,11 @@ def test_doc assert_includes(result.stdio, "test") end + def test_expr + result = run_cli("expr") + assert_includes(result.stdio, "SyntaxTree::Ident") + end + def test_format result = run_cli("format") assert_equal("test\n", result.stdio) diff --git a/test/search_test.rb b/test/search_test.rb index 6b030e99..314142e3 100644 --- a/test/search_test.rb +++ b/test/search_test.rb @@ -5,47 +5,58 @@ module SyntaxTree class SearchTest < Minitest::Test def test_search_binary_or - root = SyntaxTree.parse("Foo + Bar + 1") - scanned = Search.new("VarRef | Int").scan(root).to_a + results = search("Foo + Bar + 1", "VarRef | Int") - assert_equal 3, scanned.length - assert_equal "1", scanned.min_by { |node| node.class.name }.value + assert_equal 3, results.length + assert_equal "1", results.min_by { |node| node.class.name }.value end def test_search_const - root = SyntaxTree.parse("Foo + Bar + Baz") + results = search("Foo + Bar + Baz", "VarRef") - scanned = Search.new("VarRef").scan(root).to_a - - assert_equal 3, scanned.length - assert_equal %w[Bar Baz Foo], scanned.map { |node| node.value.value }.sort + assert_equal 3, results.length + assert_equal %w[Bar Baz Foo], results.map { |node| node.value.value }.sort end def test_search_syntax_tree_const - root = SyntaxTree.parse("Foo + Bar + Baz") - - scanned = Search.new("SyntaxTree::VarRef").scan(root).to_a + results = search("Foo + Bar + Baz", "SyntaxTree::VarRef") - assert_equal 3, scanned.length + assert_equal 3, results.length end def test_search_hash_pattern_string - root = SyntaxTree.parse("Foo + Bar + Baz") - - scanned = Search.new("VarRef[value: Const[value: 'Foo']]").scan(root).to_a + results = search("Foo + Bar + Baz", "VarRef[value: Const[value: 'Foo']]") - assert_equal 1, scanned.length - assert_equal "Foo", scanned.first.value.value + assert_equal 1, results.length + assert_equal "Foo", results.first.value.value end def test_search_hash_pattern_regexp - root = SyntaxTree.parse("Foo + Bar + Baz") + results = search("Foo + Bar + Baz", "VarRef[value: Const[value: /^Ba/]]") + + assert_equal 2, results.length + assert_equal %w[Bar Baz], results.map { |node| node.value.value }.sort + end + + def test_search_string_empty + results = search("''", "StringLiteral[parts: []]") + + assert_equal 1, results.length + end + + def test_search_symbol_empty + results = search(":''", "DynaSymbol[parts: []]") + + assert_equal 1, results.length + end + + private - query = "VarRef[value: Const[value: /^Ba/]]" - scanned = Search.new(query).scan(root).to_a + def search(source, query) + pattern = Pattern.new(query).compile + program = SyntaxTree.parse(source) - assert_equal 2, scanned.length - assert_equal %w[Bar Baz], scanned.map { |node| node.value.value }.sort + Search.new(pattern).scan(program).to_a end end end From 02b3c496e816e1b2a93e6af287131c4f50daa766 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 25 Oct 2022 12:38:55 -0400 Subject: [PATCH 4/5] Documentation for search --- README.md | 24 +++++++++++++++++++ lib/syntax_tree.rb | 6 +++++ lib/syntax_tree/pattern.rb | 48 +++++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 9b1e681d..368c9361 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ It is built with only standard library dependencies. It additionally ships with - [CLI](#cli) - [ast](#ast) - [check](#check) + - [expr](#expr) - [format](#format) - [json](#json) - [match](#match) @@ -26,6 +27,7 @@ It is built with only standard library dependencies. It additionally ships with - [SyntaxTree.read(filepath)](#syntaxtreereadfilepath) - [SyntaxTree.parse(source)](#syntaxtreeparsesource) - [SyntaxTree.format(source)](#syntaxtreeformatsource) + - [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block) - [Nodes](#nodes) - [child_nodes](#child_nodes) - [Pattern matching](#pattern-matching) @@ -129,6 +131,24 @@ To change the print width that you are checking against, specify the `--print-wi stree check --print-width=100 path/to/file.rb ``` +### expr + +This command will output a Ruby case-match expression that would match correctly against the first expression of the input. + +```sh +stree expr path/to/file.rb +``` + +For a file that contains `1 + 1`, you will receive: + +```ruby +SyntaxTree::Binary[ + left: SyntaxTree::Int[value: "1"], + operator: :+, + right: SyntaxTree::Int[value: "1"] +] +``` + ### format This command will output the formatted version of each of the listed files. Importantly, it will not write that content back to the source files. It is meant to display the formatted version only. @@ -312,6 +332,10 @@ This function takes an input string containing Ruby code and returns the syntax This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. You can optionally pass a second argument to this method as well that is the maximum width to print. It defaults to `80`. +### SyntaxTree.search(source, query, &block) + +This function takes an input string containing Ruby code, an input string containing a valid Ruby `in` clause expression that can be used to match against nodes in the tree (can be generated using `stree expr`, `stree match`, or `Node#construct_keys`), and a block. Each node that matches the given query will be yielded to the block. The block will receive the node as its only argument. + ## Nodes There are many different node types in the syntax tree. They are meant to be treated as immutable structs containing links to child nodes with minimal logic contained within their implementation. However, for the most part they all respond to a certain set of APIs, listed below. diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 3979a976..df2f43a9 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -75,4 +75,10 @@ def self.read(filepath) File.read(filepath, encoding: encoding) end + + # Searches through the given source using the given pattern and yields each + # node in the tree that matches the pattern to the given block. + def self.search(source, query, &block) + Search.new(Pattern.new(query).compile).scan(parse(source), &block) + end end diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index 5ae78a2e..aa558361 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -37,6 +37,8 @@ module SyntaxTree # do not yet support) then a SyntaxTree::Pattern::CompilationError will be # raised. class Pattern + # Raised when the query given to a pattern is either invalid Ruby syntax or + # is using syntax that we don't yet support. class CompilationError < StandardError def initialize(repr) super(<<~ERROR) @@ -76,27 +78,27 @@ def compile def combine_and(left, right) ->(node) { left.call(node) && right.call(node) } end - + def combine_or(left, right) ->(node) { left.call(node) || right.call(node) } end - def compile_node(node) - case node + def compile_node(root) + case root in AryPtn[constant:, requireds:, rest: nil, posts: []] compiled_constant = compile_node(constant) if constant - + preprocessed = requireds.map { |required| compile_node(required) } - + compiled_requireds = ->(node) do deconstructed = node.deconstruct - + deconstructed.length == preprocessed.length && - preprocessed.zip(deconstructed).all? do |(matcher, value)| - matcher.call(value) - end + preprocessed + .zip(deconstructed) + .all? { |(matcher, value)| matcher.call(value) } end - + if compiled_constant combine_and(compiled_constant, compiled_requireds) else @@ -106,39 +108,41 @@ def compile_node(node) combine_or(compile_node(left), compile_node(right)) in Const[value:] if SyntaxTree.const_defined?(value) clazz = SyntaxTree.const_get(value) - + ->(node) { node.is_a?(clazz) } in Const[value:] if Object.const_defined?(value) clazz = Object.const_get(value) - + ->(node) { node.is_a?(clazz) } - in ConstPathRef[parent: VarRef[value: Const[value: "SyntaxTree"]], constant:] + in ConstPathRef[ + parent: VarRef[value: Const[value: "SyntaxTree"]], constant: + ] compile_node(constant) in DynaSymbol[parts: []] - symbol = "".to_sym + symbol = :"" ->(node) { node == symbol } in DynaSymbol[parts: [TStringContent[value:]]] symbol = value.to_sym - + ->(attribute) { attribute == value } in HshPtn[constant:, keywords:, keyword_rest: nil] compiled_constant = compile_node(constant) - + preprocessed = keywords.to_h do |keyword, value| raise NoMatchingPatternError unless keyword.is_a?(Label) [keyword.value.chomp(":").to_sym, compile_node(value)] end - + compiled_keywords = ->(node) do deconstructed = node.deconstruct_keys(preprocessed.keys) - + preprocessed.all? do |keyword, matcher| matcher.call(deconstructed[keyword]) end end - + if compiled_constant combine_and(compiled_constant, compiled_keywords) else @@ -146,7 +150,7 @@ def compile_node(node) end in RegexpLiteral[parts: [TStringContent[value:]]] regexp = /#{value}/ - + ->(attribute) { regexp.match?(attribute) } in StringLiteral[parts: []] ->(attribute) { attribute == "" } @@ -154,7 +158,7 @@ def compile_node(node) ->(attribute) { attribute == value } in SymbolLiteral[value:] symbol = value.value.to_sym - + ->(attribute) { attribute == symbol } in VarRef[value: Const => value] compile_node(value) @@ -162,7 +166,7 @@ def compile_node(node) ->(attribute) { attribute.nil? } end rescue NoMatchingPatternError - raise CompilationError, PP.pp(node, +"").chomp + raise CompilationError, PP.pp(root, +"").chomp end end end From bd9a1c3d58026f84f57d8b012f9a7963e7de30e8 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 25 Oct 2022 13:23:30 -0400 Subject: [PATCH 5/5] Bump to v4.2.0 --- CHANGELOG.md | 14 +++++++++++++- Gemfile.lock | 2 +- lib/syntax_tree/version.rb | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a3fbca..bbaf044e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [4.2.0] - 2022-10-25 + +### Added + +- [#182](https://github.com/ruby-syntax-tree/syntax_tree/pull/182) - The new `stree expr` CLI command will function similarly to the `stree match` CLI command except that it only outputs the first expression of the program. +- [#182](https://github.com/ruby-syntax-tree/syntax_tree/pull/182) - Added the `SyntaxTree::Pattern` class for compiling `in` expressions into procs. + +### Changed + +- [#182](https://github.com/ruby-syntax-tree/syntax_tree/pull/182) - Much more syntax is now supported by the search command. + ## [4.1.0] - 2022-10-24 ### Added @@ -403,7 +414,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...HEAD +[4.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...v4.2.0 [4.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.2...v4.1.0 [4.0.2]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.1...v4.0.2 [4.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.0...v4.0.1 diff --git a/Gemfile.lock b/Gemfile.lock index 195e2226..339de160 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (4.1.0) + syntax_tree (4.2.0) prettier_print (>= 1.0.2) GEM diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index 36843ea9..0b68a850 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "4.1.0" + VERSION = "4.2.0" end