ruby2uml 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bf7a8d24d4df375a813a59abc0543e0497cfe52b
4
+ data.tar.gz: e65d5635812219d1dc77ce5538e1bd0e70a9e62b
5
+ SHA512:
6
+ metadata.gz: 2b4cc5ab718f5bbbd119c243ac9ff50f672ed0a9b819d3df57e3c3656c3beb7f0569cda3c5f4d78750aa25d57be36c4a5c2d8afb6bedd25e019de093978ed35e
7
+ data.tar.gz: 6b52d2878fea62ec642807a110b4e81517e915ab3c1b71a43f64e603fcb8688650775fc5be5008a7c0cd363b5292f277c670012d3e3bbc14c273610ab1c76e0e
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ Extract a UML diagram from Ruby code.
2
+
3
+ Usage: ruby lib/main.rb file directory ...
4
+
5
+ TODO:
6
+ add arguments to methods
7
+ add instance variables to classes
8
+ one-to-many aggregation and dependency
9
+ extend relationships
10
+ more advanced class and module resolution
11
+ convert project to gem
12
+ group classes and module in packages based on modules
13
+ include/exclude flag for modules/classes with no dependencies
data/bin/ruby2uml ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'yaml'
5
+
6
+ require_relative '../lib/directory_traverser'
7
+ require_relative '../lib/exploration/explorer_builder'
8
+ require_relative '../lib/graph_generator'
9
+ require_relative '../lib/sexp_factory'
10
+ require_relative '../lib/uml/dot_builder'
11
+
12
+ def run
13
+ config = YAML.load_file File.join(File.dirname(File.expand_path(__FILE__)), '../config/config.yml')
14
+ dot_config = YAML.load_file File.join(File.dirname(File.expand_path(__FILE__)), '../config/dot.yml')
15
+
16
+ options = {}
17
+ optparse = OptionParser.new do|opts|
18
+ opts.banner = "Usage: main.rb [options] file dir ..."
19
+
20
+ options[:verbose] = false
21
+ opts.on('-v', '--verbose', 'Display progress') do
22
+ options[:verbose] = true
23
+ end
24
+
25
+ opts.on('-h', '--help', 'Display this screen') do
26
+ puts opts
27
+ exit
28
+ end
29
+ end
30
+
31
+ optparse.parse!
32
+
33
+ if ARGV.length == 0
34
+ puts 'Must include a file name as an argument.'
35
+ exit
36
+ end
37
+
38
+ # TODO move the rest of the logic to lib
39
+
40
+ explore_file = lambda do |file_name|
41
+ file = File.open file_name, 'rb' # open file as binary to read into one string
42
+ program = file.read
43
+ SexpFactory.new.get_sexp program, 'rb'
44
+ end
45
+
46
+ paths = ARGV
47
+ traverser = DirectoryTraverser.new explore_file
48
+ generator = GraphGenerator.new
49
+ explorer = ExplorerBuilder.new.build_ruby_explorer
50
+ traverser.process(*paths) do |file, sexp|
51
+ generator.process_sexp explorer, sexp
52
+ end
53
+
54
+ graph = generator.graph
55
+
56
+ DotBuilder.new(config, dot_config).build_uml(graph)
57
+ end
58
+
59
+ puts run
data/config/config.yml ADDED
@@ -0,0 +1,6 @@
1
+ delimiter: "::"
2
+ exclude:
3
+ - Array
4
+ - Hash
5
+ - Set
6
+ - String
@@ -0,0 +1 @@
1
+ delimiter: ::
data/config/dot.yml ADDED
@@ -0,0 +1,5 @@
1
+ size: "\"10,10\""
2
+ node:
3
+ shape: record
4
+ style: filled
5
+ fillcolor: gray95
data/config/dot.yml~ ADDED
@@ -0,0 +1,5 @@
1
+ size: \"10,10\"
2
+ node:
3
+ shape: record
4
+ style: filled
5
+ fillcolor: gray95
@@ -0,0 +1,42 @@
1
+ require 'find'
2
+
3
+ class DirectoryTraverser
4
+ def initialize command, include=/.*/, verbose=false, out=$stdout, err=$stderr
5
+ @command = command
6
+ @include = include
7
+ @verbose = verbose
8
+ @out = out
9
+ @err = err
10
+ end
11
+
12
+ def process *paths, &block
13
+ paths.each do |path|
14
+ if FileTest.directory? path
15
+ process_dir path, &block
16
+ elsif FileTest.file? path
17
+ process_file path, &block unless path.match(@include).nil? # only process file if it matches @include
18
+ else
19
+ raise "#{path} must be a directory or file"
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+ def process_dir dir, &block
26
+ @out.puts "entering #{dir}" if @verbose
27
+ Find.find dir do |path|
28
+ process path, &block unless path == dir
29
+ end
30
+ end
31
+
32
+ def process_file file, &block
33
+ @out.puts "generating log for #{file}" if @verbose
34
+
35
+ #begin
36
+ result = @command.call file
37
+ # @out.puts "failed to generate log, trying again" if $? != 0
38
+ #end while $? != 0
39
+
40
+ yield file, result
41
+ end
42
+ end
@@ -0,0 +1,42 @@
1
+ require 'find'
2
+
3
+ class Logger
4
+ def initialize command, include=/.*/, verbose=false, out=$stdout, err=$stderr
5
+ @command = command
6
+ @include = include
7
+ @verbose = verbose
8
+ @out = out
9
+ @err = err
10
+ end
11
+
12
+ def process *paths, &block
13
+ paths.each do |path|
14
+ if FileTest.directory? path
15
+ process_dir path, &block
16
+ elsif FileTest.file? path
17
+ process_file path, &block unless path.match(@include).nil? # only process file if it matches @include
18
+ else
19
+ raise "#{path} must be a directory or file"
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+ def process_dir dir, &block
26
+ @out.puts "entering #{dir}" if @verbose
27
+ Find.find dir do |path|
28
+ process path, &block unless path == dir
29
+ end
30
+ end
31
+
32
+ def process_file file, &block
33
+ @out.puts "generating log for #{file}" if @verbose
34
+
35
+ begin
36
+ result = @command.call file
37
+ @out.puts "failed to generate log, trying again" if $? != 0
38
+ end while $? != 0
39
+
40
+ yield file, result
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'relation'
2
+
3
+ class AggregationRelation < Relation
4
+
5
+ def each sexp, context=nil, &block
6
+ each_by_type [:iasgn, :cvasgn], :aggregation, sexp, context, &block
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'entity'
2
+
3
+ # Explores any children of a :block element. The block element itself is not yielded as an entity only children.
4
+ class BlockEntity < Entity
5
+ def each sexp, context=nil, &block
6
+ if sexp.first == :block
7
+ sexp.each_sexp do |sub_sexp|
8
+ explore sub_sexp, context, &block
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'entity'
2
+
3
+ module Exploration
4
+ class ClassEntity < Entity
5
+ def each sexp, context=nil, &block
6
+ each_type sexp, :class, context, &block
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'entity'
2
+
3
+ class ClassEntity < Entity
4
+ def each sexp, context=nil, &block
5
+ each_type sexp, :class, context, &block
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'relation'
2
+
3
+ class DependencyRelation < Relation
4
+
5
+ def each sexp, context=nil, &block
6
+ each_by_type [:call], :dependency, sexp, context, &block
7
+ end
8
+ end
@@ -0,0 +1,64 @@
1
+ require_relative 'explorer'
2
+
3
+ class Entity < Explorer
4
+
5
+ def initialize
6
+ @explorers = Array.new
7
+ end
8
+
9
+ # exp is an Explorer object
10
+ def add_explorer exp
11
+ @explorers << exp
12
+ end
13
+
14
+ protected
15
+
16
+ # REFACTOR context should be its own class instead of a hash
17
+
18
+ # Explore each Sexp with type +type+ (ex. :class, :module).
19
+ def each_type sexp, type, context=nil, &block
20
+ # if nothing has been explored (i.e. no context) and the top element does not match what we are exploring, skip exploring this sexp
21
+ return if context.nil? && sexp.sexp_type != type
22
+
23
+ # if there is no context and top-level sexp matches type, then just explore that
24
+ if context == nil && sexp.first == type
25
+ yield_entity sexp, context, type, &block
26
+
27
+ # otherwise, explore the siblings of the sexp
28
+ else
29
+ sexp.each_sexp do |sub_sexp|
30
+ yield_entity sub_sexp, context, type, &block if sub_sexp.sexp_type == type
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Yields an individual Sexp and all of its Relations.
38
+ def yield_entity sexp, context, type, &block
39
+ name = get_entity_name sexp
40
+ namespace = get_namespace context
41
+ new_context = { name: name, namespace: namespace, type: type }
42
+ block.call new_context
43
+ explore sexp, new_context, &block
44
+ end
45
+
46
+ def explore sexp, context, &block
47
+ @explorers.each do |exp|
48
+ exp.each sexp, context, &block
49
+ end
50
+ end
51
+
52
+ def get_entity_name sexp
53
+ sexp.rest.head.to_s
54
+ end
55
+
56
+ def get_namespace context
57
+ namespace = Array.new
58
+ if context != nil
59
+ namespace = context[:namespace].dup if context.has_key? :namespace
60
+ namespace.push context[:name] if context.has_key? :name
61
+ end
62
+ namespace
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ # Contract: All classes that inherit Explorer must implement each(sexp, context=nil, &block) which yields entities (classes, modules, etc.) and their relationships among each other.
2
+ #
3
+ # == Explorer#each
4
+ # [arguments] - +sexp+ an Sexp object
5
+ # - +context+ a String representing the Entity that is calling each (default: nil)
6
+ #
7
+ # [yields] - name of entity
8
+ # - type of entity
9
+ # - relation (can be +nil+)
10
+ # - name of entity receiving relation (can be +nil+)
11
+ # - type of entity receiving relation (can be +nil+)
12
+ #
13
+ class Explorer
14
+ attr_writer :resolve_strategy
15
+ end
@@ -0,0 +1,58 @@
1
+ require_relative 'block_entity'
2
+ require_relative 'class_entity'
3
+ require_relative 'method_entity'
4
+ require_relative 'module_entity'
5
+ require_relative 'root_entity'
6
+ require_relative 'aggregation_relation'
7
+ require_relative 'dependency_relation'
8
+ require_relative 'implements_relation'
9
+ require_relative 'generalization_relation'
10
+
11
+ # All build methods must return an object that implements Explorable.
12
+ class ExplorerBuilder
13
+
14
+ # Builds Explorer for Ruby programs. This explorer picks up:
15
+ #
16
+ # * Classes
17
+ # * Generalization relationships
18
+ # * Aggregation relationships
19
+ # * Dependency relationships
20
+ # * Implementation relationships
21
+ # * Modules
22
+ # * Dependency relationships
23
+ # * Implementation relationships
24
+ def build_ruby_explorer
25
+ agg = AggregationRelation.new
26
+ dep = DependencyRelation.new
27
+ par = GeneralizationRelation.new
28
+ imp = ImplementsRelation.new
29
+
30
+ class_method_entity = MethodEntity.new
31
+ class_method_entity.add_explorer agg
32
+ class_method_entity.add_explorer dep
33
+
34
+ module_method_entity = MethodEntity.new
35
+ module_method_entity.add_explorer dep
36
+
37
+ class_entity = ClassEntity.new
38
+ class_entity.add_explorer class_method_entity
39
+ class_entity.add_explorer par
40
+ class_entity.add_explorer imp
41
+ module_entity = ModuleEntity.new
42
+ module_entity.add_explorer module_entity
43
+ module_entity.add_explorer class_entity
44
+ module_entity.add_explorer module_method_entity
45
+ module_entity.add_explorer imp
46
+
47
+ block_entity = BlockEntity.new
48
+ block_entity.add_explorer module_entity
49
+ block_entity.add_explorer class_entity
50
+
51
+ root = RootEntity.new
52
+ root.add_explorer class_entity
53
+ root.add_explorer module_entity
54
+ root.add_explorer block_entity
55
+
56
+ return root
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'relation'
2
+
3
+ class GeneralizationRelation < Relation
4
+
5
+ # Yields the parent of the +sexp+.
6
+ def each sexp, context=nil, &block
7
+ parent_node = sexp.rest.rest.head
8
+ if parent_node != nil # class has a parent
9
+ parent, namespace, explored = get_name_and_namespace parent_node
10
+ block.call context, :generalization, { name: parent, type: :class, namespace: namespace }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'relation'
2
+
3
+ class ImplementsRelation < Relation
4
+ def each sexp, context=nil, &block
5
+ sexp.each_sexp do |child|
6
+ # check if child is a call to include
7
+ if child.first == :call && child.rest.rest.first == :include
8
+ node = child.rest.rest.rest.first
9
+ name, namespace, explored = get_name_and_namespace node
10
+ block.call context, :implements, { name: name, type: :module, namespace: namespace }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'entity'
2
+
3
+ class MethodEntity < Entity
4
+
5
+ def each sexp, context=nil, &block
6
+ # NOTE does not handle file with only methods in it
7
+ sexp.each_sexp do |sub_sexp|
8
+ if [:defn, :defs].include? sub_sexp.sexp_type
9
+ # TODO handle arguments for methods
10
+ if sub_sexp.rest.first.kind_of? Sexp # TODO right now, we assume this is self, need to look into
11
+ name = sub_sexp.rest.rest.first.to_s
12
+ else
13
+ name = sub_sexp.rest.first.to_s
14
+ end
15
+ block.call context, :defines, { name: name, type: :method }
16
+ explore sub_sexp, context, &block
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'entity'
2
+
3
+ class ModuleEntity < Entity
4
+ def each sexp, context=nil, &block
5
+ each_type sexp, :module, context, &block
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Exploration
2
+ class MultiRelation < Relation
3
+
4
+ end
5
+ end
@@ -0,0 +1,73 @@
1
+ require_relative 'explorer'
2
+
3
+ class Relation < Explorer
4
+
5
+ # TODO these two methods are only used by aggregation and dependency, find a better place for them
6
+ def each_by_type types, relation, sexp, context=nil, &block
7
+ already_explored = []
8
+ sexp.deep_each do |sub_sexp|
9
+ if types.include? sub_sexp.sexp_type
10
+ # by exploring :colon2 first, we won't pick up any :const that was inside a :colon2
11
+ explore_entity_sexp :colon2, sexp, relation, context, already_explored, &block
12
+ explore_entity_sexp :const, sexp, relation, context, already_explored, &block
13
+ end
14
+ end
15
+ end
16
+
17
+ def explore_entity_sexp type, sexp, relationship, context, already_explored, &block
18
+ sexp.each_of_type(type) do |node|
19
+ if !already_explored.include?(node)
20
+ name, namespace, explored = get_name_and_namespace node
21
+ block.call context, relationship, { name: name, type: :class, namespace: namespace }
22
+ already_explored.concat explored
23
+ end
24
+ end
25
+ end
26
+
27
+ # Extracts name and namespace of the Sexp object.
28
+ #
29
+ # [params] - sexp is a Sexp object
30
+ #
31
+ # [precondition] - sexp is of the form: s(:colon2, s(:colon2 ..., s(:const, FirstNamespace), SecondNamespace, ..., ClassName).
32
+ # - :colon2 sexp's are optional but there must be a :const
33
+ #
34
+ # [return] - name as String
35
+ # - namespace as Array of String objects
36
+ # - explored Sexp objects as an Array
37
+ def get_name_and_namespace sexp
38
+ name = if sexp.first == :colon2
39
+ sexp.rest.rest.first.to_s
40
+ else
41
+ sexp.rest.first.to_s
42
+ end
43
+ namespace, explored = get_namespace sexp
44
+ explored << sexp
45
+ return name, namespace, explored
46
+ end
47
+
48
+ # Extracts the namespace from the Sexp object.
49
+ #
50
+ # [precondition] - sexp is of the form: s(:colon2, s(:colon2 ..., s(:const, FirstNamespace), SecondNamespace, ..., ClassName).
51
+ # - :colon2 sexp's are optional but there must be a :const
52
+ #
53
+ # [params] - sexp is a Sexp object
54
+ #
55
+ # [return] - Array of String objects
56
+ # - Array of Sexp objects that where encountered
57
+ #
58
+ # [example] - s(:colon2, s(:colon2, s(:const, :Baz), :Foo), :Bar) ==> ['Baz', 'Foo']
59
+ def get_namespace sexp
60
+ current = sexp
61
+ type = current.first
62
+ namespace = []
63
+ subs = []
64
+ while type == :colon2
65
+ namespace.unshift current.rest.rest.first.to_s
66
+ current = current.rest.first
67
+ subs << current
68
+ type = current.first
69
+ end
70
+ namespace.unshift current.rest.first.to_s
71
+ return namespace[0...namespace.length-1], subs # return all except for the last element
72
+ end
73
+ end
@@ -0,0 +1,44 @@
1
+ # Contract: All classes that inherit ResolveStrategy must implement +is_same?(v1, v2)+.
2
+ #
3
+ # == ResolveStrategy.is_same(v1, v2)
4
+ # [arguments] - +v1+ a Vertex object
5
+ # - +v2+ a Vertex object
6
+ #
7
+ class ResolveStrategy
8
+
9
+ # precondition: vertices contains more the one Vertex
10
+ # postcondition: the namespace of the merged vertex will be the namespace among vertices that is the longest
11
+ def merge_vertices *vertices
12
+ merged = vertices[0].dup
13
+ (1...vertices.length).each do |i|
14
+ v = vertices[i]
15
+ merged.namespace = v.namespace if v.namespace.count > merged.namespace.count
16
+ merged.paths.concat v.paths
17
+
18
+ v.each do |edge, set|
19
+ set.each do |o|
20
+ merged.add_edge edge, o
21
+ end
22
+ end
23
+ end
24
+ merged
25
+ end
26
+
27
+ # For all incoming edges, for all +vertices+, change the refernce from that vertex to the merged vertex. For example, if Foo depends on Bar, and Bar was merged into Merged then Foo should now reference Merged.
28
+ #
29
+ # postcondition: all incoming edges that reference a vertex in +vertices+ will now reference +merged+.
30
+ def rereference_incoming_edges! merged, *vertices
31
+ to_add = []
32
+ vertices.each do |vertex|
33
+ vertex.each_incoming do |edge, set|
34
+ set.each do |incoming|
35
+ incoming.remove_edge edge, vertex
36
+ to_add << [incoming, edge, merged]
37
+ end
38
+ end
39
+ end
40
+ to_add.each do |incoming, edge, merged|
41
+ incoming.add_edge edge, merged
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'resolve_strategy'
2
+
3
+ # The SimpleResolveStrategy assumes any vertex with the same name and a namespace that includes or is included by the other represents the same entity regardless of the namespace of the entity or paths where the entity is declared.
4
+ class SimpleResolveStrategy < ResolveStrategy
5
+
6
+ # Returns true if v1 and v2 represent the same vertex based on the Simple Resolve Strategy
7
+ # v1, v2 are Vertex objects
8
+ def is_same? v1, v2
9
+ v1.name.eql?(v2.name) && (
10
+ v1.namespace.is_included_by?(v2.namespace) ||
11
+ v1.namespace.does_include?(v2.namespace)
12
+ )
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ require_relative 'entity'
2
+
3
+ # A wrapper Entity to support multiple top-level entities in an explorer. Does not represent an actual Entity.
4
+ class RootEntity < Entity
5
+ def each sexp, context=nil, &block
6
+ explore sexp, context, &block
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ require 'set'
2
+
3
+ class Digraph
4
+ include Enumerable
5
+
6
+ def initialize
7
+ @vertices = Array.new
8
+ end
9
+
10
+ # Returns an Array of Vertices that match the name, namespace and path.
11
+ def find_vertex name, namespace=nil, paths=nil # REFACTOR consider changing this to a Vertex object
12
+ @vertices.select do |v|
13
+ v.name == name &&
14
+ (namespace.nil? || v.namespace.eql?(namespace)) &&
15
+ (paths.nil? || (paths - v.paths).empty?) # checks to see if paths is a subset of v.paths
16
+ end
17
+ end
18
+
19
+ # Returns true if self has a Vertex with the name +vertex_name+.
20
+ def has_vertex? vertex_name
21
+ !@vertices.select { |v| v.name == vertex_name }.empty?
22
+ end
23
+
24
+ def add_vertex vertex
25
+ @vertices.push vertex
26
+ vertex
27
+ end
28
+
29
+ def remove_vertex vertex
30
+ @vertices.delete vertex
31
+ end
32
+
33
+ def each &block
34
+ @vertices.each &block
35
+ end
36
+
37
+ def eql? obj
38
+ Set.new(self.vertices).eql? Set.new(obj.vertices)
39
+
40
+ end
41
+ alias_method :==, :eql?
42
+
43
+ protected
44
+ attr_reader :vertices # needed for eql?
45
+ public
46
+
47
+ def to_s
48
+ @vertices.map(&:to_s).join("\n")
49
+ end
50
+ end
data/lib/graph/edge.rb ADDED
@@ -0,0 +1,20 @@
1
+ class Edge
2
+ attr_accessor :type
3
+
4
+ def initialize type
5
+ @type = type
6
+ end
7
+
8
+ def eql? object
9
+ self.type.eql?(object.type)
10
+ end
11
+ alias_method :==, :eql?
12
+
13
+ def hash
14
+ @type.hash
15
+ end
16
+
17
+ def to_s
18
+ @type
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ require 'forwardable'
2
+
3
+ # Instances of Namespace are immutable.
4
+ class Namespace
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ def_delegators :@array, :join
9
+
10
+ def initialize array
11
+ @array = array || Array.new # do not want a nil array for namespace, better to have an empty array
12
+ end
13
+
14
+ # Checks if the Namespace could be included by the other Namespace.
15
+ # ex. B::C is included by A::B::C since B::C could belong to A
16
+ # other is a Namespace object
17
+ def is_included_by? other
18
+ return false if self.count > other.count
19
+ return true if self.eql? other
20
+ return other.drop(other.count - self.count).to_a.eql? self.to_a
21
+ end
22
+
23
+ # Checks if the Namespace include the other Namespace.
24
+ # ex. A::B::C includes B::C since B::C could belong to A
25
+ # other is a Namespace object
26
+ def does_include? other
27
+ return false if other.count > self.count
28
+ return true if self.eql? other
29
+ return self.drop(self.count - other.count).to_a.eql? other.to_a
30
+ end
31
+
32
+ def eql? other
33
+ @array.eql?(other.to_a) && self.class.eql?(other.class)
34
+ end
35
+ alias_method :==, :eql?
36
+
37
+ def hash
38
+ @array.hash
39
+ end
40
+
41
+ # postcondition: order will be preserved
42
+ def each &block
43
+ @array.each &block
44
+ end
45
+
46
+ def to_s
47
+ @array.join '::'
48
+ end
49
+ end
@@ -0,0 +1,89 @@
1
+ require 'set'
2
+
3
+ require_relative 'namespace'
4
+
5
+ class Vertex
6
+ include Enumerable
7
+
8
+ attr_accessor :name, :namespace, :paths, :type
9
+
10
+ def initialize name, type, namespace=[]
11
+ @name = name
12
+ @type = type
13
+ @namespace = Namespace.new namespace
14
+ @paths = Array.new
15
+
16
+ @outgoing = Hash.new
17
+ @incoming = Hash.new
18
+ end
19
+
20
+ def get_edge edge
21
+ @outgoing[edge] = Set.new if !@outgoing.has_key? edge
22
+ @outgoing[edge]
23
+ end
24
+ alias_method :[], :get_edge
25
+
26
+ def add_edge edge, vertex
27
+ @outgoing[edge] = Set.new if !@outgoing.has_key? edge
28
+ @outgoing[edge] << vertex
29
+
30
+ vertex.add_incoming_edge edge, self
31
+ end
32
+
33
+ def remove_edge edge, vertex
34
+ @outgoing[edge].delete vertex
35
+ vertex.remove_incoming_edge edge, self
36
+ end
37
+
38
+ def each &block
39
+ @outgoing.each &block
40
+ end
41
+ alias_method :each_outgoing, :each
42
+
43
+ def each_incoming &block
44
+ @incoming.each &block
45
+ end
46
+
47
+ def eql? obj
48
+ self.class.eql?(obj.class) &&
49
+ self.name.eql?(obj.name) &&
50
+ self.namespace.eql?(obj.namespace) &&
51
+ Set.new(self.each.to_a).eql?(Set.new(obj.each.to_a)) &&
52
+ self.paths.eql?(obj.paths) &&
53
+ self.type.eql?(obj.type)
54
+ end
55
+ alias_method :==, :eql?
56
+
57
+ def hash
58
+ @name.hash
59
+ end
60
+
61
+ def fully_qualified_name delimiter
62
+ ns = self.namespace.join delimiter
63
+ ns << delimiter if ns != ''
64
+ ns + self.name
65
+ end
66
+
67
+ def to_s
68
+ string = fully_qualified_name '::'
69
+ string += "~#{type}\n"
70
+ @outgoing.each do |edge, set|
71
+ string << "\t#{edge.to_s}: [ "
72
+ set.each do |v|
73
+ string << "#{v.namespace.to_s}#{'::' if v.namespace.to_s != ''}#{v.name.to_s},"
74
+ end
75
+ string << "]\n"
76
+ end
77
+ return string.chomp
78
+ end
79
+
80
+ protected
81
+ def add_incoming_edge edge, vertex
82
+ @incoming[edge] = Set.new if !@incoming.has_key? edge
83
+ @incoming[edge] << vertex
84
+ end
85
+
86
+ def remove_incoming_edge edge, vertex
87
+ @incoming[edge].delete vertex if @incoming.has_key? edge
88
+ end
89
+ end
@@ -0,0 +1,62 @@
1
+ require_relative 'exploration/resolve/simple_resolve_strategy'
2
+
3
+ require_relative 'graph/digraph'
4
+ require_relative 'graph/edge'
5
+ require_relative 'graph/namespace'
6
+ require_relative 'graph/vertex'
7
+
8
+ class GraphGenerator
9
+ attr_reader :graph
10
+ attr_writer :resolve_strategy
11
+
12
+ def initialize
13
+ @graph = Digraph.new
14
+ @resolve_strategy = SimpleResolveStrategy.new
15
+ end
16
+
17
+ # Traverse +sexp+ using the given +explorer+ and store the extracted relationships in the +graph+.
18
+ #
19
+ # [params] - explorer is an Explorable used for the traversal
20
+ # - sexp is a Sexp that is the subject of the traversal
21
+ def process_sexp explorer, sexp
22
+ explorer.each(sexp) do |entity, relation, other_entity|
23
+ vertex = get_or_create_vertex @graph, entity[:name], entity[:namespace], entity[:type]
24
+ if not relation.nil?
25
+ o_vertex = get_or_create_vertex @graph, other_entity[:name], other_entity[:namespace], other_entity[:type]
26
+ edge = get_edge relation
27
+ vertex.add_edge edge, o_vertex
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def get_or_create_vertex graph, name, namespace, type
35
+ vertex = nil
36
+ if graph.has_vertex? name
37
+ vertices = graph.find_vertex(name)
38
+ found_vertex = vertices.first
39
+ new_vertex = create_vertex(name, namespace, type)
40
+ if @resolve_strategy.is_same?(found_vertex, new_vertex)
41
+ vertex = @resolve_strategy.merge_vertices(found_vertex, new_vertex)
42
+ @resolve_strategy.rereference_incoming_edges!(vertex, found_vertex, new_vertex)
43
+ graph.remove_vertex found_vertex
44
+ graph.remove_vertex new_vertex
45
+ else
46
+ vertex = new_vertex
47
+ end
48
+ else
49
+ vertex = create_vertex(name, namespace, type)
50
+ end
51
+ graph.add_vertex vertex
52
+ return vertex
53
+ end
54
+
55
+ def create_vertex name, namespace, type
56
+ Vertex.new name, type, namespace
57
+ end
58
+
59
+ def get_edge type
60
+ Edge.new type
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ require 'ruby_parser'
2
+
3
+ # SexpFactory generates a Sexp object from a given file and for a given type. The only supported type so far is Ruby (.rb) files.
4
+ class SexpFactory
5
+
6
+ def initialize
7
+ @ext_map = Hash.new
8
+ rb_parser = RubyParser.new
9
+ @ext_map['rb'] = lambda { |program| rb_parser.parse program }
10
+ end
11
+
12
+ # Returns a Sexp representation of the program
13
+ # = +Sexp+
14
+ # Params:
15
+ # +program+:: string containing a program to be converted to s-expression
16
+ # +ext+:: string for file extensions of the program
17
+ def get_sexp program, ext
18
+ @ext_map[ext].call program
19
+ end
20
+ end
@@ -0,0 +1,84 @@
1
+ require_relative 'uml_builder'
2
+
3
+ # DotBuilder constructs a String in .dot format from a Digraph to be used with the Graphviz Dot program.
4
+ class DotBuilder
5
+ include UmlBuilder
6
+
7
+ def node_beginning vertex
8
+ id = @vertex_to_id[vertex]
9
+ name = vertex.fully_qualified_name(@config["delimiter"]).chomp("?").chomp("!") # TODO find a better solution than removing question marks and bangs from methods
10
+ "#{id}[label = \"{#{name}|"
11
+ end
12
+
13
+ def node_ending
14
+ "}\"]\n"
15
+ end
16
+
17
+ def initialize config={}, dot_config={}
18
+ super(config)
19
+ @dot_config = dot_config
20
+
21
+ @id_counter = 0
22
+ @vertex_to_id = Hash.new
23
+
24
+ def get_methods vertex
25
+ methods = vertex.get_edge(Edge.new(:defines))
26
+ return '...' if methods.empty?
27
+ methods.to_a.map(&:name).join('\n').chomp('\n') # TODO need to figure out how to add new lines correctly
28
+ end
29
+
30
+ @vertex_mappings = Hash.new lambda { |*| '' }
31
+ @vertex_mappings[:module] = lambda do |moduel|
32
+ node_beginning(moduel) +
33
+ get_methods(moduel) +
34
+ node_ending
35
+ end
36
+ @vertex_mappings[:class] = lambda do |klass|
37
+ node_beginning(klass) +
38
+ "...|" + get_methods(klass) +
39
+ node_ending
40
+ end
41
+
42
+ @edge_mappings = Hash.new lambda { |*| '' }
43
+ @edge_mappings[:generalization] = lambda do |child, parent|
44
+ return "#{parent}->#{child}[arrowtail=empty, dir=back]"
45
+ end
46
+ @edge_mappings[:implements] = lambda do |impl, type|
47
+ return "#{type}->#{impl}[arrowtail=empty, dir=back, style=dashed]"
48
+ end
49
+ @edge_mappings[:aggregation] = lambda do |aggregator, aggregate|
50
+ return "#{aggregator}->#{aggregate}[arrowtail=odiamond, constraint=false, dir=back]"
51
+ end
52
+ @edge_mappings[:dependency] = lambda do |vertex, depends_on|
53
+ return "#{vertex}->#{depends_on}[dir=forward, style=dashed]"
54
+ end
55
+ end
56
+
57
+ def build_header
58
+ header = "digraph hierarchy {\n"
59
+ header << "size=#{@dot_config["size"]}\n" if @dot_config.has_key? "size"
60
+ if @dot_config.has_key? "node"
61
+ header << "node["
62
+ @dot_config["node"].each do |setting_name, setting_value|
63
+ header << "#{setting_name}=#{setting_value}, "
64
+ end
65
+ header.chomp!(", ")
66
+ header << "]\n"
67
+ end
68
+ header
69
+ end
70
+
71
+ def build_entity vertex
72
+ @id_counter += 1
73
+ @vertex_to_id[vertex] = @id_counter
74
+ @vertex_mappings[vertex.type].call(vertex)
75
+ end
76
+
77
+ def build_relation vertex, edge, o_vertex
78
+ @edge_mappings[edge].call(@vertex_to_id[vertex], @vertex_to_id[o_vertex]) + "\n"
79
+ end
80
+
81
+ def build_footer
82
+ "}"
83
+ end
84
+ end
@@ -0,0 +1,31 @@
1
+ # Contract:
2
+ # Classes that include UmlBuilder must implement the following methods:
3
+ # * build_entity(vertex)
4
+ # * build_relation(vertex, edge, vertex)
5
+ module UmlBuilder
6
+
7
+ def initialize config={}
8
+ @config = config
9
+ @exclude = config['exclude'] || []
10
+ end
11
+
12
+ def build_uml graph
13
+ content = ''
14
+ content << build_header if self.respond_to? :build_header
15
+ graph.each do |vertex|
16
+ content << build_entity(vertex) unless @exclude.include? vertex.name
17
+ end
18
+
19
+ graph.each do |vertex|
20
+ vertex.each do |edge, set|
21
+ unless @exclude.include? vertex.name
22
+ set.each do |o_vertex|
23
+ content << build_relation(vertex, edge.type, o_vertex) unless @exclude.include? o_vertex.name
24
+ end
25
+ end
26
+ end
27
+ end
28
+ content << build_footer if self.respond_to? :build_footer
29
+ content
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby2uml
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Michael Dalton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 3.1.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 3.1.3
27
+ description: ruby2uml generates a UML Class diagram from Ruby source code.
28
+ email: michaelcdalton@gmail.com
29
+ executables:
30
+ - ruby2uml
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/graph/vertex.rb
35
+ - lib/graph/namespace.rb
36
+ - lib/graph/edge.rb
37
+ - lib/graph/digraph.rb
38
+ - lib/graph_generator.rb
39
+ - lib/directory_traverser.rb
40
+ - lib/sexp_factory.rb
41
+ - lib/exploration/dependency_relation.rb
42
+ - lib/exploration/module_entity.rb
43
+ - lib/exploration/resolve/resolve_strategy.rb
44
+ - lib/exploration/resolve/simple_resolve_strategy.rb
45
+ - lib/exploration/entity.rb
46
+ - lib/exploration/block_entity.rb
47
+ - lib/exploration/class_entity.rb
48
+ - lib/exploration/multi_relation.rb~
49
+ - lib/exploration/explorer.rb
50
+ - lib/exploration/aggregation_relation.rb
51
+ - lib/exploration/root_entity.rb
52
+ - lib/exploration/method_entity.rb
53
+ - lib/exploration/relation.rb
54
+ - lib/exploration/explorer_builder.rb
55
+ - lib/exploration/generalization_relation.rb
56
+ - lib/exploration/block_entity.rb~
57
+ - lib/exploration/implements_relation.rb
58
+ - lib/uml/dot_builder.rb
59
+ - lib/uml/uml_builder.rb
60
+ - lib/directory_traverser.rb~
61
+ - config/dot.yml
62
+ - config/dot.yml~
63
+ - config/config.yml~
64
+ - config/config.yml
65
+ - README.md
66
+ - bin/ruby2uml
67
+ homepage: http://github.com/kcdragon/ruby2uml
68
+ licenses: []
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.0.3
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: UML Class Diagram Generation
90
+ test_files: []