cpp_dependency_graph 0.1.1 → 0.1.2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +2 -0
  3. data/.rubocop_todo.yml +21 -0
  4. data/.vscode/launch.json +1 -1
  5. data/Gemfile +1 -1
  6. data/README.md +36 -4
  7. data/Rakefile +2 -0
  8. data/TODO.md +10 -2
  9. data/bin/console +2 -0
  10. data/cpp_dependency_graph.gemspec +7 -6
  11. data/docs/README.md +12 -0
  12. data/examples/leveldb_cyclic_deps.svg +1 -0
  13. data/examples/rethinkdb_cyclic_deps.svg +1 -0
  14. data/exe/cpp_dependency_graph +14 -8
  15. data/lib/cpp_dependency_graph.rb +33 -15
  16. data/lib/cpp_dependency_graph/bidirectional_hash.rb +14 -13
  17. data/lib/cpp_dependency_graph/circle_packing_visualiser.rb +20 -0
  18. data/lib/cpp_dependency_graph/component_dependency_graph.rb +56 -0
  19. data/lib/cpp_dependency_graph/config.rb +8 -0
  20. data/lib/cpp_dependency_graph/cycle_detector.rb +8 -10
  21. data/lib/cpp_dependency_graph/cyclic_link.rb +12 -15
  22. data/lib/cpp_dependency_graph/dir_tree.rb +41 -0
  23. data/lib/cpp_dependency_graph/file_dependency_graph.rb +56 -0
  24. data/lib/cpp_dependency_graph/graph_to_dot_visualiser.rb +38 -0
  25. data/lib/cpp_dependency_graph/graph_to_graphml_visualiser.rb +8 -0
  26. data/lib/cpp_dependency_graph/graph_to_html_visualiser.rb +25 -0
  27. data/lib/cpp_dependency_graph/include_dependency_graph.rb +21 -6
  28. data/lib/cpp_dependency_graph/include_to_component_resolver.rb +58 -0
  29. data/lib/cpp_dependency_graph/link.rb +42 -0
  30. data/lib/cpp_dependency_graph/project.rb +12 -30
  31. data/lib/cpp_dependency_graph/source_component.rb +8 -7
  32. data/lib/cpp_dependency_graph/source_file.rb +4 -3
  33. data/lib/cpp_dependency_graph/tsortable_hash.rb +1 -0
  34. data/lib/cpp_dependency_graph/version.rb +1 -1
  35. data/views/circle_packing.html.template +105 -0
  36. metadata +51 -25
  37. data/lib/cpp_dependency_graph/component_link.rb +0 -37
  38. data/lib/cpp_dependency_graph/dependency_graph.rb +0 -49
  39. data/lib/cpp_dependency_graph/graph_to_html_converter.rb +0 -7
  40. data/lib/cpp_dependency_graph/graph_visualiser.rb +0 -59
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'project'
4
+ require_relative 'link'
5
+ require_relative 'cycle_detector'
6
+
7
+ # Dependency tree/graph of entire project
8
+ class ComponentDependencyGraph
9
+ def initialize(project)
10
+ @project = project
11
+ end
12
+
13
+ def all_links
14
+ @all_links ||= build_hash_links
15
+ end
16
+
17
+ def all_cyclic_links
18
+ @all_cyclic_links ||= build_cyclic_links
19
+ end
20
+
21
+ def links(name)
22
+ return {} unless all_links.key?(name)
23
+ links = incoming_links(name)
24
+ links.merge!(outgoing_links(name))
25
+ links
26
+ end
27
+
28
+ private
29
+
30
+ def build_hash_links
31
+ raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c)] }.to_h
32
+ @cycle_detector ||= CycleDetector.new(raw_links)
33
+ links = raw_links.map do |source, source_links|
34
+ c_links = source_links.map { |target| Link.new(source, target, @cycle_detector.cyclic?(source, target)) }
35
+ [source, c_links]
36
+ end.to_h
37
+ links
38
+ end
39
+
40
+ def build_cyclic_links
41
+ cyclic_links = all_links.select { |_, links| links.any?(&:cyclic?) }
42
+ cyclic_links.each_value { |links| links.select!(&:cyclic?) }
43
+ end
44
+
45
+ def outgoing_links(name)
46
+ all_links.slice(name)
47
+ end
48
+
49
+ def incoming_links(target)
50
+ incoming_c_links = all_links.select { |_, c_links| c_links.any? { |link| link.target == target } }
51
+ incoming_c_links.map do |source, _|
52
+ link = Link.new(source, target, @cycle_detector.cyclic?(source, target))
53
+ [source, [link]]
54
+ end.to_h
55
+ end
56
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configuration data for project
4
+ module Config
5
+ def source_file_extensions
6
+ '.{h,hpp,hxx,c,cpp,cxx,cc}'
7
+ end
8
+ end
@@ -2,16 +2,14 @@
2
2
 
