ruby2uml 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +13 -0
- data/bin/ruby2uml +59 -0
- data/config/config.yml +6 -0
- data/config/config.yml~ +1 -0
- data/config/dot.yml +5 -0
- data/config/dot.yml~ +5 -0
- data/lib/directory_traverser.rb +42 -0
- data/lib/directory_traverser.rb~ +42 -0
- data/lib/exploration/aggregation_relation.rb +8 -0
- data/lib/exploration/block_entity.rb +12 -0
- data/lib/exploration/block_entity.rb~ +9 -0
- data/lib/exploration/class_entity.rb +7 -0
- data/lib/exploration/dependency_relation.rb +8 -0
- data/lib/exploration/entity.rb +64 -0
- data/lib/exploration/explorer.rb +15 -0
- data/lib/exploration/explorer_builder.rb +58 -0
- data/lib/exploration/generalization_relation.rb +13 -0
- data/lib/exploration/implements_relation.rb +14 -0
- data/lib/exploration/method_entity.rb +20 -0
- data/lib/exploration/module_entity.rb +7 -0
- data/lib/exploration/multi_relation.rb~ +5 -0
- data/lib/exploration/relation.rb +73 -0
- data/lib/exploration/resolve/resolve_strategy.rb +44 -0
- data/lib/exploration/resolve/simple_resolve_strategy.rb +14 -0
- data/lib/exploration/root_entity.rb +8 -0
- data/lib/graph/digraph.rb +50 -0
- data/lib/graph/edge.rb +20 -0
- data/lib/graph/namespace.rb +49 -0
- data/lib/graph/vertex.rb +89 -0
- data/lib/graph_generator.rb +62 -0
- data/lib/sexp_factory.rb +20 -0
- data/lib/uml/dot_builder.rb +84 -0
- data/lib/uml/uml_builder.rb +31 -0
- metadata +90 -0
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
data/config/config.yml~
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
delimiter: ::
|
data/config/dot.yml
ADDED
data/config/dot.yml~
ADDED
@@ -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,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,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,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,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,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
|
data/lib/graph/vertex.rb
ADDED
@@ -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
|
data/lib/sexp_factory.rb
ADDED
@@ -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: []
|