Class: Drx::ObjInfo

Inherits:
Object show all
Defined in:
lib/drx/objinfo.rb,
lib/drx/graphviz.rb,
lib/drx/arguments.rb

Overview

An object orieneted wrapper around the DrX::Core functions.

This object lets you query various properties of an object.

info = ObjInfo.new("foo")
info.has_iv_tbl?
info.klass

Constant Summary collapse

GRAPHVIZ_COMMAND =

Notes:

  • Windows’ CMD.EXE too supports “2>&1”

  • We’re generating GIF, not PNG, because that’s a format Tk supports out of the box.

  • Each extension token is replaced by the filepath with that extension.

'dot "{dot}" -Tgif -o "{gif}" -Tcmapx -o "{map}" 2>&1'
@@sizes =
{
  '100%' => "
    node[fontsize=10]
  ",
  '90%' => "
    node[fontsize=10]
    ranksep=0.4
    edge[arrowsize=0.8]
  ",
  '80%' => "
    node[fontsize=10]
    ranksep=0.3
    nodesep=0.2
    node[height=0.4]
    edge[arrowsize=0.6]
  ",
  '60%' => "
    node[fontsize=8]
    ranksep=0.18
    nodesep=0.2
    node[height=0]
    edge[arrowsize=0.5]
  "
}

Class Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(obj) ⇒ ObjInfo

Returns a new instance of ObjInfo.



13
14
15
16
# File 'lib/drx/objinfo.rb', line 13

def initialize(obj)
  @obj = obj
  @type = Core::get_type(@obj)
end

Class Attribute Details

.use_arguments_gemObject

Whether to use the ‘arguments’ gem, if present. This isn’t on by default because it’s slow.



10
11
12
# File 'lib/drx/arguments.rb', line 10

def use_arguments_gem
  @use_arguments_gem
end

Instance Method Details

#==(other) ⇒ Object



22
23
24
# File 'lib/drx/objinfo.rb', line 22

def ==(other)
  other.is_a?(ObjInfo) and address == other.address
end

#_method_arguments__by_arguments_gem(method_name) ⇒ Object

Strategy: use the ‘arguments’ gem, which, in turn, uses either ParseTree (Ruby 1.8) or RubyParser (Ruby 1.9)

pros: shows default values; works for 1.8 too. cons: very slow.



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
# File 'lib/drx/arguments.rb', line 27

def _method_arguments__by_arguments_gem(method_name)
  @@once__arguments ||= begin
    begin
      require 'arguments'
    rescue LoadError
      # Not installed.
    end
    1
  end
  return nil if not defined? Arguments
  args = Arguments.names(the_object, method_name, false)
  # Convert this to a Ruby 1.9.2 format:
  return args.map do |arg|
    if arg.size == 2
      [:opt, arg[0], arg[1]]
    else
      name = arg[0].to_s
      case name[0,1]
      when '*' then [:rest, name[1..-1]]
      when '&' then [:block, name[1..-1]]
      else [:req, name]
      end
    end
  end
rescue SyntaxError => e
  # We could just return nil here, to continue to the next strategy,
  # but we want to inform the user of the suckiness of RubyParser.
  raise
rescue Exception
  nil
end

#_method_arguments__by_arity(method_name) ⇒ Object

Strategy: simulation via Method#arity.

This is used as a fallback if any of the other strategies fail.

pros: fast; work without any gems. cons: doesn’t show argument names.



89
90
91
92
93
94
95
96
97
98
99
# File 'lib/drx/arguments.rb', line 89

def _method_arguments__by_arity(method_name)
  method = the_object.instance_method(method_name)
  ary, rest = method.arity, false
  if ary < 0
    ary = -ary - 1
    rest = true
  end
  args = [[:req]] * ary
  args << [:rest] if rest
  return args
end

#_method_arguments__by_methopara(method_name) ⇒ Object

Strategy: use Method#parameters (for ruby 1.9 only).

pros: fast. cons: doesn’t show default values.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/drx/arguments.rb', line 63

