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.
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