summaryrefslogtreecommitdiff
path: root/sample/prism
diff options
context:
space:
mode:
Diffstat (limited to 'sample/prism')
-rw-r--r--sample/prism/find_calls.rb105
-rw-r--r--sample/prism/find_comments.rb100
-rw-r--r--sample/prism/locate_nodes.rb84
-rw-r--r--sample/prism/make_tags.rb302
-rw-r--r--sample/prism/multiplex_constants.rb138
-rw-r--r--sample/prism/relocate_constants.rb43
-rw-r--r--sample/prism/visit_nodes.rb63
7 files changed, 835 insertions, 0 deletions
diff --git a/sample/prism/find_calls.rb b/sample/prism/find_calls.rb
new file mode 100644
index 0000000000..30af56c719
--- /dev/null
+++ b/sample/prism/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/prism/find_comments.rb b/sample/prism/find_comments.rb
new file mode 100644
index 0000000000..6a26cd32b7
--- /dev/null
+++ b/sample/prism/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/prism/locate_nodes.rb b/sample/prism/locate_nodes.rb
new file mode 100644
index 0000000000..7a51db4367
--- /dev/null
+++ b/sample/prism/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/prism/make_tags.rb b/sample/prism/make_tags.rb
new file mode 100644
index 0000000000..dc770ab1b0
--- /dev/null
+++ b/sample/prism/make_tags.rb
@@ -0,0 +1,302 @@
+# This script generates a tags file using Prism to parse the Ruby files.
+
+require "prism"
+
+# This visitor is responsible for visiting the nodes in the AST and generating
+# the appropriate tags. The tags are stored in the entries array as strings.
+class TagsVisitor < Prism::Visitor
+ # This represents an entry in the tags file, which is a tab-separated line. It
+ # houses the logic for how an entry is constructed.
+ class Entry
+ attr_reader :parts
+
+ def initialize(name, filepath, pattern, type)
+ @parts = [name, filepath, pattern, type]
+ end
+
+ def attribute(key, value)
+ parts << "#{key}:#{value}"
+ end
+
+ def attribute_class(nesting, names)
+ return if nesting.empty? && names.length == 1
+ attribute("class", [*nesting, names].flatten.tap(&:pop).join("."))
+ end
+
+ def attribute_inherits(names)
+ attribute("inherits", names.join(".")) if names
+ end
+
+ def to_line
+ parts.join("\t")
+ end
+ end
+
+ private_constant :Entry
+
+ attr_reader :entries, :filepath, :lines, :nesting, :singleton
+
+ # Initialize the visitor with the given parameters. The first three parameters
+ # are constant throughout the visit, while the last two are controlled by the
+ # visitor as it traverses the AST. These are treated as immutable by virtue of
+ # the visit methods constructing new visitors when they need to change.
+ def initialize(entries, filepath, lines, nesting = [], singleton = false)
+ @entries = entries
+ @filepath = filepath
+ @lines = lines
+ @nesting = nesting
+ @singleton = singleton
+ end
+
+ # Visit a method alias node and generate the appropriate tags.
+ #
+ # alias m2 m1
+ #
+ def visit_alias_method_node(node)
+ enter(node.new_name.unescaped.to_sym, node, "a") do |entry|
+ entry.attribute_class(nesting, [nil])
+ end
+
+ super
+ end
+
+ # Visit a method call to attr_reader, attr_writer, or attr_accessor without a
+ # receiver and generate the appropriate tags. Note that this ignores the fact
+ # that these methods could be overridden, which is a limitation of this
+ # script.
+ #
+ # attr_accessor :m1
+ #
+ def visit_call_node(node)
+ if !node.receiver && %i[attr_reader attr_writer attr_accessor].include?(name = node.name)
+ (node.arguments&.arguments || []).grep(Prism::SymbolNode).each do |argument|
+ if name != :attr_writer
+ enter(:"#{argument.unescaped}", argument, singleton ? "F" : "f") do |entry|
+ entry.attribute_class(nesting, [nil])
+ end
+ end
+
+ if name != :attr_reader
+ enter(:"#{argument.unescaped}=", argument, singleton ? "F" : "f") do |entry|
+ entry.attribute_class(nesting, [nil])
+ end
+ end
+ end
+ end
+
+ super
+ end
+
+ # Visit a class node and generate the appropriate tags.
+ #
+ # class C1
+ # end
+ #
+ def visit_class_node(node)
+ if (names = names_for(node.constant_path))
+ enter(names.last, node, "c") do |entry|
+ entry.attribute_class(nesting, names)
+ entry.attribute_inherits(names_for(node.superclass))
+ end
+
+ node.body&.accept(copy_visitor([*nesting, names], singleton))
+ end
+ end
+
+ # Visit a constant path write node and generate the appropriate tags.
+ #
+ # C1::C2 = 1
+ #
+ def visit_constant_path_write_node(node)
+ if (names = names_for(node.target))
+ enter(names.last, node, "C") do |entry|
+ entry.attribute_class(nesting, names)
+ end
+ end
+
+ super
+ end
+
+ # Visit a constant write node and generate the appropriate tags.
+ #
+ # C1 = 1
+ #
+ def visit_constant_write_node(node)
+ enter(node.name, node, "C") do |entry|
+ entry.attribute_class(nesting, [nil])
+ end
+
+ super
+ end
+
+ # Visit a method definition node and generate the appropriate tags.
+ #
+ # def m1; end
+ #
+ def visit_def_node(node)
+ enter(node.name, node, (node.receiver || singleton) ? "F" : "f") do |entry|
+ entry.attribute_class(nesting, [nil])
+ end
+
+ super
+ end
+
+ # Visit a module node and generate the appropriate tags.
+ #
+ # module M1
+ # end
+ #
+ def visit_module_node(node)
+ if (names = names_for(node.constant_path))
+ enter(names.last, node, "m") do |entry|
+ entry.attribute_class(nesting, names)
+ end
+
+ node.body&.accept(copy_visitor([*nesting, names], singleton))
+ end
+ end
+
+ # Visit a singleton class node and generate the appropriate tags.
+ #
+ # class << self
+ # end
+ #
+ def visit_singleton_class_node(node)
+ case node.expression
+ when Prism::SelfNode
+ node.body&.accept(copy_visitor(nesting, true))
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
+ if (names = names_for(node.expression))
+ node.body&.accept(copy_visitor([*nesting, names], true))
+ end
+ else
+ node.body&.accept(copy_visitor([*nesting, nil], true))
+ end
+ end
+
+ private
+
+ # Generate a new visitor with the given dynamic options. The static options
+ # are copied over automatically.
+ def copy_visitor(nesting, singleton)
+ TagsVisitor.new(entries, filepath, lines, nesting, singleton)
+ end
+
+ # Generate a new entry for the given name, node, and type and add it into the
+ # list of entries. The block is used to add additional attributes to the
+ # entry.
+ def enter(name, node, type)
+ line = lines[node.location.start_line - 1].chomp
+ pattern = "/^#{line.gsub("\\", "\\\\\\\\").gsub("/", "\\/")}$/;\""
+
+ entry = Entry.new(name, filepath, pattern, type)
+ yield entry
+
+ entries << entry.to_line
+ end
+
+ # Retrieve the names for the given node. This is used to construct the class
+ # attribute for the tags.
+ def names_for(node)
+ case node
+ when Prism::ConstantPathNode
+ names = names_for(node.parent)
+ return unless names
+
+ names << node.name
+ when Prism::ConstantReadNode
+ [node.name]
+ when Prism::SelfNode
+ [:self]
+ else
+ # dynamic
+ end
+ end
+end
+
+# Parse the Ruby file and visit all of the nodes in the resulting AST. Once all
+# of the nodes have been visited, the entries array should be populated with the
+# tags.
+result = Prism.parse_stream(DATA)
+result.value.accept(TagsVisitor.new(entries = [], __FILE__, result.source.lines))
+
+# Print the tags to STDOUT.
+puts "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/"
+puts "!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/"
+puts entries.sort
+
+# =>
+# !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
+# !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
+# C1 sample/prism/make_tags.rb /^ class C1$/;" c class:M1.M2
+# C2 sample/prism/make_tags.rb /^ class C2 < Object$/;" c class:M1.M2.C1 inherits:Object
+# C6 sample/prism/make_tags.rb /^ C6 = 1$/;" C class:M1
+# C7 sample/prism/make_tags.rb /^ C7 = 2$/;" C class:M1
+# C9 sample/prism/make_tags.rb /^ C8::C9 = 3$/;" C class:M1.C8
+# M1 sample/prism/make_tags.rb /^module M1$/;" m
+# M2 sample/prism/make_tags.rb /^ module M2$/;" m class:M1
+# M4 sample/prism/make_tags.rb /^ module M3::M4$/;" m class:M1.M3
+# M5 sample/prism/make_tags.rb /^ module self::M5$/;" m class:M1.self
+# m1 sample/prism/make_tags.rb /^ def m1; end$/;" f class:M1.M2.C1.C2
+# m10 sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4
+# m10= sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4
+# m11 sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4
+# m11= sample/prism/make_tags.rb /^ attr_accessor :m10, :m11$/;" f class:M1.M3.M4
+# m12 sample/prism/make_tags.rb /^ attr_reader :m12, :m13, :m14$/;" f class:M1.M3.M4
+# m13 sample/prism/make_tags.rb /^ attr_reader :m12, :m13, :m14$/;" f class:M1.M3.M4
+# m14 sample/prism/make_tags.rb /^ attr_reader :m12, :m13, :m14$/;" f class:M1.M3.M4
+# m15= sample/prism/make_tags.rb /^ attr_writer :m15$/;" f class:M1.M3.M4
+# m2 sample/prism/make_tags.rb /^ def m2; end$/;" f class:M1.M2.C1.C2
+# m3 sample/prism/make_tags.rb /^ alias m3 m1$/;" a class:M1.M2.C1.C2
+# m4 sample/prism/make_tags.rb /^ alias :m4 :m2$/;" a class:M1.M2.C1.C2
+# m5 sample/prism/make_tags.rb /^ def self.m5; end$/;" F class:M1.M2.C1.C2
+# m6 sample/prism/make_tags.rb /^ def m6; end$/;" F class:M1.M2.C1.C2
+# m7 sample/prism/make_tags.rb /^ def m7; end$/;" F class:M1.M2.C1.C2.C3
+# m8 sample/prism/make_tags.rb /^ def m8; end$/;" F class:M1.M2.C1.C2.C4.C5
+# m9 sample/prism/make_tags.rb /^ def m9; end$/;" F class:M1.M2.C1.C2.
+
+__END__
+module M1
+ module M2
+ class C1
+ class C2 < Object
+ def m1; end
+ def m2; end
+
+ alias m3 m1
+ alias :m4 :m2
+
+ def self.m5; end
+
+ class << self
+ def m6; end
+ end
+
+ class << C3
+ def m7; end
+ end
+
+ class << C4::C5
+ def m8; end
+ end
+
+ class << c
+ def m9; end
+ end
+ end
+ end
+ end
+
+ module M3::M4
+ attr_accessor :m10, :m11
+ attr_reader :m12, :m13, :m14
+ attr_writer :m15
+ end
+
+ module self::M5
+ end
+
+ C6 = 1
+ C7 = 2
+ C8::C9 = 3
+end
diff --git a/sample/prism/multiplex_constants.rb b/sample/prism/multiplex_constants.rb
new file mode 100644
index 0000000000..e39f2c36f6
--- /dev/null
+++ b/sample/prism/multiplex_constants.rb
@@ -0,0 +1,138 @@
+# This script indexes the classes and modules within a set of files using the
+# saved source functionality.
+
+require "prism"
+require "etc"
+require "tempfile"
+
+module Indexer
+ # A class that implements the #enter functionality so that it can be passed to
+ # the various save* APIs. This effectively bundles up all of the node_id and
+ # field_name pairs so that they can be written back to the parent process.
+ class Repository
+ attr_reader :scope, :entries
+
+ def initialize
+ @scope = []
+ @entries = []
+ end
+
+ def with(next_scope)
+ previous_scope = scope
+ @scope = scope + next_scope
+ yield
+ @scope = previous_scope
+ end
+
+ def empty?
+ entries.empty?
+ end
+
+ def enter(node_id, field_name)
+ entries << [scope.join("::"), node_id, field_name]
+ end
+ end
+
+ # Visit the classes and modules in the AST and save their locations into the
+ # repository.
+ class Visitor < Prism::Visitor
+ attr_reader :repository
+
+ def initialize(repository)
+ @repository = repository
+ end
+
+ def visit_class_node(node)
+ repository.with(node.constant_path.full_name_parts) do
+ node.constant_path.save_location(repository)
+ visit(node.body)
+ end
+ end
+
+ def visit_module_node(node)
+ repository.with(node.constant_path.full_name_parts) do
+ node.constant_path.save_location(repository)
+ visit(node.body)
+ end
+ end
+ end
+
+ # Index the classes and modules within a file. If there are any entries,
+ # return them as a serialized string to the parent process.
+ def self.index(filepath)
+ repository = Repository.new
+ Prism.parse_file(filepath).value.accept(Visitor.new(repository))
+ "#{filepath}|#{repository.entries.join("|")}" unless repository.empty?
+ end
+end
+
+def index_glob(glob, count = Etc.nprocessors - 1)
+ process_ids = []
+ filepath_writers = []
+ index_reader, index_writer = IO.pipe
+
+ # For each number in count, fork off a worker that has access to two pipes.
+ # The first pipe is the index_writer, to which it writes all of the results of
+ # indexing the various files. The second pipe is the filepath_reader, from
+ # which it reads the filepaths that it needs to index.
+ count.times do
+ filepath_reader, filepath_writer = IO.pipe
+
+ process_ids << fork do
+ filepath_writer.close
+ index_reader.close
+
+ while (filepath = filepath_reader.gets(chomp: true))
+ results = Indexer.index(filepath)
+ index_writer.puts(results) if results
+ end
+ end
+
+ filepath_reader.close
+ filepath_writers << filepath_writer
+ end
+
+ index_writer.close
+
+ # In a separate thread, write all of the filepaths to the various worker
+ # processes. This is done in a separate threads since puts will eventually
+ # block when each of the pipe buffers fills up. We write in a round-robin
+ # fashion to the various workers. This could be improved using a work-stealing
+ # algorithm, but is fine if you don't end up having a ton of variety in the
+ # size of your files.
+ writer_thread =
+ Thread.new do
+ Dir[glob].each_with_index do |filepath, index|
+ filepath_writers[index % count].puts(filepath)
+ end
+ end
+
+ index = Hash.new { |hash, key| hash[key] = [] }
+
+ # In a separate thread, read all of the results from the various worker
+ # processes and store them in the index. This is done in a separate thread so
+ # that reads and writes can be interleaved. This is important so that the
+ # index pipe doesn't fill up and block the writer.
+ reader_thread =
+ Thread.new do
+ while (line = index_reader.gets(chomp: true))
+ filepath, *entries = line.split("|")
+ repository = Prism::Relocation.filepath(filepath).filepath.lines.code_unit_columns(Encoding::UTF_16LE).leading_comments
+
+ entries.each_slice(3) do |(name, node_id, field_name)|
+ index[name] << repository.enter(Integer(node_id), field_name.to_sym)
+ end
+ end
+ end
+
+ writer_thread.join
+ filepath_writers.each(&:close)
+
+ reader_thread.join
+ index_reader.close
+
+ process_ids.each { |process_id| Process.wait(process_id) }
+ index
+end
+
+index_glob(File.expand_path("../../lib/**/*.rb", __dir__))
diff --git a/sample/prism/relocate_constants.rb b/sample/prism/relocate_constants.rb
new file mode 100644
index 0000000000..faa48f6388
--- /dev/null
+++ b/sample/prism/relocate_constants.rb
@@ -0,0 +1,43 @@
+# This script finds the declaration of all classes and modules and stores them
+# in a hash for an in-memory database of constants.
+
+require "prism"
+
+class RelocationVisitor < Prism::Visitor
+ attr_reader :index, :repository, :scope
+
+ def initialize(index, repository, scope = [])
+ @index = index
+ @repository = repository
+ @scope = scope
+ end
+
+ def visit_class_node(node)
+ next_scope = scope + node.constant_path.full_name_parts
+ index[next_scope.join("::")] << node.constant_path.save(repository)
+ node.body&.accept(RelocationVisitor.new(index, repository, next_scope))
+ end
+
+ def visit_module_node(node)
+ next_scope = scope + node.constant_path.full_name_parts
+ index[next_scope.join("::")] << node.constant_path.save(repository)
+ node.body&.accept(RelocationVisitor.new(index, repository, next_scope))
+ end
+end
+
+# Create an index that will store a mapping between the names of constants to a
+# list of the locations where they are declared or re-opened.
+index = Hash.new { |hash, key| hash[key] = [] }
+
+# Loop through every file in the lib directory of this repository and parse them
+# with Prism. Then visit them using the RelocateVisitor to store their
+# repository entries in the index.
+Dir[File.expand_path("../../lib/**/*.rb", __dir__)].each do |filepath|
+ repository = Prism::Relocation.filepath(filepath).filepath.lines.code_unit_columns(Encoding::UTF_16LE)
+ Prism.parse_file(filepath).value.accept(RelocationVisitor.new(index, repository))
+end
+
+puts index["Prism::ParametersNode"].map { |entry| "#{entry.filepath}:#{entry.start_line}:#{entry.start_code_units_column}" }
+# =>
+# prism/lib/prism/node.rb:13889:8
+# prism/lib/prism/node_ext.rb:267:8
diff --git a/sample/prism/visit_nodes.rb b/sample/prism/visit_nodes.rb
new file mode 100644
index 0000000000..5ba703b0a3
--- /dev/null
+++ b/sample/prism/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