def _method_arguments__by_methopara(method_name)
  @@once__methopara ||= begin
    # For ruby 1.9.0 and 1.9.1, we need to use a gem.
    begin
      require 'methopara'
    rescue LoadError
      # Not installed.
    end
    1
  end
  method = the_object.instance_method(method_name)
  if method.respond_to? :parameters
    return method.parameters
  end
rescue NotImplementedError
  # For some methods #parameters raises an exception. We return nil
  # to move on to the next strategy.
  return nil
end

#addressObject



18
19
20
# File 'lib/drx/objinfo.rb', line 18

def address
  Core::get_address(@obj)
end

#class_like?Boolean

Returns true if this object is either a class or a module. When true, you know it has ‘m_tbl’ and ‘super’.

Returns:

  • (Boolean)


32
33
34
# File 'lib/drx/objinfo.rb', line 32

def class_like?
  [Core::T_CLASS, Core::T_ICLASS, Core::T_MODULE].include? @type
end

#display_klass?(kls) ⇒ Boolean

Whether to display the klass.

Returns:

  • (Boolean)


208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/drx/graphviz.rb', line 208

def display_klass?(kls)
  if t_iclass?
    # We're interested in an ICLASS's klass only if it isn't Module.
    #
    # Usually this means that the ICLASS has a singleton (see "Singletons
    # of included modules" in display_super?()). We want to see this
    # singleton.

    # Unfortunately, here is a special treatment for the 'arguments' gem,
    # which our GUI uses. That gem includes the 'Arguments' module in both
    # Class and Module (this is redundant!) and having the singleton twice
    # in our graph may break its nice rectangular structure. So we don't
    # show its singleton.
    if defined? ::Arguments
       return false if ::Arguments == klass.the_object
    end

    return kls != _Module
  else
    # Displaying a singleton's klass is confusing and usually unneeded.
    return !singleton?
  end
end

#display_super?(spr, my_ancestors) ⇒ Boolean

Whether to display the super.

Returns:

  • (Boolean)


233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/drx/graphviz.rb', line 233

def display_super?(spr, my_ancestors)
  if spr == _Module
    # Many objects have Module as their super (e.g., singletones
    # of included modules, or modules included in them). To prevent
    # clutter we print the arrow to Module only if it comes from
    # Class (or a module included in it).
    return (my_ancestors + [self]).include?(_Class)
    #
    # A somewhat irrelevant note (I don't have a better place to put it):
    #
    # "Singletons of included modules" often exist solely for their
    # #included method. For example, DataMapper#Resource has
    # such a singleton.
  end
  return true
end

#dot_fragment(opts = {}, ancestors = []) {|_self| ... } ⇒ Object

:yield:

Yields:

  • (_self)

Yield Parameters:

  • _self (Drx::ObjInfo)

    the object that the method was called on



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/drx/graphviz.rb', line 132

def dot_fragment(opts = {}, ancestors = [], &block) # :yield:
  out = ''
  # Note: since 'obj' may be a T_ICLASS, it doesn't respond to many methods,
  # including is_a?. So when we're querying things we're using Drx calls
  # instead.

  seen = @@seen[address]
  @@seen[address] = true

  if not seen
    dot_style = method('dot_style__' + (opts[:style] || 'default')).call
    out << "#{dot_id} [#{dot_style}, label=#{dot_quote dot_label}, URL=#{dot_quote dot_url}];" "\n"
  end

  yield self if block_given?

  return '' if seen

  if class_like?
    if spr = self.super and display_super?(spr, ancestors)
      out << spr.dot_fragment(opts, ancestors + [self], &block)
      if insignificant_super_arrow?(opts, ancestors)
        # We don't want these relatively insignificant lines to clutter the display,
        # so we paint them lightly and tell DOT they aren't to affect the layout (width=0).
        out << "#{dot_id} -> #{spr.dot_id} [color=gray85, weight=0];" "\n"
      else
        out << "#{dot_id} -> #{spr.dot_id};" "\n"
      end
    end
  end

  kls = effective_klass
  if display_klass?(kls)
    out << kls.dot_fragment(opts, &block)
    # Recall that in Ruby there are two main inheritance groups: the class
    # inheritance and the singleton inheritance.
    #
    # When an ICLASS has a singleton, we want this singleton to appear close
    # to the ICLASS, because we want to keep the two groups visually distinct.
    # We do this by setting the arrow's weight to 1.0.
    #
    # (To see the effect of this, set the weight unconditionally to '0' and
    # see the graph for DataMapper.)
    weight = t_iclass? ? 1 : 0

    # However, here's a special case. DOT seems to have a bug: it sometimes
    # doesn't draw the arrows going out of Class and Module (to their
    # singletons). Making their weight 1 makes DOT draw them.
    weight = 1 if self == _Class or self == _Module

    out << "#{dot_id} -> #{kls.dot_id} [style=dotted, weight=#{weight}];" "\n"
    out << "{ rank=same; #{dot_id}; #{kls.dot_id}; }" "\n"
  end

  out
