shikhalev/rdot

View on GitHub
lib/rdot.rb

Summary

Maintainability
F
1 wk
Test Coverage
# encoding: utf-8

require 'is/monkey/sandbox'
require 'is/monkey/namespace'
require 'rdot-common'

# @private
class Module

  def module_scope
    m = self.inspect
    if m[0...8] == '#<Class:'
      s = m.rindex '>'
      v = m[8...s]
      begin
        value = sandbox { eval v }
        return [value, :class]
      rescue
        return [nil, nil]
      end
    else
      return [self, :instance]
    end
  end

  alias :rdot_old_attr :attr
  alias :rdot_old_attr_reader :attr_reader
  alias :rdot_old_attr_writer :attr_writer
  alias :rdot_old_attr_accessor :attr_accessor

  def parse_caller clr
    clr.each do |s|
      if s.include?('`<module:') || s.include?('`<class:') ||
          s.include?("`singletonclass'") || s.include?('`block in <')
        a = s.split(':')
        begin
          return [a[0], a[1].to_i]
        rescue
        end
      end
    end
    return nil
  end

  def attr *names
    RDot.register_attribute *module_scope, names, '[r]', parse_caller(caller)
    rdot_old_attr *names
  end

  def attr_reader *names
    RDot.register_attribute *module_scope, names, '[r]', parse_caller(caller)
    rdot_old_attr_reader *names
  end

  def attr_writer *names
    RDot.register_attribute *module_scope, names, '[w]', parse_caller(caller)
    rdot_old_attr_writer *names
  end

  def attr_accessor *names
    RDot.register_attribute *module_scope, names, '[rw]', parse_caller(caller)
    rdot_old_attr_accessor *names
  end

  private :module_scope, :parse_caller, :attr, :attr_reader, :attr_writer,
      :attr_accessor, :rdot_old_attr, :rdot_old_attr_accessor,
      :rdot_old_attr_reader, :rdot_old_attr_writer

end

