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 +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: []
|