Skip to content

Commit 58f4dd0

Browse files
Add template for component d3 circle packing view
1 parent 575d54b commit 58f4dd0

22 files changed

+430
-141
lines changed

Diff for: .travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dist: trusty
33
cache: bundler
44
sudo: false
55
rvm:
6+
- 2.4
67
- 2.5
78
before_install:
89
- gem update --system

Diff for: .vscode/launch.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"pathToBundler": "C:/tools/ruby25/bin/bundle.bat",
2525
"pathToRDebugIDE": "C:/tools/ruby25/bin/rdebug-ide.bat",
2626
"showDebuggerOutput": true,
27-
"args": ["visualise", "-r", "../TileDB", "-f", "dot"]
27+
"args": ["visualise_components", "-r", "../TileDB"]
2828
},
2929
{
3030
"name": "Listen for rdebug-ide",

Diff for: README.md

+10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ The pretty `d3` visualisations are directly copied from `objc-dependency-visuali
2929

3030
A huge shout out to the people behind these projects.
3131

32+
## Comparison With Other Tools
33+
34+
### cpp-dependencies
35+
36+
### cinclude2dot
37+
38+
### dep-matrix
39+
40+
41+
3242
## Usage
3343

3444
### Installation

Diff for: TODO.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
- [x] Allow user to specify a single component and the tool should print only that component
1313
- [x] Print out dependency graphs for all components by default if no single component is specified
1414
- [ ] Use a compiler to get the includes for a file rather than manually scanning the contents
15+
- [ ] Command to generate overall file/class dependency graph if a project doesn't have many components
16+
- [ ] Command to generate individual file/class dependency graph
1517
- [ ] Manual scanning does not work when #includes are #ifdefed out for example
1618
- [ ] <system> include vs "project" include (how will this work for third party sources that are not part of the codebase?)
1719
- [ ] Work with any type of include relative includes ('blah.h') vs absolute includes ('/path/blah.h') vs relative with path includes ('blah/blah.h')

Diff for: exe/cpp_dependency_graph

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ begin
7676
elsif args['visualise_includes']
7777
generate_component_include_graph(project_dir, args['--component'], args['--output_format'], args['--output_file'])
7878
elsif args['visualise_components']
79-
# TODO: Output components
79+
generate_enclosure_diagram(project_dir, args['--output_file'])
8080
elsif args['visualise_cyclic_deps']
8181
generate_cyclic_dependencies(project_dir, args['--output_format'], args['--output_file'])
8282
end

Diff for: lib/cpp_dependency_graph.rb

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,46 @@
11
# frozen_string_literal: true
22

3-
require_relative 'cpp_dependency_graph/project'
4-
require_relative 'cpp_dependency_graph/dependency_graph'
5-
require_relative 'cpp_dependency_graph/include_dependency_graph'
3+
require_relative 'cpp_dependency_graph/circle_packing_visualiser'
4+
require_relative 'cpp_dependency_graph/component_dependency_graph'
5+
require_relative 'cpp_dependency_graph/dir_tree'
66
require_relative 'cpp_dependency_graph/graph_to_dot_visualiser'
77
require_relative 'cpp_dependency_graph/graph_to_html_visualiser'
8+
require_relative 'cpp_dependency_graph/include_dependency_graph'
9+
require_relative 'cpp_dependency_graph/project'
810
require_relative 'cpp_dependency_graph/version'
911

1012
# Generates dependency graphs of a project in various output forms
1113
module CppDependencyGraph
1214
def generate_project_graph(project_dir, format, output_file)
1315
project = Project.new(project_dir)
14-
graph = DependencyGraph.new(project)
15-
deps = graph.all_component_links
16+
graph = ComponentDependencyGraph.new(project)
17+
deps = graph.all_links
1618
generate_visualisation(deps, format, output_file)
1719
end
1820

1921
def generate_component_graph(project_dir, component, format, output_file)
2022
project = Project.new(project_dir)
21-
graph = DependencyGraph.new(project)
22-
deps = graph.component_links(component)
23+
graph = ComponentDependencyGraph.new(project)
24+
deps = graph.links(component)
2325
generate_visualisation(deps, format, output_file)
2426
end
2527

2628
def generate_component_include_graph(project_dir, component_name, format, output_file)
2729
project = Project.new(project_dir)
2830
graph = IncludeDependencyGraph.new(project)
29-
deps = graph.include_links(component_name)
31+
deps = graph.links(component_name)
3032
generate_visualisation(deps, format, output_file)
3133
end
3234

35+
def generate_enclosure_diagram(project_dir, output_file)
36+
dir_tree = DirTree.new(project_dir)
37+
tree = dir_tree.tree
38+
CirclePackingVisualiser.new.generate(tree, output_file)
39+
end
40+
3341
def generate_cyclic_dependencies(project_dir, format, file)
3442
project = Project.new(project_dir)
35-
graph = DependencyGraph.new(project)
43+
graph = ComponentDependencyGraph.new(project)
3644
deps = graph.all_cyclic_links
3745
generate_visualisation(deps, format, file)
3846
end
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
# Outputs a `d3 force graph layout` equipped HTML representation of a dependency graph
6+
class CirclePackingVisualiser
7+
def generate(tree, file)
8+
json_tree = JSON.pretty_generate(tree)
9+
template_file = resolve_file_path('views/circle_packing.html.template')
10+
template = File.read(template_file)
11+
contents = format(template, tree: json_tree)
12+
File.write(file, contents)
13+
end
14+
15+
private
16+
17+
def resolve_file_path(path)
18+
File.expand_path("../../../#{path}", __FILE__)
19+
end
20+
end
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'project'
4+
require_relative 'link'
5+
require_relative 'cycle_detector'
6+
7+
# Dependency tree/graph of entire project
8+
class ComponentDependencyGraph
9+
def initialize(project)
10+
@project = project
11+
end
12+
13+
def all_links
14+
@all_links ||= build_hash_links
15+
end
16+
17+
def all_cyclic_links
18+
@all_cyclic_links ||= build_cyclic_links
19+
end
20+
21+
def links(name)
22+
return {} unless all_links.key?(name)
23+
links = incoming_links(name)
24+
links.merge!(outgoing_links(name))
25+
links
26+
end
27+
28+
private
29+
30+
def build_hash_links
31+
raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c)] }.to_h
32+
@cycle_detector ||= CycleDetector.new(raw_links)
33+
links = raw_links.map do |source, source_links|
34+
c_links = source_links.map { |target| Link.new(source, target, @cycle_detector.cyclic?(source, target)) }
35+
[source, c_links]
36+
end.to_h
37+
links
38+
end
39+
40+
def build_cyclic_links
41+
cyclic_links = all_links.select { |_, links| links.any?(&:cyclic?) }
42+
cyclic_links.each_value { |links| links.select!(&:cyclic?) }
43+
end
44+
45+
def outgoing_links(name)
46+
all_links.slice(name)
47+
end
48+
49+
def incoming_links(target)
50+
incoming_c_links = all_links.select { |_, c_links| c_links.any? { |link| link.target == target } }
51+
incoming_c_links.map do |source, _|
52+
link = Link.new(source, target, @cycle_detector.cyclic?(source, target))
53+
[source, [link]]
54+
end.to_h
55+
end
56+
end

Diff for: lib/cpp_dependency_graph/cycle_detector.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
require_relative 'cyclic_link'
44

5-
# Detects cycles between components
5+
# Detects cycles between nodes
66
class CycleDetector
7-
def initialize(component_links)
8-
@cyclic_links = component_links.flat_map do |source, links|
9-
links.select { |target| component_links[target].include?(source) }.map do |target|
7+
def initialize(links)
8+
@cyclic_links = links.flat_map do |source, source_links|
9+
source_links.select { |target| links[target].include?(source) }.map do |target|
1010
[CyclicLink.new(source, target), true]
1111
end
1212
end.to_h

Diff for: lib/cpp_dependency_graph/cyclic_link.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

3-
# A special class designed to be used as a key in a hash
3+
require 'set'
4+
5+
# Represents a cyclic link betwee two nodes, it is special in the sense it is designed to be used as a key in a hash
46
class CyclicLink
57
attr_reader :node_a
68
attr_reader :node_b

Diff for: lib/cpp_dependency_graph/dependency_graph.rb

-60
This file was deleted.

Diff for: lib/cpp_dependency_graph/dir_tree.rb

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
# Returns a root directory as a tree of directories
6+
class DirTree
7+
attr_reader :tree
8+
9+
def initialize(path)
10+
@tree ||= File.directory?(path) ? parse_dirs(path) : {}
11+
end
12+
13+
private
14+
15+
def parse_dirs(path, name = nil)
16+
data = {}
17+
data[:name] = (name || path)
18+
data[:children] = children = []
19+
# TODO: Use Dir.map.compact|filter instead here
20+
Dir.foreach(path) do |entry|
21+
next if ['..', '.'].include?(entry)
22+
full_path = File.join(path, entry)
23+
next unless File.directory?(full_path)
24+
children << parse_dirs(full_path, entry)
25+
end
26+
data
27+
end
28+
29+
def to_s
30+
@tree.to_json
31+
end
32+
end

Diff for: lib/cpp_dependency_graph/file_dependency_graph.rb

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'project'
4+
require_relative 'link'
5+
require_relative 'cycle_detector'
6+
7+
# Dependency tree/graph of entire project
8+
class FileDependencyGraph
9+
def initialize(project)
10+
@project = project
11+
end
12+
13+
def all_links
14+
@all_links ||= build_hash_links
15+
end
16+
17+
def all_cyclic_links
18+
@all_cyclic_links ||= build_cyclic_links
19+
end
20+
21+
def links(name)
22+
return {} unless all_links.key?(name)
23+
links = incoming_links(name)
24+
links.merge!(outgoing_links(name))
25+
links
26+
end
27+
28+
private
29+
30+
def build_hash_links
31+
raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c)] }.to_h
32+
@cycle_detector ||= CycleDetector.new(raw_links)
33+
links = raw_links.map do |source, source_links|
34+
c_links = source_links.map { |target| Link.new(source, target, @cycle_detector.cyclic?(source, target)) }
35+
[source, c_links]
36+
end.to_h
37+
links
38+
end
39+
40+
def build_cyclic_links
41+
cyclic_links = all_links.select { |_, links| links.any?(&:cyclic?) }
42+
cyclic_links.each_value { |links| links.select!(&:cyclic?) }
43+
end
44+
45+
def outgoing_links(name)
46+
all_links.slice(name)
47+
end
48+
49+
def incoming_links(target)
50+
incoming_c_links = all_links.select { |_, c_links| c_links.any? { |link| link.target == target } }
51+
incoming_c_links.map do |source, _|
52+
link = Link.new(source, target, @cycle_detector.cyclic?(source, target))
53+
[source, [link]]
54+
end.to_h
55+
end
56+
end
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
# Outputs a `graphml` langugage representation of a dependency graph
4+
class GraphToGraphmlVisualiser
5+
def generate(deps, file)
6+
# TODO: Implement
7+
end
8+
end

0 commit comments

Comments
 (0)