summaryrefslogtreecommitdiff
path: root/lib/irb/source_finder.rb
blob: 5d7d729d19a5935da48bb8ebdf7f6e7a5fd562fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# frozen_string_literal: true

require_relative "ruby-lex"

module IRB
  class SourceFinder
    class EvaluationError < StandardError; end

    class Source
      attr_reader :file, :line
      def initialize(file, line, ast_source = nil)
        @file = file
        @line = line
        @ast_source = ast_source
      end

      def file_exist?
        File.exist?(@file)
      end

      def binary_file?
        # If the line is zero, it means that the target's source is probably in a binary file.
        @line.zero?
      end

      def file_content
        @file_content ||= File.read(@file)
      end

      def colorized_content
        if !binary_file? && file_exist?
          end_line = find_end
          # To correctly colorize, we need to colorize full content and extract the relevant lines.
          colored = IRB::Color.colorize_code(file_content)
          colored.lines[@line - 1...end_line].join
        elsif @ast_source
          IRB::Color.colorize_code(@ast_source)
        end
      end

      private

      def find_end
        lex = RubyLex.new
        code = file_content
        lines = code.lines[(@line - 1)..-1]
        tokens = RubyLex.ripper_lex_without_warning(lines.join)
        prev_tokens = []

        # chunk with line number
        tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk|
          code = lines[0..lnum].join
          prev_tokens.concat chunk
          continue = lex.should_continue?(prev_tokens)
          syntax = lex.check_code_syntax(code, local_variables: [])
          if !continue && syntax == :valid
            return @line + lnum
          end
        end
        @line
      end
    end

    private_constant :Source

    def initialize(irb_context)
      @irb_context = irb_context
    end

    def find_source(signature, super_level = 0)
      case signature
      when /\A(::)?[A-Z]\w*(::[A-Z]\w*)*\z/ # ConstName, ::ConstName, ConstPath::ConstName
        eval_receiver_or_owner(signature) # trigger autoload
        *parts, name = signature.split('::', -1)
        base =
          if parts.empty? # ConstName
            find_const_owner(name)
          elsif parts == [''] # ::ConstName
            Object
          else # ConstPath::ConstName
            eval_receiver_or_owner(parts.join('::'))
          end
        file, line = base.const_source_location(name)
      when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method
        owner = eval_receiver_or_owner(Regexp.last_match[:owner])
        method = Regexp.last_match[:method]
        return unless owner.respond_to?(:instance_method)
        method = method_target(owner, super_level, method, "owner")
        file, line = method&.source_location
      when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
        receiver = eval_receiver_or_owner(Regexp.last_match[:receiver] || 'self')
        method = Regexp.last_match[:method]
        return unless receiver.respond_to?(method, true)
        method = method_target(receiver, super_level, method, "receiver")
        file, line = method&.source_location
      end
      return unless file && line

      if File.exist?(file)
        Source.new(file, line)
      elsif method
        # Method defined with eval, probably in IRB session
        source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
        Source.new(file, line, source)
      end
    rescue EvaluationError
      nil
    end

    private

    def method_target(owner_receiver, super_level, method, type)
      case type
      when "owner"
        target_method = owner_receiver.instance_method(method)
      when "receiver"
        target_method = owner_receiver.method(method)
      end
      super_level.times do |s|
        target_method = target_method.super_method if target_method
      end
      target_method
    rescue NameError
      nil
    end

    def eval_receiver_or_owner(code)
      context_binding = @irb_context.workspace.binding
      eval(code, context_binding)
    rescue NameError
      raise EvaluationError
    end

    def find_const_owner(name)
      module_nesting = @irb_context.workspace.binding.eval('::Module.nesting')
      module_nesting.find { |mod| mod.const_defined?(name, false) } || module_nesting.find { |mod| mod.const_defined?(name) } || Object
    end
  end
end