Skip to content

Commit 6961042

Browse files
committed
Add Prism.node_for(Method|UnboundMethod|Proc) to get a Prism node for a callable
* See https://bugs.ruby-lang.org/issues/21005 and https://bugs.ruby-lang.org/issues/20999
1 parent f781416 commit 6961042

File tree

3 files changed

+60
-0
lines changed

3 files changed

+60
-0
lines changed

lib/prism/parse_result.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ def code_units_column(byte_offset, encoding)
143143
code_units_offset(byte_offset, encoding) - code_units_offset(line_start(byte_offset), encoding)
144144
end
145145

146+
# Returns the byte offset for a given line number and column number
147+
def line_and_column_to_byte_offset(line, column)
148+
line_start = offsets[line - 1]
149+
line_end = offsets[line]
150+
line_start + @source.byteslice(line_start, line_end)[0...column].bytesize
151+
end
152+
146153
# Freeze this object and the objects it contains.
147154
def deep_freeze
148155
source.freeze
@@ -272,6 +279,13 @@ def code_units_cache(encoding)
272279
def code_units_column(byte_offset, encoding)
273280
byte_offset - line_start(byte_offset)
274281
end
282+
283+
# Specialized version of `line_and_column_to_byte_offset`
284+
# which does not need to access the source String
285+
def line_and_column_to_byte_offset(line, column)
286+
line_start = offsets[line - 1]
287+
line_start + column
288+
end
275289
end
276290

277291
# This represents a location in the source.
@@ -895,4 +909,46 @@ def initialize(locals, forwarding)
895909
def self.scope(locals: [], forwarding: [])
896910
Scope.new(locals, forwarding)
897911
end
912+
913+
# Given a Method, UnboundMethod or Proc, use its #source_location to parse the file and return a Prism node representing it.
914+
# The returned node will either be a DefNode, LambdaNode or CallNode.
915+
# Raises ArgumentError if it cannot be found for any reason.
916+
# Only works on Ruby 4+ as it needs #source_location to contain column and end line information.
917+
def self.node_for(callable)
918+
unless callable.is_a?(Method) || callable.is_a?(UnboundMethod) || callable.is_a?(Proc)
919+
raise ArgumentError, 'Prism.node_for requires a Method, UnboundMethod or Proc'
920+
end
921+
source_location = callable.source_location
922+
raise ArgumentError, "#source_location is nil for #{callable}" if source_location.nil?
923+
raise ArgumentError, '#source_location does not contain column and end_line, this method only works on Ruby 4+' if source_location.size != 5
924+
file, start_line, start_column, end_line, end_column = source_location
925+
926+
unless File.exist?(file)
927+
raise ArgumentError, "#source_location[0] is #{file} but this file does not exist"
928+
end
929+
930+
parse_result = Prism.parse_file(file)
931+
unless parse_result.success?
932+
raise ArgumentError, "#{file} has syntax errors: #{parse_result.errors_format}"
933+
end
934+
root = parse_result.value
935+
start_offset = parse_result.source.line_and_column_to_byte_offset(start_line, start_column)
936+
end_offset = parse_result.source.line_and_column_to_byte_offset(end_line, end_column)
937+
938+
found = root.breadth_first_search do |node|
939+
case node
940+
when DefNode
941+
node.start_offset == start_offset && node.end_offset == end_offset
942+
when LambdaNode
943+
# Proc#source_location returns start_column 2 for `-> { ... }` (just after the `->`)
944+
node.operator_loc.end_offset == start_offset && node.end_offset == end_offset
945+
when CallNode
946+
# Proc#source_location returns start_column 5 for `proc { ... }` (the `{`)
947+
node.block && node.block.opening_loc.start_offset == start_offset && node.end_offset == end_offset
948+
end
949+
end
950+
951+
raise ArgumentError, "Could not find node for #{callable} in #{file} at (#{start_line},#{start_column})-(#{end_line},#{end_column})" unless found
952+
found
953+
end
898954
end

sig/prism/parse_result.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module Prism
2424
def code_units_offset: (Integer byte_offset, Encoding encoding) -> Integer
2525
def code_units_cache: (Encoding encoding) -> _CodeUnitsCache
2626
def code_units_column: (Integer byte_offset, Encoding encoding) -> Integer
27+
def line_and_column_to_byte_offset: (Integer line, Integer column) -> Integer
2728
def deep_freeze: () -> void
2829

2930
def self.for: (String source) -> Source
@@ -40,6 +41,7 @@ module Prism
4041
def code_units_offset: (Integer byte_offset, Encoding encoding) -> Integer
4142
def code_units_cache: (Encoding encoding) -> _CodeUnitsCache
4243
def code_units_column: (Integer byte_offset, Encoding encoding) -> Integer
44+
def line_and_column_to_byte_offset: (Integer line, Integer column) -> Integer
4345
end
4446

4547
class Location

templates/sig/prism.rbs.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,6 @@ module Prism
9090
) -> ParseResult
9191

9292
def self.scope: (?locals: Array[Symbol], ?forwarding: Array[Symbol]) -> Scope
93+
94+
def self.node_for: (callable: Method | UnboundMethod | Proc) -> DefNode | LambdaNode | CallNode | nil
9395
end

0 commit comments

Comments
 (0)