3
3
  require_relative 'cyclic_link'
4
4
 
5
- # Detects cycles between components
5
+ # Detects cycles between nodes
6
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
7
+ def initialize(links)
8
+ @cyclic_links = links.flat_map do |source, source_links|
9
+ source_links.select { |target| links[target].include?(source) }.map do |target|
10
+ [CyclicLink.new(source, target), true]
11
+ end
12
+ end.to_h
15
13
  end
16
14
 
17
15
  def cyclic?(source, target)
@@ -20,6 +18,6 @@ class CycleDetector
20
18
  end
21
19
 
22
20
  def to_s
23
- @cyclic_links.each { |k, _| puts k }
21
+ @cyclic_links.keys
24
22
  end
25
23
  end
@@ -1,30 +1,27 @@
1
1
  # frozen_string_literal: true
2
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
3
+ require 'set'
9
4
 
10
- def nodeA
11
- @nodeA
12
- end
5
+ # Represents a cyclic link betwee two nodes, it is special in the sense it is designed to be used as a key in a hash
6
+ class CyclicLink
7
+ attr_reader :node_a
8
+ attr_reader :node_b
13
9
 
14
- def nodeB
15
- @nodeB
10
+ def initialize(node_a, node_b)
11
+ @node_a = node_a
12
+ @node_b = node_b
16
13
  end
17
14
 
18
15
  def eql?(other)
19
- (@nodeA == other.nodeA && @nodeB == other.nodeB) ||
20
- (@nodeA == other.nodeB && @nodeB == other.nodeA)
16
+ (@node_a == other.node_a && @node_b == other.node_b) ||
17
+ (@node_a == other.node_b && @node_b == other.node_a)
21
18
  end
22
19
 
23
20
  def hash
24
- [@nodeA, @nodeB].to_set.hash
21
+ [@node_a, @node_b].to_set.hash
25
22
  end
26
23
 
27
24
  def to_s
28
- "#{nodeA} <-> #{nodeB}"
25
+ "#{node_a} <-> #{node_b}"
29
26
  end