module RDot

  class << self

    # @private
    def register_attribute mod, scope, names, access, source
      @attributes ||= {}
      @attributes[mod] ||= {}
      @attributes[mod][scope] ||= {}
      names.each do |name|
        @attributes[mod][scope][name.intern] = {
          :access => access,
          :source => source
        }
      end
    end

    # @private
    def get_method_object mod, scope, name
      case scope
      when :instance
        mod.instance_method(name)
      when :class
        mod.singleton_class.instance_method(name)
      end
    end

    # @private
    def add_method acc, mod, scope, visibility, name, opts
      m = get_method_object mod, scope, name
      src = m.source_location
      obj = {}
      nm = name.to_s
      nm = nm[0..-2] if nm[-1] == '='
      nm = nm.intern
      if @attributes && @attributes[mod] && @attributes[mod][scope] &&
              @attributes[mod][scope][nm]
        src = @attributes[mod][scope][nm][:source]
        obj[:access] = @attributes[mod][scope][nm][:access]
      end
      if src
        obj[:file] = get_file src[0]
        obj[:line] = src[1]
        if opts[:exclude_files]
          opts[:exclude_files].each do |f|
            if File.expand_path(f) == File.expand_path(src[0])
              return nil
            end
          end
        end
        if opts[:filter_files]
          opts[:filter_files].each do |f|
            if File.expand_path(f) != File.expand_path(src[0])
              return nil
            end
          end
        end
      elsif opts[:filter_files]
        return nil
      end
      acc[scope] ||= {}
      acc[scope][visibility] ||= {}
      if opts[:select_attributes] && obj[:access]
        obj[:signature] = nm.to_s + ' ' + obj[:access]
        acc[scope][visibility][:attributes] ||= {}
        acc[scope][visibility][:attributes][nm] = obj
      else
        if ! opts[:hide_arguments]
          ltr = 'a'
          obj[:signature] = name.to_s + '(' + m.parameters.map do |q, n|
            nm = n || ltr
            ltr = ltr.succ
            case q
            when :req
              nm
            when :opt
              "#{nm} = <…>"
            when :rest
              "*#{nm}"
            when :key
              "#{nm}: <…>"
            when :keyreq
              "#{nm}:"
            when :keyrest
              "**#{nm}"
            when :block
              "&#{nm}"
            end
          end.join(', ') + ')'
        else
          obj[:signature] = name.to_s + '()'
        end
        acc[scope][visibility][:methods] ||= {}
        acc[scope][visibility][:methods][name] = obj
      end
      return obj
    end

    # @private
    def get_module mod, opts
      result = {}
      result[:module] = mod
      incs = mod.included_modules - [mod]
      exts = mod.singleton_class.included_modules - Module.included_modules
      if Class === mod
        exts -= Class.included_modules
        result[:superclass] = mod.superclass && mod.superclass.inspect || nil
        if mod.superclass
          incs -= mod.superclass.included_modules
          exts -= mod.superclass.singleton_class.included_modules
        end
      end
      incs.dup.each { |d| incs -= d.included_modules }
      exts.dup.each { |d| exts -= d.included_modules }
      result[:included] = incs.map &:inspect
      result[:extended] = exts.map &:inspect
      result[:nested] = mod.namespace && mod.namespace.inspect || nil
      if opts[:no_scan]
        return result
      end
      if ! opts[:hide_constants]
        result[:constants] = {}
        mod.constants(false).each do |c|
          next if mod == Object && c == :Config
          begin
          if (auto = mod.autoload?(c))
            result[:constants][c] = 'auto:' + get_file(auto)
          elsif mod.const_defined? c
            result[:constants][c] = mod.const_get(c).class.inspect
          else
            result[:constants][c] = 'undefined'
          end
          rescue NameError
            result[:constants][c] = 'invalid'
        end
        end
      end
      if ! opts[:hide_methods]
        mod.public_instance_methods(false).each do |m|
          add_method result, mod, :instance, :public, m, opts
        end
        mod.singleton_class.public_instance_methods(false).each do |m|
          add_method result, mod, :class, :public, m, opts
        end
        if opts[:show_protected]
          mod.protected_instance_methods(false).each do |m|
            add_method result, mod, :instance, :protected, m, opts
          end
          mod.singleton_class.protected_instance_methods(false).each do |m|
            add_method result, mod, :class, :protected, m, opts
          end
        end
        if opts[:show_private]
          mod.private_instance_methods(false).each do |m|
            add_method result, mod, :instance, :private, m, opts
          end
          mod.singleton_class.private_instance_methods(false).each do |m|
            add_method result, mod, :class, :private, m, opts
          end
        end
      end
      result
    end

    # @private
    def add_module acc, mod, opts
      if opts[:exclude_classes]
        opts[:exclude_classes].each do |c|
          return nil if mod <= c
        end
      end
      if opts[:filter_classes]
        opts[:filter_classes].each do |c|
          return nil unless mod <= c
        end
      end
      if opts[:exclude_namespaces]
        opts[:exclude_namespaces].each do |n|
          return nil if mod == n || mod.in?(n)
        end
      end
      if opts[:filter_namespaces]
        opts[:filter_namespaces].each do |n|
          return nil unless mod == n || mod.in?(n)
        end
      end
      if opts[:filter_global]
        return nil unless mod.global?
      end
      acc[mod.inspect] = get_module mod, opts
    end

    # Make hash with data of objectspace.
    #
    # @param [Hash] opts Options (see also {dot} & {diff}).
    # @option opts [Array<Class>] :exclude_classes don't add classes listed and
    #                                              their descendants;
    # @option opts [Array<String>] :exclude_files don't add methods defined in
    #                                             files listed;
    # @option opts [Array<Module>] :exclude_namespaces don't add classes/modules
    #                                                  listed and their inners;
    # @option opts [Array<Class>] :filter_classes add only classes listed and
    #                                             their descendants;
    # @option opts [Array<String>] :filter_files add only methods defined in
    #                                            files listed;
    # @option opts [Boolean] :filter_global add only classes/modules in global
    #                                       namespace;
    # @option opts [Array<Module>] :filter_namespaces add only classes/modules
    #                                                 listed and their inners;
    # @option opts [Boolean] :hide_arguments don't add arguments info to
    #                                        methods' names;
    # @option opts [Boolean] :hide_constants don't add constants' data;
    # @option opts [Boolean] :hide_methods don't add methods' data;
    # @option opts [Boolean] :select_attributes make attributes data instead
    #                                           getter and setter methods;
    # @option opts [Boolean] :show_private add data of private methods;
    # @option opts [Boolean] :show_protected add data of protected methods.
    # @return [Hash] Objectspace.
    def snapshot opts = {}
      opts = defaults.merge opts
      result = {}
      ObjectSpace.each_object(Module) { |m| add_module result, m, opts }
      result
    end

    # @private
    def diff_module base, other, opts
      if ! other
        return base.merge :new => true
      end
      if opts[:show_preloaded]
        return base
      end
      result = {}
      result[:module] = base[:module]
      result[:superclass] = base[:superclass]
      result[:nested] = base[:nested]
      result[:included] = base[:included] # - other[:included]
      test_included = base[:included] - other[:included]
      result[:extended] = base[:extended] # - other[:extended]
      test_extended = base[:extended] - other[:extended]
      result[:constants] = {}
      if base[:constants]
        base[:constants].each do |c|
          if base[:constants][c] != other[:constants][c]
            result[:constants][c] = base[:constants][c]
          end
        end
      end
      [:class, :instance].each do |s|
        [:public, :protected, :private].each do |v|
          [:attributes, :methods].each do |k|
            if base[s] && base[s][v] && base[s][v][k]
              base[s][v][k].each do |n, m|
                unless other[s] && other[s][v] && other[s][v][k] &&
                    other[s][v][k][n] &&
                    other[s][v][k][n][:file] == m[:file] &&
                    other[s][v][k][n][:line] == m[:line]
                  result[s] ||= {}
                  result[s][v] ||= {}
                  result[s][v][k] ||= {}
                  result[s][v][k][n] = m
                end
              end
            end
          end
        end
      end
      if test_included.empty? && test_extended.empty? &&
          result[:constants].empty? && result[:class].nil? &&
          result[:instance].nil?
        nil
      else
        if result[:module] == Object
          result[:included] << 'Kernel' if ! result[:included].include?(Kernel)
        end
        result
      end
    end

    # Make diff between two objectspaces.
    #
    # @param [Hash] base 'New' objectspace.
    # @param [Hash] other 'Old' objectspace
    # @param [Hash] opts Options (see also {snapshot} & {dot}).
    # @option opts [Boolean] :show_preloaded if true old classes/modules will not
    #                                   deleted from difference.
    # @return [Hash] Difference objectspace.
    def diff base, other, opts = {}
      opts = defaults.merge opts
      if other == nil
        return base
      end
      result = {}
      base.each do |n, m|
        d = diff_module m, other[n], opts
        result[n] = d if d
      end
      result
    end

    # @private
    def find_module space, name
      return space[name] if space[name]
      begin
        mod = sandbox { eval name }
        return get_module(mod, :no_scan => true)
      rescue
      end
      nil
    end

    # Default values for {dot} options.
    #
    # @return [Hash] see source for details or {dot} for description.
    def defaults
      {
        :graph_fontname                 => 'sans-serif',
        :graph_fontsize                 => 24,
        :graph_label                    => 'RDot Graph',
        :node_fontname                  => 'monospace',
        :node_fontsize                  => 9,
        :color_class                    => '#77FF77',
        :color_class_preloaded          => '#AAFFAA',
        :color_class_core               => '#DDFFDD',
        :color_exception                => '#FF7777',
        :color_exception_preloaded      => '#FFAAAA',
        :color_exception_core           => '#FFDDDD',
        :color_module                   => '#9999FF',
        :color_module_preloaded         => '#BBBBFF',
        :color_module_core              => '#DDDDFF',
#         :color_class                    => '#BBFFBB',
#         :color_class_preloaded          => '#CCEECC',
#         :color_class_core               => '#DDFF99',
#         :color_exception                => '#FFBBBB',
#         :color_exception_preloaded      => '#EECCCC',
#         :color_exception_core           => '#FFDD99',
#         :color_module                   => '#BBBBFF',
#         :color_module_preloaded         => '#CCCCEE',
#         :color_module_core              => '#99DDFF',
        :color_protected                => '#EEEEEE',
        :color_private                  => '#DDDDDD',
        :color_inherited                => '#0000FF',
        :color_included                 => '#00AAFF',
        :color_extended                 => '#AA00FF',
        :color_nested                   => '#EEEEEE',
        :graph_splines                  => 'spline'
      }
    end

    # @private
    def node_name name
      'node_' + name.gsub(/\W/, '_')
    end

    # @private
    def module_stage m
      if @preset.include? m[:module]
        :core
      elsif m[:new]
        :new
      else
        :old
      end
    end

    # @private
    def node_color m, opts
      mod = m[:module]
      stg = module_stage m
      if Class === mod
        if mod <= Exception
          case stg
          when :core
            opts[:color_exception_core]
          when :old
            opts[:color_exception_preloaded]
          when :new
            opts[:color_exception]
          end
        else
          case stg
          when :core
            opts[:color_class_core]
          when :old
            opts[:color_class_preloaded]
          when :new
            opts[:color_class]
          end
        end
      else
        case stg
        when :core
          opts[:color_module_core]
        when :old
          opts[:color_module_preloaded]
        when :new
          opts[:color_module]
        end
      end
    end

    # @private
    def module_kind m
      stg = module_stage m
      if Class === m[:module]
        if m[:module] <= Exception
          "[#{stg}] exception"
        else
          "[#{stg}] class"
        end
      else
        "[#{stg}] module"
      end
    end

    # @private
    def escape s
      s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub("\n", '\n')
    end

    # @private
    def dot_constants m
      result = []
      if m[:constants]
        m[:constants].sort.each do |n, c|
          result << "#{escape(n.to_s)} &lt;#{escape(c)}&gt;"
        end
      end
      if result.size != 0
        '<TR><TD ROWSPAN="' + result.size.to_s +
            '" ALIGN="RIGHT" VALIGN="TOP">const</TD><TD COLSPAN="3" ALIGN="LEFT">' +
            result.join('</TD></TR><TR><TD COLSPAN="3" ALIGN="LEFT">') +
            '</TD></TR>'
      else
        ''
      end
    end

    # @private
    def dot_scope m, scope, opts
      result = []
      if m[scope]
        if m[scope][:public]
          if m[scope][:public][:attributes]
            m[scope][:public][:attributes].sort.each do |n, a|
              result << '<TD ALIGN="LEFT">' + escape(a[:signature]) +
                  '</TD><TD ALIGN="RIGHT">' + escape(a[:file].to_s) +
                  '</TD><TD ALIGN="RIGHT">' + a[:line].to_s + '</TD>'
            end
          end
          if m[scope][:public][:methods]
            m[scope][:public][:methods].sort.each do |n, a|
              result << '<TD ALIGN="LEFT">' + escape(a[:signature]) +
                  '</TD><TD ALIGN="RIGHT">' + escape(a[:file].to_s) +
                  '</TD><TD ALIGN="RIGHT">' + a[:line].to_s + '</TD>'
            end
          end
        end
        if m[scope][:protected]
          if m[scope][:protected][:attributes]
            m[scope][:protected][:attributes].sort.each do |n, a|
              result << '<TD BGCOLOR="' + opts[:color_protected] +
                  '" ALIGN="LEFT">' + escape(a[:signature]) +
                  '</TD><TD BGCOLOR="' + opts[:color_protected] +
                  '" ALIGN="RIGHT">' + escape(a[:file].to_s) +
                  '</TD><TD BGCOLOR="' + opts[:color_protected] +
                  '" ALIGN="RIGHT">' + a[:line].to_s + '</TD>'
            end
          end
          if m[scope][:protected][:methods]
            m[scope][:protected][:methods].sort.each do |n, a|
              result << '<TD BGCOLOR="' + opts[:color_protected] +
                  '" ALIGN="LEFT">' + escape(a[:signature]) +
                  '</TD><TD BGCOLOR="' + opts[:color_protected] +
                  '" ALIGN="RIGHT">' + escape(a[:file].to_s) +
                  '</TD><TD BGCOLOR="' + opts[:color_protected] +
                  '" ALIGN="RIGHT">' + a[:line].to_s + '</TD>'
            end
          end
        end
        if m[scope][:private]
          if m[scope][:private][:attributes]
            m[scope][:private][:attributes].sort.each do |n, a|
              result << '<TD BGCOLOR="' + opts[:color_private] +
                  '" ALIGN="LEFT">' + escape(a[:signature]) +
                  '</TD><TD BGCOLOR="' + opts[:color_private] +
                  '" ALIGN="RIGHT">' + escape(a[:file].to_s) +
                  '</TD><TD BGCOLOR="' + opts[:color_private] +
                  '" ALIGN="RIGHT">' + a[:line].to_s + '</TD>'
            end
          end
          if m[scope][:private][:methods]
            m[scope][:private][:methods].sort.each do |n, a|
              result << '<TD BGCOLOR="' + opts[:color_private] +
                  '" ALIGN="LEFT">' + escape(a[:signature]) +
                  '</TD><TD BGCOLOR="' + opts[:color_private] +
                  '" ALIGN="RIGHT">' + escape(a[:file].to_s) +
                  '</TD><TD BGCOLOR="' + opts[:color_private] +
                  '" ALIGN="RIGHT">' + a[:line].to_s + '</TD>'
            end
          end
        end
      end
      if result.size != 0
        '<TR><TD ROWSPAN="' + result.size.to_s +
            '" ALIGN="RIGHT" VALIGN="TOP">' + scope.to_s + '</TD>' +
            result.join('</TR><TR>') + '</TR>'
      else
        ''
      end
    end

    # @private
    def node_label name, m, opts
      result = []
      result << '<TABLE CELLBORDER="0" CELLSPACING="0">'
      result << '<TR>'
      result << '<TD ALIGN="RIGHT" BGCOLOR="' + node_color(m, opts) + '">'
      result << '<B>'
      result << module_kind(m)
      result << '</B>'
      result << '</TD>'
      result << '<TD COLSPAN="3" ALIGN="LEFT" BGCOLOR="' +
          node_color(m, opts) + '">'
      result << '<B>'
      result << escape(name)
      result << '</B>'
      result << '</TD>'
      result << '</TR>'
      result << dot_constants(m)
      result << dot_scope(m, :class, opts)
      result << dot_scope(m, :instance, opts)
      result << '</TABLE>'
      result.join ''
    end

    # @private
    def dot_module space, name, m, opts
      if m == nil
        # $stderr.puts "Warning: nil module by name \"#{name}\"!"
        # return nil
        m = get_module eval(name), no_scan: true
      end
      if @processed.include?(m[:module])
        return nil
      else
        @processed << m[:module]
      end
      result = []
      result << node_name(name) + '['
      result << '  label=<' + node_label(name, m, opts) + '>'
      result << '];'
      if opts[:hide_nested] || ! m[:module].namespace #) && ! (Class === m[:module]) && ! (m[:module] == Kernel)
        result << node_name(name) + ' -> nullnode[color=transparent,minlen=0,weight=1];'
      end
      if m[:nested] && ! opts[:hide_nested]
        ns = find_module space, m[:nested]
        result << dot_module(space, m[:nested], ns, opts)
        @nested << node_name(name) + ' -> ' + node_name(m[:nested]) + ';'
      end
      if ! opts[:hide_extended]
        m[:extended].each do |e|
          ext = find_module space, e
          result << dot_module(space, e, ext, opts)
          @extended << node_name(name) + ' -> ' + node_name(e) + ';'
        end
      end
      if ! opts[:hide_included]
        m[:included].each do |i|
          next if m[:module].name == 'CMath' && i == 'Math'
          inc = find_module space, i
          result << dot_module(space, i, inc, opts)
          @included << node_name(name) + ' -> ' + node_name(i) + ';'
        end
      end
      if m[:superclass]
        spc = find_module space, m[:superclass]
        result << dot_module(space, m[:superclass], spc, opts)
        @inherited << node_name(name) + ' -> ' + node_name(m[:superclass]) + ';'
      end
      result.join "\n  "
    end

    # Make .dot text from snapshot or difference.
    #
    # @param [Hash] space Snapshot or difference objectspace.
    # @param [Hash] opts Options (see also {snapshot} & {diff}). This Hash will
    #                    be merged over default values (see {defaults}).
    # @option opts [String] :color_class class node title background color;
    # @option opts [String] :color_class_core core class node title background
    #                                         color;
    # @option opts [String] :color_class_preloaded preloaded class node title
    #                                              background color;
    # @option opts [String] :color_exception exception node title background
    #                                        color;
    # @option opts [String] :color_exception_core core exception node title
    #                                             background color;
    # @option opts [String] :color_exception_preloaded preloaded exception node
    #                                                  title background color;
    # @option opts [String] :color_extended color of 'extended' edges;
    # @option opts [String] :color_included color of 'included' edges;
    # @option opts [String] :color_inherited color of 'inherited' edges;
    # @option opts [String] :color_module module node title background color;
    # @option opts [String] :color_module_core core module node title background
    #                                          color;
    # @option opts [String] :color_module_preloaded preloaded module title
    #                                               background color;
    # @option opts [String] :color_nested color of 'nested' edges;
    # @option opts [String] :color_private background color for private methods;
    # @option opts [String] :color_protected background color for protected
    #                                        methods;
    # @option opts [String] :graph_fontname font name of graph title;
    # @option opts [Numeric] :graph_fontsize font size of graph title;
    # @option opts [String] :graph_label graph title;
    # @option opts [Boolean] :hide_extended if true hide 'extended' edges;
    # @option opts [Boolean] :hide_included if true hide 'included' edges;
    # @option opts [Boolean] :hide_nested if true hide 'nested' edges;
    # @option opts [String] :node_fontname font name of class/module nodes;
    # @option opts [Numeric] :node_fontsize font size of class/module nodes.
    # @return [String] Text of .dot-file.
    def dot space, opts = {}
      opts = defaults.merge opts
      result = []
      result << 'digraph graph_RDot{'
      result << '  graph['
      result << '    rankdir=RL,'
      result << '    splines=' + opts[:graph_splines] + ','
      result << '    labelloc=t,searchsize=1000,'
      result << '    fontname="' + opts[:graph_fontname] + '",'
      result << '    fontsize=' + opts[:graph_fontsize].to_s + ','
      result << '    label="' + opts[:graph_label] + '"'
      result << '  ];'
      result << '  node['
      result << '    shape=plaintext,'
      result << '    fontname="' + opts[:node_fontname] + '",'
      result << '    fontsize=' + opts[:node_fontsize].to_s + ''
      result << '  ];'
      result << '  edge['