end

#dot_idObject

Create an ID for the DOT node representing this object.



40
41
42
43
44
45
46
# File 'lib/drx/graphviz.rb', line 40

def dot_id
   ('o' + address.to_s).sub('-', '_')
   # Tip: when examining the DOT output you may wish to
   # uncomment the following line. It will show you which
   # ruby object the DOT node represents.
   #('o' + address.to_s).sub('-', '_') + " /* #{repr} */ "
end

#dot_label(max = 20) ⇒ Object

Returns the DOT label for the node.

The representation may be quite big, so we trim it.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/drx/graphviz.rb', line 103

def dot_label(max = 20)
  if class_like?
    # Let's be more lenient when trimming a class/module name.
    # We want to show The::Last::Component and possibly a singleton's
    # trailing 'S.
    max = 60 if max < 60
  end
  r = repr
  if r.length > max
    r[0, max] + ' ...'
  else
    r
  end
end

#dot_quote(s) ⇒ Object

Quotes a string to be used in DOT source.



54
55
56
# File 'lib/drx/graphviz.rb', line 54

def dot_quote(s)
  '"' + s.gsub('\\') { '\\\\' }.gsub('"', '\\"').gsub("\n", '\\n') + '"'
end

#dot_source(opts = {}, &block) ⇒ Object

Builds the DOT source for the diagram. if you’re only interested in the output image, use generate_diagram() instead.



120
121
122
123
124
125
126
127
128
129
130
# File 'lib/drx/graphviz.rb', line 120

def dot_source(opts = {}, &block) # :yield:
  opts = opts.dup
  opts[:base] = self
  @@seen = {}

  out = 'digraph {' "\n"
  out << @@sizes[opts[:size] || '100%']
  out << dot_fragment(opts, &block)
  out << '}' "\n"
  out
end

#dot_style__crazyObject

Returns the DOT style for the node.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/drx/graphviz.rb', line 81

def dot_style__crazy
  craze = "distortion=#{2*rand-1},skew=#{2*rand-1},orientation=#{360*rand}"
  crazy_oval = "shape=polygon,sides=25," + craze
  crazy_rect = "shape=polygon,sides=#{4+rand(3)}," + craze
  if singleton?
    # A singleton class
    "#{crazy_oval},color=palevioletred3,style=filled,fontcolor=white,peripheries=3"
  elsif t_class?
    # A class
    "#{crazy_oval},color=palevioletred1,style=filled"
  elsif t_iclass? or t_module?
    # A module
    "#{crazy_rect},color=peachpuff1,style=filled"
  else
    # Else: a "normal" object, or an immediate.
    "shape=house,color=pink,style=filled"
  end
end

#dot_style__defaultObject

Returns the DOT style for the node.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/drx/graphviz.rb', line 59

def dot_style__default
  if singleton?
    # A singleton class
    "shape=oval,color=skyblue1,style=filled"
  elsif t_class?
    # A class
    "shape=oval,color=lightblue1,style=filled"
  elsif t_iclass? or t_module?
    # A module
    if repr['#']
      # Paint anonymous modules only lightly.
      "shape=box,style=filled,color=\"#D9FFF2\",fontcolor=gray60"
    else
      "shape=box,style=filled,color=aquamarine"
    end
  else
    # Else: a "normal" object, or an immediate.
    "shape=house,color=wheat1,style=filled"
  end
