cpp_dependency_graph 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +3 -0
  4. data/.vscode/launch.json +79 -0
  5. data/.vscode/tasks.json +12 -0
  6. data/CODE_OF_CONDUCT.md +46 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +21 -0
  9. data/README.md +84 -0
  10. data/Rakefile +9 -0
  11. data/TODO.md +38 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/cpp_dependency_graph.gemspec +44 -0
  15. data/examples/leveldb_overall.png +0 -0
  16. data/examples/leveldb_overall.svg +215 -0
  17. data/examples/leveldb_overall_d3.png +0 -0
  18. data/examples/leveldb_overall_d3.svg +1 -0
  19. data/examples/rethinkdb_queue_component.png +0 -0
  20. data/examples/rethinkdb_queue_component.svg +234 -0
  21. data/examples/rethinkdb_queue_component_d3.png +0 -0
  22. data/examples/rethinkdb_queue_component_d3.svg +1 -0
  23. data/examples/rethinkdb_queue_include.png +0 -0
  24. data/examples/rethinkdb_queue_include.svg +211 -0
  25. data/examples/rethinkdb_queue_include_d3.png +0 -0
  26. data/examples/rethinkdb_queue_include_d3.svg +1 -0
  27. data/examples/rocksdb_overall_d3.svg +1 -0
  28. data/examples/rocksdb_table_component.png +0 -0
  29. data/exe/cpp_dependency_graph +87 -0
  30. data/lib/cpp_dependency_graph/bidirectional_hash.rb +30 -0
  31. data/lib/cpp_dependency_graph/component_link.rb +37 -0
  32. data/lib/cpp_dependency_graph/cycle_detector.rb +25 -0
  33. data/lib/cpp_dependency_graph/cyclic_link.rb +30 -0
  34. data/lib/cpp_dependency_graph/dependency_graph.rb +49 -0
  35. data/lib/cpp_dependency_graph/graph_to_html_converter.rb +7 -0
  36. data/lib/cpp_dependency_graph/graph_visualiser.rb +59 -0
  37. data/lib/cpp_dependency_graph/include_dependency_graph.rb +24 -0
  38. data/lib/cpp_dependency_graph/project.rb +66 -0
  39. data/lib/cpp_dependency_graph/source_component.rb +37 -0
  40. data/lib/cpp_dependency_graph/source_file.rb +51 -0
  41. data/lib/cpp_dependency_graph/tsortable_hash.rb +12 -0
  42. data/lib/cpp_dependency_graph/version.rb +5 -0
  43. data/lib/cpp_dependency_graph.rb +48 -0
  44. data/views/index.html.template +990 -0
  45. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generates an HTML file with a D3 force directed graph layout
4
+ class GraphToHtmlConverter
5
+ def self.generate(graph, file)
6
+ end
7
+ 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tsort'
4
+
5
+ class TsortableHash < Hash
6
+ include TSort
7
+
8
+ alias tsort_each_node each_key
9
+ def tsort_each_child(node, &block)
10
+ fetch(node).each(&block)
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CppDependencyGraph
4
+ VERSION = '0.1.1'
5
+ 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