ruby2uml 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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: []