30
27
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ require_relative 'config'
6
+
7
+ # Returns a root directory as a tree of directories
8
+ class DirTree
9
+ include Config
10
+
11
+ attr_reader :tree
12
+
13
+ def initialize(path)
14
+ @tree ||= File.directory?(path) ? parse_dirs(path) : {}
15
+ end
16
+
17
+ private
18
+
19
+ def parse_dirs(path, name = nil)
20
+ data = Hash.new{|h, k| h[k] = []}
21
+ data[:name] = (name || path)
22
+ # TODO: Use Dir.map.compact|filter instead here
23
+ Dir.foreach(path) do |entry|
24
+ next if ['..', '.'].include?(entry)
25
+ full_path = File.join(path, entry)
26
+ next unless File.directory?(full_path)
27
+ next unless source_files_present?(full_path)
28
+ data[:children] << parse_dirs(full_path, entry)
29
+ end
30
+ data
31
+ end
32
+
33
+ def source_files_present?(full_path)
34
+ files = Dir.glob(File.join(full_path, File.join('**', '*' + source_file_extensions)))
35
+ files.size.positive?
36
+ end
37
+
38
+ def to_s
39
+ @tree.to_json
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'project'
4
+ require_relative 'link'
5
+ require_relative 'cycle_detector'
6
+
7
+ # Dependency tree/graph of entire project
8
+ class FileDependencyGraph
9
+ def initialize(project)
10
+ @project = project
11
+ end
12
+
13
+ def all_links
14
+ @all_links ||= build_hash_links
15
+ end
16
+
17
+ def all_cyclic_links
18
+ @all_cyclic_links ||= build_cyclic_links
19
+ end
20
+
21
+ def links(name)
22
+ return {} unless all_links.key?(name)
23
+ links = incoming_links(name)
24
+ links.merge!(outgoing_links(name))
25
+ links
26
+ end
27
+
28
+ private
29
+
30
+ def build_hash_links
31
+ raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c)] }.to_h
32
+ @cycle_detector ||= CycleDetector.new(raw_links)
33
+ links = raw_links.map do |source, source_links|
34
+ c_links = source_links.map { |target| Link.new(source, target, @cycle_detector.cyclic?(source, target)) }
35
+ [source, c_links]
36
+ end.to_h
37
+ links
38
+ end
39
+
40
+ def build_cyclic_links
41
+ cyclic_links = all_links.select { |_, links| links.any?(&:cyclic?) }
42
+ cyclic_links.each_value { |links| links.select!(&:cyclic?) }
43
+ end
44
+
45
+ def outgoing_links(name)
46
+ all_links.slice(name)
47
+ end
48
+
49
+ def incoming_links(target)
50
+ incoming_c_links = all_links.select { |_, c_links| c_links.any? { |link| link.target == target } }
51
+ incoming_c_links.map do |source, _|
52
+ link = Link.new(source, target, @cycle_detector.cyclic?(source, target))
53
+ [source, [link]]
54
+ end.to_h
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphviz'
4
+
5
+ # Outputs a `dot` langugage representation of a dependency graph
6
+ class GraphToDotVisualiser
7
+ def generate(deps, file)
8
+ @g = Graphviz::Graph.new('dependency_graph')
9
+ nodes = create_nodes(deps)
10
+ connect_nodes(deps, nodes)
11
+ File.write(file, @g.to_dot)
12
+ end
13
+
14
+ private
15
+
16
+ def create_nodes(deps)
17
+ node_names = deps.flat_map do |_, links|
18
+ links.map { |link| [link.source, link.target] }.flatten
19
+ end.uniq
20
+ nodes = node_names.map { |name| [name, create_node(name)] }.to_h
21
+ nodes
22
+ end
23
+
24
+ def create_node(name)
25
+ node = @g.add_node(name)
26
+ node.attributes[:shape] = 'box3d'
27
+ node
28
+ end
29
+
30
+ def connect_nodes(deps, nodes)
31
+ deps.each do |source, links|
32
+ links.each do |link|
33
+ connection = nodes[source].connect(nodes[link.target])
34
+ connection.attributes[:color] = 'red' if link.cyclic?
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Outputs a `graphml` langugage representation of a dependency graph
4
+ class GraphToGraphmlVisualiser
5
+ def generate(deps, file)
6
+ # TODO: Implement
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # Outputs a `d3 force graph layout` equipped HTML representation of a dependency graph
6
+ class GraphToHtmlVisualiser
7
+ def generate(deps, file)
8
+ connections = deps.flat_map do |_, links|
9
+ links.map do |link|
10
+ { source: link.source, dest: link.target }
11
+ end
12
+ end
13
+ json_connections = JSON.pretty_generate(connections)
14
+ template_file = resolve_file_path('views/index.html.template')
15
+ template = File.read(template_file)
16
+ contents = format(template, dependency_links: json_connections)
17
+ File.write(file, contents)
18
+ end
19
+
20
+ private
21
+
22
+ def resolve_file_path(path)
23
+ File.expand_path("../../../#{path}", __FILE__)
24
+ end
25
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'project'
4
- require_relative 'component_link'
4
+ require_relative 'link'
5
5
  require_relative 'cycle_detector'
6
6
 
7
7
  # Returns a hash of intra-component include links
@@ -10,15 +10,30 @@ class IncludeDependencyGraph
10
10
  @project = project
11
11
  end
12
12
 