end

#dot_urlObject

Creates a pseudo URL for the HTML imagemap.



49
50
51
# File 'lib/drx/graphviz.rb', line 49

def dot_url
  "http://ruby/object/#{dot_id}"
end

#effective_klassObject

Like klass(), but without surprises.

Since the klass of an ICLASS is the module itself, we need to invoke klass() twice.



254
255
256
257
258
259
260
# File 'lib/drx/graphviz.rb', line 254

def effective_klass
  if t_iclass?
    klass.klass
  else
    klass
  end
end

#examine(level = 0, title = '', &block) ⇒ Object

A utility function to print the inheritance hierarchy of an object.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/drx/objinfo.rb', line 136

def examine(level = 0, title = '', &block) # :yield:
  # Note: since '@obj' may be a T_ICLASS, it doesn't repond to may methods,
  # including is_a?. So when we're querying things we're using Drx calls
  # instead.

  @@seen = {} if level.zero?
  line = ('  ' * level) + title + ' ' + repr

  seen = @@seen[address]
  @@seen[address] = true

  if seen
    line += " [seen]"
  end

  if block_given?
    yield line, self
  else
    puts line
  end

  return if seen

  if class_like?
    if spr = self.super
      spr.examine(level + 1, '[super]', &block)
    end
  end
 
  # Displaying a T_ICLASS's klass isn't very useful, because the data
  # is already mirrored in the m_tbl and iv_tvl of the T_ICLASS itself.
  if not t_iclass?
    klass.examine(level + 1, '[klass]', &block)
  end
end

#generate_diagram(files, opts = {}, &block) ⇒ Object

Generates a diagram of the inheritance hierarchy. It accepts a hash pointing to filepaths to write the result to. A Tempfiles hash can be used instead.

the_object = "some object"
Tempfiles.new do |files|
  ObjInfo.new(the_object).generate_diagram
  system('xview ' + files['gif'])
end


283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/drx/graphviz.rb', line 283

def generate_diagram(files, opts = {}, &block)
  source = self.dot_source(opts, &block)
  File.open(files['dot'], 'w') { |f| f.write(source) }
  # Replace {extension} tokens with their corresponding filepaths:
  command = GRAPHVIZ_COMMAND.gsub(/\{([a-z]+)\}/) { files[$1] }
  message = Kernel.`(command)  # `
  if $? != 0
    error = <<-EOS % [command, message]
ERROR: Failed to run the 'dot' command. Make sure you have the GraphViz
package installed and that its bin folder appears in your PATH.

The command I tried to execute is this:

%s

And the response I got is this:

%s
    EOS
    raise error
  end
end

#get_ivar(name) ⇒ Object

Returns the value of an instance variable. Actually, of any sort of variable that’s recorded in the variable-table.



79
80
81
82
83
84
85
86
# File 'lib/drx/objinfo.rb', line 79

def get_ivar(name)
  if class_like? and name.to_s =~ /^[A-Z]/
    # If it's a constant, it may be 'autoloaded'. We
    # trigger the loading by calling const_get().
    @obj.const_get(name)
  end
  Core::get_ivar(@obj, name)
end

#has_iv_tbl?Boolean

Returns:

  • (Boolean)


67
68
69
# File 'lib/drx/objinfo.rb', line 67

def has_iv_tbl?
  t_object? || class_like?
end

#insignificant_super_arrow?(opts, my_ancestors) ⇒ Boolean

Whether the ‘super’ arrow is infignificant and must not affect the DOT layout

A Ruby object graph is cyclic. We don’t want to feed DOT a cyclic graph because it will ruin our nice “rectangular” layout. The purpose of the following method is to break the cycle. Normally we break the cycle at Module (and its singleton). When the user is examining a module, we instead break the cycle at Class (and its singleton).

Returns:

  • (Boolean)


