diff options
Diffstat (limited to 'sample')
-rw-r--r-- | sample/find_calls.rb | 105 | ||||
-rw-r--r-- | sample/find_comments.rb | 100 | ||||
-rw-r--r-- | sample/locate_nodes.rb | 84 | ||||
-rw-r--r-- | sample/visit_nodes.rb | 63 |
4 files changed, 352 insertions, 0 deletions
diff --git a/sample/find_calls.rb b/sample/find_calls.rb new file mode 100644 index 0000000000..30af56c719 --- /dev/null +++ b/sample/find_calls.rb @@ -0,0 +1,105 @@ +# This script finds calls to a specific method with a certain keyword parameter +# within a given source file. + +require "prism" +require "pp" + +# For deprecation or refactoring purposes, it's often useful to find all of the +# places that call a specific method with a specific k eyword parameter. This is +# easily accomplished with a visitor such as this one. +class QuxParameterVisitor < Prism::Visitor + def initialize(calls) + @calls = calls + end + + def visit_call_node(node) + @calls << node if qux?(node) + super + end + + private + + def qux?(node) + # All nodes implement pattern matching, so you can use the `in` operator to + # pull out all of their individual fields. As you can see by this extensive + # pattern match, this is quite a powerful feature. + node in { + # This checks that the receiver is the constant Qux or the constant path + # ::Qux. We are assuming relative constants are fine in this case. + receiver: ( + Prism::ConstantReadNode[name: :Qux] | + Prism::ConstantPathNode[parent: nil, name: :Qux] + ), + # This checks that the name of the method is qux. We purposefully are not + # checking the call operator (., ::, or &.) because we want all of them. + # In other ASTs, this would be multiple node types, but prism combines + # them all into one for convenience. + name: :qux, + arguments: Prism::ArgumentsNode[ + # Here we're going to use the "find" pattern to find the keyword hash + # node that has the correct key. + arguments: [ + *, + Prism::KeywordHashNode[ + # Here we'll use another "find" pattern to find the key that we are + # specifically looking for. + elements: [ + *, + # Finally, we can assert against the key itself. Note that we are + # not looking at the value of hash pair, because we are only + # specifically looking for a key. + Prism::AssocNode[key: Prism::SymbolNode[unescaped: "qux"]], + * + ] + ], + * + ] + ] + } + end +end + +calls = [] +Prism.parse_stream(DATA).value.accept(QuxParameterVisitor.new(calls)) + +calls.each do |call| + print "CallNode " + puts PP.pp(call.location, +"") + print " " + puts call.slice +end + +# => +# CallNode (5,6)-(5,29) +# Qux.qux(222, qux: true) +# CallNode (9,6)-(9,30) +# Qux&.qux(333, qux: true) +# CallNode (20,6)-(20,51) +# Qux::qux(888, qux: ::Qux.qux(999, qux: true)) +# CallNode (20,25)-(20,50) +# ::Qux.qux(999, qux: true) + +__END__ +module Foo + class Bar + def baz1 + Qux.qux(111) + Qux.qux(222, qux: true) + end + + def baz2 + Qux&.qux(333, qux: true) + Qux&.qux(444) + end + + def baz3 + qux(555, qux: false) + 666.qux(666) + end + + def baz4 + Qux::qux(777) + Qux::qux(888, qux: ::Qux.qux(999, qux: true)) + end + end +end diff --git a/sample/find_comments.rb b/sample/find_comments.rb new file mode 100644 index 0000000000..6a26cd32b7 --- /dev/null +++ b/sample/find_comments.rb @@ -0,0 +1,100 @@ +# This script finds all of the comments within a given source file for a method. + +require "prism" + +class FindMethodComments < Prism::Visitor + def initialize(target, comments, nesting = []) + @target = target + @comments = comments + @nesting = nesting + end + + # These visit methods are specific to each class. Defining a visitor allows + # you to group functionality that applies to all node types into a single + # class. You can find which method corresponds to which node type by looking + # at the class name, calling #type on the node, or by looking at the #accept + # method definition on the node. + def visit_module_node(node) + visitor = FindMethodComments.new(@target, @comments, [*@nesting, node.name]) + node.compact_child_nodes.each { |child| child.accept(visitor) } + end + + def visit_class_node(node) + # We could keep track of an internal state where we push the class name here + # and then pop it after the visit is complete. However, it is often simpler + # and cleaner to generate a new visitor instance when the state changes, + # because then the state is immutable and it's easier to reason about. This + # also provides for more debugging opportunity in the initializer. + visitor = FindMethodComments.new(@target, @comments, [*@nesting, node.name]) + node.compact_child_nodes.each { |child| child.accept(visitor) } + end + + def visit_def_node(node) + if [*@nesting, node.name] == @target + # Comments are always attached to locations (either inner locations on a + # node like the location of a keyword or the location on the node itself). + # Nodes are considered either "leading" or "trailing", which means that + # they occur before or after the location, respectively. In this case of + # documentation, we only want to consider leading comments. You can also + # fetch all of the comments on a location with #comments. + @comments.concat(node.location.leading_comments) + else + super + end + end +end + +# Most of the time, the concept of "finding" something in the AST can be +# accomplished either with a queue or with a visitor. In this case we will use a +# visitor, but a queue would work just as well. +def find_comments(result, path) + target = path.split(/::|#/).map(&:to_sym) + comments = [] + + result.value.accept(FindMethodComments.new(target, comments)) + comments +end + +result = Prism.parse_stream(DATA) +result.attach_comments! + +find_comments(result, "Foo#foo").each do |comment| + puts comment.inspect + puts comment.slice +end + +# => +# #<Prism::InlineComment @location=#<Prism::Location @start_offset=205 @length=27 start_line=13>> +# # This is the documentation +# #<Prism::InlineComment @location=#<Prism::Location @start_offset=235 @length=21 start_line=14>> +# # for the foo method. + +find_comments(result, "Foo::Bar#bar").each do |comment| + puts comment.inspect + puts comment.slice +end + +# => +# #<Prism::InlineComment @location=#<Prism::Location @start_offset=126 @length=23 start_line=7>> +# # This is documentation +# #<Prism::InlineComment @location=#<Prism::Location @start_offset=154 @length=21 start_line=8>> +# # for the bar method. + +__END__ +# This is the documentation +# for the Foo module. +module Foo + # This is documentation + # for the Bar class. + class Bar + # This is documentation + # for the bar method. + def bar + end + end + + # This is the documentation + # for the foo method. + def foo + end +end diff --git a/sample/locate_nodes.rb b/sample/locate_nodes.rb new file mode 100644 index 0000000000..7a51db4367 --- /dev/null +++ b/sample/locate_nodes.rb @@ -0,0 +1,84 @@ +# This script locates a set of nodes determined by a line and column (in bytes). + +require "prism" +require "pp" + +# This method determines if the given location covers the given line and column. +# It's important to note that columns (and offsets) in prism are always in +# bytes. This is because prism supports all 90 source encodings that Ruby +# supports. You can always retrieve the column (or offset) of a location in +# other units with other provided APIs, like #start_character_column or +# #start_code_units_column. +def covers?(location, line:, column:) + start_line = location.start_line + end_line = location.end_line + + if start_line == end_line + # If the location only spans one line, then we only check if the line + # matches and that the column is covered by the column range. + line == start_line && (location.start_column...location.end_column).cover?(column) + else + # Otherwise, we check that it is on the start line and the column is greater + # than or equal to the start column, or that it is on the end line and the + # column is less than the end column, or that it is between the start and + # end lines. + (line == start_line && column >= location.start_column) || + (line == end_line && column < location.end_column) || + (line > start_line && line < end_line) + end +end + +# This method descends down into the AST whose root is `node` and returns the +# array of all of the nodes that cover the given line and column. +def locate(node, line:, column:) + queue = [node] + result = [] + + # We could use a recursive method here instead if we wanted, but it's + # important to note that that will not work for ASTs that are nested deeply + # enough to cause a stack overflow. + while (node = queue.shift) + result << node + + # Nodes have `child_nodes` and `compact_child_nodes`. `child_nodes` have + # consistent indices but include `nil` for optional fields that are not + # present, whereas `compact_child_nodes` has inconsistent indices but does + # not include `nil` for optional fields that are not present. + node.compact_child_nodes.find do |child| + queue << child if covers?(child.location, line: line, column: column) + end + end + + result +end + +result = Prism.parse_stream(DATA) +locate(result.value, line: 4, column: 14).each_with_index do |node, index| + print " " * index + print node.class.name.split("::", 2).last + print " " + puts PP.pp(node.location, +"") +end + +# => +# ProgramNode (1,0)-(7,3) +# StatementsNode (1,0)-(7,3) +# ModuleNode (1,0)-(7,3) +# StatementsNode (2,2)-(6,5) +# ClassNode (2,2)-(6,5) +# StatementsNode (3,4)-(5,7) +# DefNode (3,4)-(5,7) +# StatementsNode (4,6)-(4,21) +# CallNode (4,6)-(4,21) +# CallNode (4,6)-(4,15) +# ArgumentsNode (4,12)-(4,15) +# IntegerNode (4,12)-(4,15) + +__END__ +module Foo + class Bar + def baz + 111 + 222 + 333 + end + end +end diff --git a/sample/visit_nodes.rb b/sample/visit_nodes.rb new file mode 100644 index 0000000000..5ba703b0a3 --- /dev/null +++ b/sample/visit_nodes.rb @@ -0,0 +1,63 @@ +# This script visits all of the nodes of a specific type within a given source +# file. It uses the visitor class to traverse the AST. + +require "prism" +require "pp" + +class CaseInsensitiveRegularExpressionVisitor < Prism::Visitor + def initialize(regexps) + @regexps = regexps + end + + # As the visitor is walking the tree, this method will only be called when it + # encounters a regular expression node. We can then call any regular + # expression -specific APIs. In this case, we are only interested in the + # regular expressions that are case-insensitive, which we can retrieve with + # the #ignore_case? method. + def visit_regular_expression_node(node) + @regexps << node if node.ignore_case? + super + end + + def visit_interpolated_regular_expression_node(node) + @regexps << node if node.ignore_case? + + # The default behavior of the visitor is to continue visiting the children + # of the node. Because Ruby is so dynamic, it's actually possible for + # another regular expression to be interpolated in statements contained + # within the #{} contained in this interpolated regular expression node. By + # calling `super`, we ensure the visitor will continue. Failing to call + # `super` will cause the visitor to stop the traversal of the tree, which + # can also be useful in some cases. + super + end +end + +result = Prism.parse_stream(DATA) +regexps = [] + +result.value.accept(CaseInsensitiveRegularExpressionVisitor.new(regexps)) +regexps.each do |node| + print node.class.name.split("::", 2).last + print " " + puts PP.pp(node.location, +"") + + if node.is_a?(Prism::RegularExpressionNode) + print " " + p node.unescaped + end +end + +# => +# InterpolatedRegularExpressionNode (3,9)-(3,47) +# RegularExpressionNode (3,16)-(3,22) +# "bar" +# RegularExpressionNode (4,9)-(4,15) +# "bar" + +__END__ +class Foo + REG1 = /foo/ + REG2 = /foo #{/bar/i =~ "" ? "bar" : "baz"}/i + REG3 = /bar/i +end |