13
- def include_links(component_name)
13
+ def all_links
14
+ components = @project.source_components
15
+ all_source_files = components.values.flat_map(&:source_files)
16
+ all_source_files.map do |file|
17
+ source = file.basename
18
+ links = file.includes.map { |inc| Link.new(source, inc, false) }
19
+ [source, links]
20
+ end.to_h
21
+ end
22
+
23
+ def all_cyclic_links
24
+ # TODO: Implement
25
+ end
26
+
27
+ def links(component_name)
14
28
  component = @project.source_component(component_name)
15
29
  source_files = component.source_files
30
+ external_includes = @project.external_includes(component)
16
31
  source_files.map do |file|
17
- class_name = file.basename
18
32
  # 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]
33
+ source = file.basename
34
+ internal_includes = file.includes.reject { |inc| external_includes.any?(inc) }
35
+ links = internal_includes.map { |inc| Link.new(source, inc, false) }
36
+ [source, links]
22
37
  end.to_h
23
38
  end
24
39
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'source_component'
4
+
5
+ # Resolves a given include to a source component
6
+ class IncludeToComponentResolver
7
+ def initialize(components)
8
+ @components = components
9
+ @component_external_include_cache = {}
10
+ @component_include_map_cache = {}
11
+ end
12
+
13
+ def external_includes(component)
14
+ unless @component_external_include_cache.key?(component)
15
+ @component_external_include_cache[component] = external_includes_private(component)
16
+ end
17
+ @component_external_include_cache[component]
18
+ end
19
+
20
+ def component_for_include(include)
21
+ return '' unless source_files.key?(include)
22
+ @component_include_map_cache[include] = component_for_include_private(include) unless @component_include_map_cache.key?(include)
23
+ @component_include_map_cache[include]
24
+ end
25
+
26
+ private
27
+
28
+ def external_includes_private(component)
29
+ include_components = component.includes.map { |inc| [inc, component_for_include(inc)] }.to_h
30
+ external_include_components = include_components.delete_if { |_, c| c == component.name }
31
+ external_include_components.keys
32
+ end
33
+
34
+ def component_for_include_private(include)
35
+ header_file = source_files[include]
36
+ implementation_files = implementation_files(header_file)
37
+ return header_file.parent_component if implementation_files.empty?
38
+ implementation_files[0].parent_component
39
+ end
40
+
41
+ def implementation_files(header_file)
42
+ implementation_files = []
43
+ source_files.each_value do |file|
44
+ implementation_files.push(file) if file.basename_no_extension == header_file.basename_no_extension
45
+ end
46
+ implementation_files.reject! { |file| file.basename == header_file.basename }
47
+ end
48
+
49
+ def source_files
50
+ @source_files ||= build_source_file_map
51
+ end
52
+
53
+ def build_source_file_map
54
+ # TODO: SourceComponent should return a hash for source files which can be merged here
55
+ source_files = @components.values.flat_map(&:source_files)
56
+ source_files.map { |s| [s.basename, s] }.to_h
57
+ end
58
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'set'
5
+
6
+ # Represents a link between two entities, a source and a target
7
+ class Link
8
+ attr_reader :source
9
+ attr_reader :target
10
+
11
+ def initialize(source, target, cyclic = false)
12
+ @source = source
13
+ @target = target
14
+ @cyclic = cyclic
15
+ end
16
+
17
+ def cyclic?
18
+ @cyclic
19
+ end
20
+
21
+ def ==(other)
22
+ (source == other.source && target == other.target && cyclic? == other.cyclic?) ||
23
+ (source == other.target && target == other.source && cyclic? == other.cyclic?)
24
+ end
25
+
26
+ def hash
27
+ [source, target, cyclic?].to_set.hash
28
+ end
29
+
30
+ def to_s
31
+ if cyclic?
32
+ "#{source} <-> #{target}"
33
+ else
34
+ "#{source} -> #{target}"
35
+ end
36
+ end
37
+
38
+ def to_json(*options)
39
+ { json_class: self.class.name,
40
+ source: source, target: target, cyclic: cyclic? }.to_json(*options)
41
+ end
42
+ end