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.
- checksums.yaml +4 -4
- data/.gitattributes +2 -0
- data/.rubocop_todo.yml +21 -0
- data/.vscode/launch.json +1 -1
- data/Gemfile +1 -1
- data/README.md +36 -4
- data/Rakefile +2 -0
- data/TODO.md +10 -2
- data/bin/console +2 -0
- data/cpp_dependency_graph.gemspec +7 -6
- data/docs/README.md +12 -0
- data/examples/leveldb_cyclic_deps.svg +1 -0
- data/examples/rethinkdb_cyclic_deps.svg +1 -0
- data/exe/cpp_dependency_graph +14 -8
- data/lib/cpp_dependency_graph.rb +33 -15
- data/lib/cpp_dependency_graph/bidirectional_hash.rb +14 -13
- data/lib/cpp_dependency_graph/circle_packing_visualiser.rb +20 -0
- data/lib/cpp_dependency_graph/component_dependency_graph.rb +56 -0
- data/lib/cpp_dependency_graph/config.rb +8 -0
- data/lib/cpp_dependency_graph/cycle_detector.rb +8 -10
- data/lib/cpp_dependency_graph/cyclic_link.rb +12 -15
- data/lib/cpp_dependency_graph/dir_tree.rb +41 -0
- data/lib/cpp_dependency_graph/file_dependency_graph.rb +56 -0
- data/lib/cpp_dependency_graph/graph_to_dot_visualiser.rb +38 -0
- data/lib/cpp_dependency_graph/graph_to_graphml_visualiser.rb +8 -0
- data/lib/cpp_dependency_graph/graph_to_html_visualiser.rb +25 -0
- data/lib/cpp_dependency_graph/include_dependency_graph.rb +21 -6
- data/lib/cpp_dependency_graph/include_to_component_resolver.rb +58 -0
- data/lib/cpp_dependency_graph/link.rb +42 -0
- data/lib/cpp_dependency_graph/project.rb +12 -30
- data/lib/cpp_dependency_graph/source_component.rb +8 -7
- data/lib/cpp_dependency_graph/source_file.rb +4 -3
- data/lib/cpp_dependency_graph/tsortable_hash.rb +1 -0
- data/lib/cpp_dependency_graph/version.rb +1 -1
- data/views/circle_packing.html.template +105 -0
- metadata +51 -25
- data/lib/cpp_dependency_graph/component_link.rb +0 -37
- data/lib/cpp_dependency_graph/dependency_graph.rb +0 -49
- data/lib/cpp_dependency_graph/graph_to_html_converter.rb +0 -7
- 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
|
@@ -2,16 +2,14 @@
|
|
2
2
|
|
3
3
|
require_relative 'cyclic_link'
|
4
4
|
|
5
|
-
# Detects cycles between
|
5
|
+
# Detects cycles between nodes
|
6
6
|
class CycleDetector
|
7
|
-
def initialize(
|
8
|
-
@cyclic_links =
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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.
|
21
|
+
@cyclic_links.keys
|
24
22
|
end
|
25
23
|
end
|
@@ -1,30 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
class CyclicLink
|
5
|
-
def initialize(nodeA, nodeB)
|
6
|
-
@nodeA = nodeA
|
7
|
-
@nodeB = nodeB
|
8
|
-
end
|
3
|
+
require 'set'
|
9
4
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
15
|
-
@
|
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
|
-
(@
|
20
|
-
|
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
|
-
[@
|
21
|
+
[@node_a, @node_b].to_set.hash
|
25
22
|
end
|
26
23
|
|
27
24
|
def to_s
|
28
|
-
"#{
|
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,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 '
|
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
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|