Skip to content

Commit

Permalink
Add some code samples
Browse files Browse the repository at this point in the history
  • Loading branch information
kddnewton committed Jun 6, 2024
1 parent 6a4fe21 commit f5c883a
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 89 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The repository contains the infrastructure for both a shared library (libprism)
├── rust
│   ├── ruby-prism Rustified crate for the shared library
│   └── ruby-prism-sys FFI binding for Rust
├── sample Sample code that uses the Ruby API for documentation
├── sig RBS type signatures for the Ruby library
├── src
│   ├── util various utility files
Expand Down
1 change: 1 addition & 0 deletions rakelib/check_manifest.rake
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ task check_manifest: :templates do
pkg
rakelib
rust
sample
sorbet
suppressions
templates
Expand Down
105 changes: 105 additions & 0 deletions sample/find_calls.rb
Original file line number Diff line number Diff line change
@@ -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
98 changes: 88 additions & 10 deletions sample/find_comments.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,100 @@
# This script finds all of the comments within a given source file.
# This script finds all of the comments within a given source file for a method.

require "prism"

Prism.parse_comments(DATA.read).each do |comment|
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=0 @length=42 start_line=1>>
# # This is documentation for the Foo class.
# #<Prism::InlineComment @location=#<Prism::Location @start_offset=55 @length=43 start_line=3>>
# # This is documentation for the bar method.
# #<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 documentation for the Foo class.
class Foo
# This is documentation for the bar method.
def bar
# 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
41 changes: 0 additions & 41 deletions sample/find_nodes.rb

This file was deleted.

91 changes: 53 additions & 38 deletions sample/locate_nodes.rb
Original file line number Diff line number Diff line change
@@ -1,63 +1,78 @@
# 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)
# Each node that we visit should be added to the result, so that we end up
# with an array of the nodes that we traversed.
result << node

# Iterate over each child node.
node.compact_child_nodes.each do |child_node|
child_location = child_node.location

start_line = child_location.start_line
end_line = child_location.end_line

# Here we determine if the given coordinates are contained within the
# child node's location.
if start_line == end_line
if line == start_line && column >= child_location.start_column && column < child_location.end_column
queue << child_node
break
end
elsif (line == start_line && column >= child_location.start_column) || (line == end_line && column < child_location.end_column)
queue << child_node
break
elsif line > start_line && line < end_line
queue << child_node
break
end
# 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

# Finally, we return the result.
result
end

result = Prism.parse_stream(DATA)
locate(result.value, line: 4, column: 14).each_with_index do |node, index|
location = node.location
puts "#{" " * index}#{node.type}@#{location.start_line}:#{location.start_column}-#{location.end_line}:#{location.end_column}"
print " " * index
print node.class.name.split("::", 2).last
print " "
puts PP.pp(node.location, +"")
end

# =>
# program_node@1:0-7:3
# statements_node@1:0-7:3
# module_node@1:0-7:3
# statements_node@2:2-6:5
# class_node@2:2-6:5
# statements_node@3:4-5:7
# def_node@3:4-5:7
# statements_node@4:6-4:21
# call_node@4:6-4:21
# call_node@4:6-4:15
# arguments_node@4:12-4:15
# integer_node@4:12-4:15
# 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
Expand Down
Loading

0 comments on commit f5c883a

Please sign in to comment.