@@ -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
898954end
0 commit comments