#      result << '    dir=back,'
#      result << '    arrowhead=vee,'
      result << '    penwidth=0.5, arrowsize=0.5'
      result << '  ];'
      result << '  nullnode[label=""];'
#      result << '  nullnode -> ' + node_name('Kernel') + '[color=yellow,weight=0,minlen=0];'
      @processed = []
      @nested = []
      @extended = []
      @included = []
      @inherited = []
      space.each do |n, m|
        mm = dot_module space, n, m, opts
        result << '  ' + mm if mm
      end
      result << '  subgraph subNested{'
      result << '    edge['
      result << '      color="' + opts[:color_nested] + '",'
      result << '      weight=1,'
      result << '      minlen=0'
      result << '    ];'
      result << '    ' + @nested.join("\n    ")
      result << '  }'
      result << '  subgraph subExtended{'
      result << '    edge['
      result << '      color="' + opts[:color_extended] + '",'
      result << '      weight=1,'
      result << '      minlen=0'
      result << '    ];'
      result << '    ' + @extended.join("\n    ")
      result << '  }'
      result << '  subgraph subIncluded{'
      result << '    edge['
      result << '      color="' + opts[:color_included] + '",'
      result << '      weight=2,'
      result << '      minlen=1'
      result << '    ];'
      result << '    ' + @included.join("\n    ")
      result << '  }'
      result << '  subgraph subInherited{'
      result << '    edge['
      result << '      color="' + opts[:color_inherited] + '",'
      result << '      weight=4,'
      result << '      minlen=1'
      result << '    ];'
      result << '    ' + @inherited.join("\n    ")
      result << '  }'
      result << '}'
      result.join "\n"
    end

    private :get_method_object, :get_module, :add_method,
        :add_module, :diff_module, :find_module, :dot_module, :node_name,
        :node_color, :node_label, :module_kind, :dot_constants, :dot_scope,
        :module_stage, :escape

  end

  @preset = []
  ObjectSpace.each_object(Module) { |m| @preset << m if m != ::RDot }

end