Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Search updates #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
/vendor/

test.rb
query.txt
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions lib/syntax_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,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
33 changes: 29 additions & 4 deletions lib/syntax_tree/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
172 changes: 172 additions & 0 deletions lib/syntax_tree/pattern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# 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
# 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)
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(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? { |(matcher, value)| matcher.call(value) }
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 = :""

->(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(root, +"").chomp
end
end
end
Loading