Skip to content

Commit 8d37a7c

Browse files
Implement visualise_cyclic_deps
1 parent 8d540a9 commit 8d37a7c

11 files changed

+101
-9
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ Here's a component include dependency visualisation generated for the `queue` co
7474

7575
![Queue include graph d3](examples/rethinkdb_queue_include_d3.svg)
7676

77+
### Cyclic dependencies only graph
78+
79+
This will highlight cyclic dependencies between components within a project. This is especially useful for targeted refactoring activities to reduce coupling between components.
80+
81+
`cpp_dependency_graph visualise_cyclic_deps -r spec\test\example_project\`
82+
83+
Here's the cyclic dependencies only visualisation generated for [rethinkdb](https://github.com/rethinkdb/rethinkdb) and [leveldb](https://github.com/google/leveldb)
84+
85+
![rethinkdb](examples/rethinkdb_cyclic_deps.svg)
86+
87+
![leveldb](examples/leveldb_cyclic_deps.svg)
88+
7789
## Development
7890

7991
`bundle exec cpp_dependency_graph visualise -r <dir>`

TODO.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
- [ ] Documentation
44
- [ ] Screencast of how to use tool on projects
55
- [ ] Create a github.io homepage
6+
- [ ] Add interactive `html` or `svg` examples to homepage
67
- [ ] Add a [CONTRIBUTING.md](https://github.com/nayafia/contributing-template/blob/master/CONTRIBUTING-template.md)
7-
- [ ] Add interactive `html` or `svg` examples into README, is this possible?
88
- [ ] Progress messages
99
- [ ] Progress bar?
1010
- [x] Allow user to specify a single component and the tool should print only that component
@@ -13,6 +13,7 @@
1313
- [ ] Manual scanning does not work when #includes are #ifdefed out for example
1414
- [ ] <system> include vs "project" include (how will this work for third party sources that are not part of the codebase?)
1515
- [ ] Work with any type of include relative includes ('blah.h') vs absolute includes ('/path/blah.h') vs relative with path includes ('blah/blah.h')
16+
- [ ] Look at https://github.com/Sarcasm/compdb to see if it can generate dependencies
1617
- [ ] Parallelise dependency scanning as much as possible to get the best possible performance
1718
- [ ] Open up the graph automatically after generating an individual graph
1819
- [ ] Work with any sort of project structure

examples/leveldb_cyclic_deps.svg

+1
Loading

examples/rethinkdb_cyclic_deps.svg

+1
Loading

exe/cpp_dependency_graph

+4-4
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ begin
5858
Kernel.exit(0)
5959
end
6060

61-
unless File.directory?(args['--root_dir'])
61+
project_dir = args['--root_dir'].gsub(/\\/,'/')
62+
63+
unless File.directory?(project_dir)
6264
puts('Not a valid project source directory')
6365
Kernel.exit(1)
6466
end
6567

66-
project_dir = args['--root_dir'].gsub(/\\/,'/')
67-
6868
if args['visualise']
6969
if args['--component']
7070
generate_component_graph(project_dir, args['--component'], args['--output_format'], args['--output_file'])
@@ -76,7 +76,7 @@ begin
7676
elsif args['visualise_components']
7777
# TODO: Output components
7878
elsif args['visualise_cyclic_deps']
79-
output_cyclic_dependencies(project_dir)
79+
generate_cyclic_dependencies(project_dir, args['--output_format'], args['--output_file'])
8080
end
8181
rescue Docopt::Exit => e
8282
puts e.message

lib/cpp_dependency_graph.rb

+5-4
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ def generate_component_include_graph(project_dir, component_name, format, output
2929
generate_visualisation(deps, format, output_file)
3030
end
3131

32-
def output_cyclic_dependencies(project_dir)
32+
def generate_cyclic_dependencies(project_dir, format, file)
3333
project = Project.new(project_dir)
3434
graph = DependencyGraph.new(project)
35-
deps = graph.all_component_links
36-
cyclic_deps = deps.values.flatten.select { |dep| dep.cyclic? }
37-
puts JSON.pretty_generate(cyclic_deps)
35+
deps = graph.all_cyclic_links
36+
generate_visualisation(deps, format, file)
3837
end
3938

4039
def generate_visualisation(deps, format, file)
@@ -43,6 +42,8 @@ def generate_visualisation(deps, format, file)
4342
GraphVisualiser.new.generate_dot_file(deps, file)
4443
when 'html'
4544
GraphVisualiser.new.generate_html_file(deps, file)
45+
when 'json'
46+
File.write(file, JSON.pretty_generate(deps))
4647
end
4748
end
4849
end

lib/cpp_dependency_graph/component_link.rb

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ def cyclic?
2121
@cyclic
2222
end
2323

24+
def ==(other)
25+
(source == other.source && target == other.target && cyclic? == other.cyclic?) ||
26+
(source == other.target && target == other.source && cyclic? == other.cyclic?)
27+
end
28+
29+
def hash
30+
[source, target, cyclic?].to_set.hash
31+
end
32+
2433
def to_s
2534
if cyclic?
2635
"#{source} <-> #{target}"

lib/cpp_dependency_graph/dependency_graph.rb

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def all_component_links
1818
@all_component_links ||= build_hash_component_links
1919
end
2020

21+
def all_cyclic_links
22+
@cyclic_links ||= build_cyclic_links
23+
end
24+
2125
def component_links(name)
2226
return {} unless all_component_links.key?(name)
2327
incoming_links(name).merge(outgoing_links(name))
@@ -35,6 +39,11 @@ def build_hash_component_links
3539
component_links
3640
end
3741

42+
def build_cyclic_links
43+
cyclic_links = all_component_links.select { |_, links| links.any? { |link| link.cyclic? } }
44+
cyclic_links.each { |_, links| links.select! { |link| link.cyclic? } }
45+
end
46+
3847
def outgoing_links(name)
3948
all_component_links.slice(name)
4049
end

spec/test/component_link_spec.rb

+12
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,16 @@
1616
it 'returns correct cyclic? attribute' do
1717
expect(ComponentLink.new('source', 'target', true).cyclic?).to eq(true)
1818
end
19+
20+
it 'returns equal if another instance has same attributes' do
21+
c1 = ComponentLink.new('source', 'target', true)
22+
c2 = ComponentLink.new('target', 'source', true)
23+
expect(c1). to eq(c2)
24+
end
25+
26+
it 'returns not equal if another instance has different attributes' do
27+
c1 = ComponentLink.new('source1', 'target1', true)
28+
c2 = ComponentLink.new('source2', 'target2', true)
29+
expect(c1).to_not eq(c2)
30+
end
1931
end

spec/test/cyclic_link_spec.rb

+9
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,13 @@
88
it 'returns the same hash for two equivalent objects with the same nodes' do
99
expect(CyclicLink.new('nodeA', 'nodeB').hash).to eq(CyclicLink.new('nodeB', 'nodeA').hash)
1010
end
11+
12+
it 'implements eql? operator correctly for usage as a hash key' do
13+
h = {}
14+
c1 = CyclicLink.new('nodeA', 'nodeB')
15+
c2 = CyclicLink.new('nodeB', 'nodeA')
16+
h[c1] = true
17+
h[c2] = false
18+
expect(h[c1]).to eq(false)
19+
end
1120
end

spec/test/dependency_graph_spec.rb

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
require 'cpp_dependency_graph/dependency_graph'
2+
require 'cpp_dependency_graph/component_link'
3+
4+
RSpec.describe DependencyGraph do
5+
let(:project) { Project.new('spec/test/example_project') }
6+
let(:dependency_graph) { DependencyGraph.new(project) }
7+
8+
it 'returns all links for a project' do
9+
expected_links = {}
10+
expected_links['UI'] = [ComponentLink.new('UI', 'Framework', false),
11+
ComponentLink.new('UI', 'Engine', true)]
12+
expected_links['DataAccess'] = [ComponentLink.new('DataAccess', 'Framework', false)]
13+
expected_links['main'] = [ComponentLink.new('main', 'UI', false)]
14+
expected_links['Framework'] = []
15+
expected_links['System'] = []
16+
expected_links['Engine'] = [ComponentLink.new('Engine', 'Framework', false),
17+
ComponentLink.new('Engine', 'UI', true),
18+
ComponentLink.new('Engine', 'DataAccess', false)]
19+
expect(dependency_graph.all_component_links).to eq(expected_links)
20+
end
21+
22+
it 'returns links for a specified component of a project' do
23+
expected_links = {}
24+
expected_links['UI'] = [ComponentLink.new('UI', 'Engine', false)] # TODO
25+
expected_links['Engine'] = [ComponentLink.new('Engine', 'Framework', false),
26+
ComponentLink.new('Engine', 'UI', true),
27+
ComponentLink.new('Engine', 'DataAccess', false)]
28+
expect(dependency_graph.component_links('Engine')).to eq(expected_links)
29+
end
30+
31+
it 'returns all cyclic links of a project' do
32+
expected_links = {}
33+
expected_links['Engine'] = [ComponentLink.new('Engine', 'UI', true)]
34+
expected_links['UI'] = [ComponentLink.new('UI', 'Engine', true)]
35+
expect(dependency_graph.all_cyclic_links).to eq(expected_links)
36+
end
37+
end

0 commit comments

Comments
 (0)