Module: Magic::Help

Defined in:
lib/magic/help.rb,
lib/magic/help/tracepoint.rb,
lib/magic/help/set_trace_func.rb

Class Method Summary collapse

Class Method Details

.help_method_extract(m) ⇒ Object



6
7
8
9
10
11
# File 'lib/magic/help.rb', line 6

def self.help_method_extract(m)
  unless m.inspect =~ %r[\A#<(?:Unbound)?Method: (.*?)>\Z]
    raise "Cannot parse result of #{m.class}#inspect: #{m.inspect}"
  end
  $1.sub(/\A.*?\((.*?)\)(.*)\Z/){ "#{$1}#{$2}" }.sub(/\./, "::").sub(/#<Class:(.*?)>#/) { "#{$1}::" }
end

.postprocess(m, res = nil) ⇒ Object

Magic::Help.postprocess is used to postprocess queries in two cases:

  • help “Foo.bar” queries - res defined, more hacks

  • help { Foo.bar } queries - res not defined, fewer hacks



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
# File 'lib/magic/help.rb', line 16

def self.postprocess(m, res=nil)
  # Kernel#method_missing here means class was found but method wasn't
  # It is possible that such method exists, it was simply not included.
  # Example - Time::rfc2822 from time.rb.
  #
  # Do not correct it if actual method_missing was called.
  if res and m == "Kernel#method_missing"
      m = res unless res =~ /\A(?:.*)(?:\#|::|\.)method_missing\Z/
  # Most classes do not override Foo::new, but provide new documentation anyway !
  # Two cases are possible
  # * Class#new is used
  # * Bar::new is used, for Bar being some ancestor of Foo
  elsif res and (m =~ /\A(.*)\#new\Z/ or m =~ /\A(.*)::new\Z/)
    cls = $1
    # Do not correct requests for Foo#new
    # If Foo#new became Class#new, it must have been
    # by some evil metaclass hackery.
    #
    # Foo.new or Foo::new both become Foo::new
    if res =~ /\A(.*)(::|\.)new\Z/
      cls_requested, k = $1, $2
      # Condition just to get "Class#new" working correctly
      # Otherwise it would be changed to "Class::new"
      m = "#{cls_requested}::new" unless cls == cls_requested
    end
  end

  m
end

.resolve_help_block(&block) ⇒ Object



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
# File 'lib/magic/help/tracepoint.rb', line 3

def self.resolve_help_block(&block)
  call_event = nil
  done = false
  res = nil

  # We want to capture calls to method_missing too
  # This should be available via TracePoint but isn't
  original_method_missing = BasicObject.instance_method(:method_missing)
  BasicObject.class_eval do
    define_method(:method_missing) do |*args|
      if args.empty?
        # This is presumably called on self, and without arguments
        throw :done, {cls: method(:method_missing).owner, meth: :method_missing, self: self}
      end

      if self.is_a?(Class)
        throw :done, {cls: self.singleton_class, meth: args[0], self: self}
      else
        throw :done, {cls: self.class, meth: args[0], self: self}
      end
    end
  end

  trace = TracePoint.new do |ev|
    next if done
    case ev.event
    when :call, :c_call
      if ev.defined_class == BasicObject and ev.method_id == :method_missing
        done = true
        # Let it reach our special handler
      # elsif ev.self == ArgumentError and ev.method_id == :new
        # Function was called without full number of arguments
        # There doesn't seem to be any way to recover from this in ruby 2.x
        # In 1.8 we'd get extra return event
        #
        # It's possible to hack argument name from stack trace,
        # (with massive hacking)
        # but not self/class, so it's not most useful
      else
        done = true
        throw :done, {cls: ev.defined_class, meth: ev.method_id, self: ev.self}
      end
    else
      # Ignore everything eles
    end
  end
  call_event = catch(:done) do
    trace.enable
    res = yield
    nil
  end
  done = true
  trace.disable

  BasicObject.instance_eval do
    define_method(:method_missing, original_method_missing)
    # It was originally private, restore it as such
    private :method_missing
  end

  if call_event
    cls = call_event[:cls]
    meth = call_event[:meth]
    bound_self = call_event[:self]
    is_singleton = (cls.is_a?(Class) and cls.singleton_class?)
    if is_singleton or meth == :new
      query = "#{bound_self}::#{meth}"
    else
      query = "#{cls}##{meth}"
    end
    postprocess(query)
  else
    resolve_help_res(res)
  end
end

.resolve_help_query(*args, &block) ⇒ Object

Raises:

  • (ArgumentError)


91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/magic/help.rb', line 91

def self.resolve_help_query(*args, &block)
  raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1
  raise ArgumentError, "help cannot take both arguments and block" if args.size > 0 and block_given?
  if block_given?
    resolve_help_block(&block)
  elsif args.empty?
    # No block, no arguments
    nil
  else
    resolve_help_res(args[0])
  end
end

.resolve_help_res(res) ⇒ Object



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
# File 'lib/magic/help.rb', line 46

def self.resolve_help_res(res)
  query = case res
  when Module
    res.to_s
  when UnboundMethod, Method
    help_method_extract(res)
  when /\A(.*)(#|::|\.)(.*)\Z/
    cp, k, m = $1, $2, $3
    begin
      # For multielement paths like File::Stat const_get must be
      # called multiple times, that is:
      # Object.const_get("File").const_get("Stat")
      cls = cp.split(/::/).inject(Object){|c, path_elem| c.const_get(path_elem) }
      case k
      when "#"
        m = cls.instance_method(m)
        m = help_method_extract(m)
      when "::"
        m = cls.method(m)
        # Make sure a module method is returned
        # It fixes `Class::new' resolving to `Class#new'
        # (Class::new and Class#new are the same thing,
        # but their documentations are different)
        m = help_method_extract(m)
        m = m.sub(/\#/, "::") if cls == Class && m == "Class#new"
      when "."
        begin
          m = cls.instance_method(m)
        rescue NameError
          m = cls.method(m)
        end
        m = help_method_extract(m)
      end
      postprocess(m, res)
    rescue NameError
      res
    end
  when String
    res
  else
    res.class.to_s
  end
  query
end