cpp_dependency_graph 0.1.1
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/.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
|