cpp_dependency_graph 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.vscode/launch.json +79 -0
- data/.vscode/tasks.json +12 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/Rakefile +9 -0
- data/TODO.md +38 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cpp_dependency_graph.gemspec +44 -0
- data/examples/leveldb_overall.png +0 -0
- data/examples/leveldb_overall.svg +215 -0
- data/examples/leveldb_overall_d3.png +0 -0
- data/examples/leveldb_overall_d3.svg +1 -0
- data/examples/rethinkdb_queue_component.png +0 -0
- data/examples/rethinkdb_queue_component.svg +234 -0
- data/examples/rethinkdb_queue_component_d3.png +0 -0
- data/examples/rethinkdb_queue_component_d3.svg +1 -0
- data/examples/rethinkdb_queue_include.png +0 -0
- data/examples/rethinkdb_queue_include.svg +211 -0
- data/examples/rethinkdb_queue_include_d3.png +0 -0
- data/examples/rethinkdb_queue_include_d3.svg +1 -0
- data/examples/rocksdb_overall_d3.svg +1 -0
- data/examples/rocksdb_table_component.png +0 -0
- data/exe/cpp_dependency_graph +87 -0
- data/lib/cpp_dependency_graph/bidirectional_hash.rb +30 -0
- data/lib/cpp_dependency_graph/component_link.rb +37 -0
- data/lib/cpp_dependency_graph/cycle_detector.rb +25 -0
- data/lib/cpp_dependency_graph/cyclic_link.rb +30 -0
- data/lib/cpp_dependency_graph/dependency_graph.rb +49 -0
- data/lib/cpp_dependency_graph/graph_to_html_converter.rb +7 -0
- data/lib/cpp_dependency_graph/graph_visualiser.rb +59 -0
- data/lib/cpp_dependency_graph/include_dependency_graph.rb +24 -0
- data/lib/cpp_dependency_graph/project.rb +66 -0
- data/lib/cpp_dependency_graph/source_component.rb +37 -0
- data/lib/cpp_dependency_graph/source_file.rb +51 -0
- data/lib/cpp_dependency_graph/tsortable_hash.rb +12 -0
- data/lib/cpp_dependency_graph/version.rb +5 -0
- data/lib/cpp_dependency_graph.rb +48 -0
- data/views/index.html.template +990 -0
- metadata +258 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'cyclic_link'
|
4
|
+
|
5
|
+
# Detects cycles between components
|
6
|
+
class CycleDetector
|
7
|
+
def initialize(component_links)
|
8
|
+
@cyclic_links = {}
|
9
|
+
component_links.each do |source, links|
|
10
|
+
links.each do |target|
|
11
|
+
links_of_target = component_links[target]
|
12
|
+
@cyclic_links[CyclicLink.new(source, target)] = true if links_of_target.include?(source)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def cyclic?(source, target)
|
18
|
+
k = CyclicLink.new(source, target)
|
19
|
+
@cyclic_links.key?(k)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
@cyclic_links.each { |k, _| puts k }
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A special class designed to be used as a key in a hash
|
4
|
+
class CyclicLink
|
5
|
+
def initialize(nodeA, nodeB)
|
6
|
+
@nodeA = nodeA
|
7
|
+
@nodeB = nodeB
|
8
|
+
end
|
9
|
+
|
10
|
+
def nodeA
|
11
|
+
@nodeA
|
12
|
+
end
|
13
|
+
|
14
|
+
def nodeB
|
15
|
+
@nodeB
|
16
|
+
end
|
17
|
+
|
18
|
+
def eql?(other)
|
19
|
+
(@nodeA == other.nodeA && @nodeB == other.nodeB) ||
|
20
|
+
(@nodeA == other.nodeB && @nodeB == other.nodeA)
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash
|
24
|
+
[@nodeA, @nodeB].to_set.hash
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"#{nodeA} <-> #{nodeB}"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'project'
|
4
|
+
require_relative 'component_link'
|
5
|
+
require_relative 'cycle_detector'
|
6
|
+
|
7
|
+
# Returns a hash of component links
|
8
|
+
class DependencyGraph
|
9
|
+
def initialize(project)
|
10
|
+
@project = project
|
11
|
+
end
|
12
|
+
|
13
|
+
def components
|
14
|
+
@components ||= source_components
|
15
|
+
end
|
16
|
+
|
17
|
+
def all_component_links
|
18
|
+
@all_component_links ||= build_hash_component_links
|
19
|
+
end
|
20
|
+
|
21
|
+
def component_links(name)
|
22
|
+
return {} unless all_component_links.key?(name)
|
23
|
+
incoming_links(name).merge(outgoing_links(name))
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build_hash_component_links
|
29
|
+
raw_links = @project.source_components.map { |c| [c.name, @project.dependencies(c)] }.to_h
|
30
|
+
cycle_detector = CycleDetector.new(raw_links)
|
31
|
+
component_links = raw_links.map do |source, links|
|
32
|
+
c_links = links.map { |target| ComponentLink.new(source, target, cycle_detector.cyclic?(source, target)) }
|
33
|
+
[source, c_links]
|
34
|
+
end.to_h
|
35
|
+
component_links
|
36
|
+
end
|
37
|
+
|
38
|
+
def outgoing_links(name)
|
39
|
+
all_component_links.slice(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def incoming_links(target)
|
43
|
+
incoming_c_links = all_component_links.select { |c, c_links| c_links.any? { |link| link.target == target } }
|
44
|
+
incoming_c_links.map do |c_name, _|
|
45
|
+
link = ComponentLink.new(c_name, target, false)
|
46
|
+
[c_name, [link]]
|
47
|
+
end.to_h
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphviz'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
class GraphVisualiser
|
7
|
+
def generate_dot_file(deps, file)
|
8
|
+
@g = Graphviz::Graph.new(name = 'dependency_graph')
|
9
|
+
|
10
|
+
node_names = deps.flat_map do |_, links|
|
11
|
+
links.map { |link| [link.source, link.target] }.flatten
|
12
|
+
end.uniq
|
13
|
+
nodes = node_names.map { |name| [name, create_node(name)] }.to_h
|
14
|
+
|
15
|
+
deps.each do |source, links|
|
16
|
+
links.each do |link|
|
17
|
+
connection = nodes[source].connect(nodes[link.target])
|
18
|
+
connection.attributes[:color] = 'red' if link.cyclic?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
File.write(file, @g.to_dot)
|
23
|
+
# Graphviz::output(g, path: 'file') # TODO: https://github.com/ioquatix/graphviz/issues/7
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_html_file(deps, file)
|
27
|
+
node_names = deps.flat_map do |_, links|
|
28
|
+
links.map { |link| [link.source, link.target] }.flatten
|
29
|
+
end.uniq
|
30
|
+
nodes = node_names.map { |name| { name: name } }
|
31
|
+
|
32
|
+
connections = deps.flat_map do |_, links|
|
33
|
+
links.map do |link|
|
34
|
+
{ source: link.source, dest: link.target }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
json_nodes = JSON.pretty_generate(nodes)
|
39
|
+
json_connections = JSON.pretty_generate(connections)
|
40
|
+
# File.write(file, json_nodes + json_connections)
|
41
|
+
|
42
|
+
template_file = resolve_file_path('views/index.html.template')
|
43
|
+
template = File.read(template_file)
|
44
|
+
contents = template % { dependency_links: json_connections }
|
45
|
+
File.write(file, contents)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def create_node(name)
|
51
|
+
node = @g.add_node(name)
|
52
|
+
node.attributes[:shape] = 'box3d'
|
53
|
+
node
|
54
|
+
end
|
55
|
+
|
56
|
+
def resolve_file_path(path)
|
57
|
+
File.expand_path("../../../#{path}", __FILE__)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'project'
|
4
|
+
require_relative 'component_link'
|
5
|
+
require_relative 'cycle_detector'
|
6
|
+
|
7
|
+
# Returns a hash of intra-component include links
|
8
|
+
class IncludeDependencyGraph
|
9
|
+
def initialize(project)
|
10
|
+
@project = project
|
11
|
+
end
|
12
|
+
|
13
|
+
def include_links(component_name)
|
14
|
+
component = @project.source_component(component_name)
|
15
|
+
source_files = component.source_files
|
16
|
+
source_files.map do |file|
|
17
|
+
class_name = file.basename
|
18
|
+
# TODO: Very inefficient
|
19
|
+
internal_includes = file.includes.reject { |inc| @project.external_includes(component).any?(inc) }
|
20
|
+
include_links = internal_includes.map { |inc| ComponentLink.new(class_name, inc, false) }
|
21
|
+
[class_name, include_links]
|
22
|
+
end.to_h
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'find'
|
4
|
+
|
5
|
+
require_relative 'source_component'
|
6
|
+
|
7
|
+
# Parses all components of a project
|
8
|
+
class Project
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
end
|
12
|
+
|
13
|
+
def source_components
|
14
|
+
@source_components ||= build_source_components
|
15
|
+
end
|
16
|
+
|
17
|
+
def source_component(name)
|
18
|
+
source_components.detect { |c| c.name == name }
|
19
|
+
end
|
20
|
+
|
21
|
+
def dependencies(component)
|
22
|
+
external_includes(component).map { |include| component_for_include(include) }.reject(&:empty?).uniq
|
23
|
+
end
|
24
|
+
|
25
|
+
def external_includes(component)
|
26
|
+
filter_internal_includes(component)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_source_components
|
32
|
+
# TODO: Dealing with source components with same dir name?
|
33
|
+
dirs = fetch_all_dirs(@path)
|
34
|
+
source_components = dirs.map { |dir| SourceComponent.new(dir) }
|
35
|
+
source_components.reject { |c| c.source_files.size.zero? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def filter_internal_includes(component)
|
39
|
+
# TODO: This is super inefficient, refactor it
|
40
|
+
source_file_basenames = component.source_files.map(&:basename)
|
41
|
+
include_components = component.includes.map { |inc| [inc, component_for_include(inc)] }.to_h
|
42
|
+
filter = include_components.reject { |_, c| c == component.name }
|
43
|
+
filter.keys
|
44
|
+
end
|
45
|
+
|
46
|
+
def component_for_include(include)
|
47
|
+
header_file = source_files.find { |file| file.basename == include }
|
48
|
+
parent_component(header_file)
|
49
|
+
end
|
50
|
+
|
51
|
+
def source_files
|
52
|
+
@source_files ||= source_components.flat_map(&:source_files)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parent_component(header_file)
|
56
|
+
return '' if header_file.nil?
|
57
|
+
files = source_files.select { |file| file.basename_no_extension == header_file.basename_no_extension }
|
58
|
+
corresponding_files = files.reject { |file| file.basename == header_file.basename }
|
59
|
+
return header_file.parent_component if corresponding_files.size == 0
|
60
|
+
corresponding_files[0].parent_component
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_all_dirs(source_dir)
|
64
|
+
Find.find(source_dir).select { |e| File.directory?(e) }
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'source_file'
|
4
|
+
|
5
|
+
class SourceComponent
|
6
|
+
def initialize(path)
|
7
|
+
@path = path
|
8
|
+
end
|
9
|
+
|
10
|
+
def path
|
11
|
+
@path
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
@name ||= File.basename(@path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def source_files
|
19
|
+
@source_files ||= parse_source_files('.{h,hpp,hxx,c,cpp,cxx,cc}')
|
20
|
+
end
|
21
|
+
|
22
|
+
def includes
|
23
|
+
@includes ||= source_files.flat_map(&:includes).uniq.map { |include| File.basename(include) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def loc
|
27
|
+
@loc ||= source_files.inject(0) { |total_loc, file| total_loc + file.loc }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_source_files(extensions)
|
33
|
+
path = File.join(@path, File::SEPARATOR) + '*' + extensions
|
34
|
+
files = Dir.glob(path).select { |e| File.file?(e) }
|
35
|
+
files.map { |file| SourceFile.new(file) }
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Source file and metadata
|
4
|
+
class SourceFile
|
5
|
+
def initialize(file)
|
6
|
+
@path = file
|
7
|
+
end
|
8
|
+
|
9
|
+
def basename
|
10
|
+
@basename ||= File.basename(@path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def basename_no_extension
|
14
|
+
@basename_no_extension ||= File.basename(@path, File.extname(@path))
|
15
|
+
end
|
16
|
+
|
17
|
+
def path
|
18
|
+
@path ||= File.absolute_path(@path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def header?
|
22
|
+
false # TODO: Implement check extension
|
23
|
+
end
|
24
|
+
|
25
|
+
def parent_component
|
26
|
+
@parent_component ||= File.dirname(@path).split('/').last
|
27
|
+
end
|
28
|
+
|
29
|
+
def includes
|
30
|
+
@includes ||= all_includes
|
31
|
+
end
|
32
|
+
|
33
|
+
def loc
|
34
|
+
@loc ||= file_contents.lines.count
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def all_includes
|
40
|
+
@all_includes ||= scan_includes
|
41
|
+
end
|
42
|
+
|
43
|
+
def scan_includes
|
44
|
+
# file_contents.scan(/#include "([^"]+)"/).uniq.flatten
|
45
|
+
file_contents.scan(/#include ["|<](.+)["|>]/).uniq.flatten # TODO: use compiler lib to scan includes? llvm/clang?
|
46
|
+
end
|
47
|
+
|
48
|
+
def file_contents
|
49
|
+
@file_contents ||= File.read(@path)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'cpp_dependency_graph/project'
|
4
|
+
require_relative 'cpp_dependency_graph/dependency_graph'
|
5
|
+
require_relative 'cpp_dependency_graph/include_dependency_graph'
|
6
|
+
require_relative 'cpp_dependency_graph/graph_visualiser'
|
7
|
+
require_relative 'cpp_dependency_graph/version'
|
8
|
+
|
9
|
+
# Generates dependency graphs of a project in various output forms
|
10
|
+
module CppDependencyGraph
|
11
|
+
def generate_project_graph(project_dir, format, output_file)
|
12
|
+
project = Project.new(project_dir)
|
13
|
+
graph = DependencyGraph.new(project)
|
14
|
+
deps = graph.all_component_links
|
15
|
+
generate_visualisation(deps, format, output_file)
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate_component_graph(project_dir, component, format, output_file)
|
19
|
+
project = Project.new(project_dir)
|
20
|
+
graph = DependencyGraph.new(project)
|
21
|
+
deps = graph.component_links(component)
|
22
|
+
generate_visualisation(deps, format, output_file)
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_component_include_graph(project_dir, component_name, format, output_file)
|
26
|
+
project = Project.new(project_dir)
|
27
|
+
graph = IncludeDependencyGraph.new(project)
|
28
|
+
deps = graph.include_links(component_name)
|
29
|
+
generate_visualisation(deps, format, output_file)
|
30
|
+
end
|
31
|
+
|
32
|
+
def output_cyclic_dependencies(project_dir)
|
33
|
+
project = Project.new(project_dir)
|
34
|
+
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)
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_visualisation(deps, format, file)
|
41
|
+
case format
|
42
|
+
when 'dot'
|
43
|
+
GraphVisualiser.new.generate_dot_file(deps, file)
|
44
|
+
when 'html'
|
45
|
+
GraphVisualiser.new.generate_html_file(deps, file)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|