197
198
199
200
201
202
203
204
205
# File 'lib/drx/graphviz.rb', line 197

def insignificant_super_arrow?(opts, my_ancestors)
  if opts[:base].t_module?
    self == _ClassS or
      (self.super == _Module and (my_ancestors + [self]).include? _Class)
  else
    self == _ModuleS or
      (self.super == _Object and (my_ancestors + [self]).include? _Module)
  end
end

#iv_tblObject

Returns the variable-table of an object.



72
73
74
75
# File 'lib/drx/objinfo.rb', line 72

def iv_tbl
  return nil if not has_iv_tbl?
  Core::get_iv_tbl(@obj)
end

#klassObject

Note: the klass of an iclass is the included module.



109
110
111
# File 'lib/drx/objinfo.rb', line 109

def klass
  ObjInfo.new Core::get_klass(@obj)
end

#locate_method(method_name) ⇒ Object

Returns the source-code position where a method is defined.

Returns one of:

- [ 'file.rb', 12 ]
- "<c>", "<undef>", "<alias>", etc., if not a ruby code (for Ruby 1.9,
  returns only "<c>").
- nil, if functionality not implemented.
- raises NameError if method not found.


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/drx/objinfo.rb', line 49

def locate_method(method_name)
  if Core.respond_to? :locate_method
    # Ruby 1.8
    Core::locate_method(@obj, method_name)
  elsif Method.method_defined? :source_location
    # Ruby 1.9
    location = @obj.instance_method(method_name).source_location
    if location
      location
    else
      '<c>'
    end
  else
    # Some version of ruby that doesn't have Method#source_loction
    nil
  end
end

#m_tblObject

Returns the method-table of an object.



37
38
39
# File 'lib/drx/objinfo.rb', line 37

def m_tbl
  Core::get_m_tbl(@obj)
end

#method_arguments(method_name) ⇒ Object

Returns a Ruby 1.9.2-compatible array describing the arguments a method expects.



14
15
16
17
18
19
20
# File 'lib/drx/arguments.rb', line 14

def method_arguments(method_name)
  if ObjInfo.use_arguments_gem
    _method_arguments__by_arguments_gem(method_name) || _method_arguments__by_arity(method_name)
  else
    _method_arguments__by_methopara(method_name) || _method_arguments__by_arity(method_name)
  end
end

#reprObject

Returns a string representation of the object. Similar to Object#inspect.



124
125
126
127
128
129
130
131
132
133
# File 'lib/drx/objinfo.rb', line 124

def repr
  if t_iclass?
    'include ' + klass.repr
  elsif singleton?
    attached = get_ivar('__attached__') || self
    attached.inspect + " 'S"
  else
    @obj.inspect
  end
end

#singleton?Boolean

Returns:

  • (Boolean)


88
89
90
# File 'lib/drx/objinfo.rb', line 88

def singleton?
  class_like? && (Core::get_flags(@obj) & Core::FL_SINGLETON).nonzero?
end

#superObject

Returns the ‘super’ of a class-like object. Returns nil for end of chain.

Examples: Kernel has a NULL super. Modules too have NULL super, unless when ‘include’ing.



117
118
119
120
121
# File 'lib/drx/objinfo.rb', line 117

def super
  spr = Core::get_super(@obj)
  # Note: we can't do 'if spr.nil?' because T_ICLASS doesn't "have" #nil.
  spr ? ObjInfo.new(spr) : nil
end

#t_class?Boolean

Returns:

  • (Boolean)


96
97
98
# File 'lib/drx/objinfo.rb', line 96

def t_class?
  @type == Core::T_CLASS
end

#t_iclass?Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/drx/objinfo.rb', line 92

def t_iclass?
  @type == Core::T_ICLASS
end

#t_module?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'lib/drx/objinfo.rb', line 104

def t_module?
  @type == Core::T_MODULE
end

#t_object?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'lib/drx/objinfo.rb', line 100

def t_object?
  @type == Core::T_OBJECT
end

#the_objectObject



26
27
28
# File 'lib/drx/objinfo.rb', line 26

def the_object
  @obj
end