├── docs ├── _config.yml ├── examples │ ├── leveldb_overall.png │ ├── leveldb_overall_d3.png │ ├── rethinkdb_queue_include.png │ ├── rocksdb_table_component.png │ ├── rethinkdb_queue_component.png │ ├── rethinkdb_queue_component_d3.png │ ├── rethinkdb_queue_include_d3.png │ ├── leveldb_cyclic_deps.svg │ ├── leveldb_overall.svg │ ├── rethinkdb_queue_component.svg │ ├── rethinkdb_queue_include.svg │ ├── leveldb_overall_d3.svg │ ├── rethinkdb_queue_include_d3.svg │ └── rethinkdb_queue_component_d3.svg ├── README.md └── index.md ├── spec ├── test │ ├── example_project │ │ ├── .hidden │ │ │ └── dummy │ │ ├── System │ │ │ ├── Engine.h │ │ │ ├── System.h │ │ │ └── System.cpp │ │ ├── Engine │ │ │ ├── OldEngine.h │ │ │ ├── Engine.cpp │ │ │ └── Engine.h │ │ ├── DataAccess │ │ │ └── DA.h │ │ ├── UI │ │ │ ├── Display.cpp │ │ │ └── Display.h │ │ ├── main │ │ │ └── main.cpp │ │ └── Framework │ │ │ └── framework.h │ ├── gem_spec.rb │ ├── cyclic_link_spec.rb │ ├── include_file_dependency_graph_spec.rb │ ├── component_link_spec.rb │ ├── include_component_dependency_graph_spec.rb │ ├── dir_tree_spec.rb │ ├── source_component_spec.rb │ ├── source_file_spec.rb │ ├── project_spec.rb │ └── component_dependency_graph_spec.rb └── spec_helper.rb ├── .rspec ├── .gitattributes ├── lib ├── cpp_dependency_graph │ ├── version.rb │ ├── config.rb │ ├── graph_to_graphml_visualiser.rb │ ├── tsortable_hash.rb │ ├── directory_parser.rb │ ├── logging.rb │ ├── circle_packing_visualiser.rb │ ├── cyclic_link.rb │ ├── include_file_dependency_graph.rb │ ├── bidirectional_hash.rb │ ├── cycle_detector.rb │ ├── graph_to_html_visualiser.rb │ ├── source_component.rb │ ├── link.rb │ ├── graph_to_svg_visualiser.rb │ ├── dir_tree.rb │ ├── include_component_dependency_graph.rb │ ├── source_file.rb │ ├── file_dependency_graph.rb │ ├── component_dependency_graph.rb │ ├── project.rb │ └── include_to_component_resolver.rb └── cpp_dependency_graph.rb ├── .rubocop.yml ├── bin ├── setup └── console ├── Gemfile ├── Rakefile ├── .vscode ├── tasks.json └── launch.json ├── .gitignore ├── .github └── workflows │ └── github-stale-action.yml ├── .rubocop_todo.yml ├── LICENSE ├── cpp_dependency_graph.gemspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── views └── circle_packing.html.template ├── TODO.md ├── exe └── cpp_dependency_graph └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /spec/test/example_project/.hidden/dummy: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/test/example_project/System/Engine.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/test/example_project/System/System.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/test/example_project/Engine/OldEngine.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/test/example_project/DataAccess/DA.h: -------------------------------------------------------------------------------- 1 | #include "framework.h" 2 | -------------------------------------------------------------------------------- /spec/test/example_project/UI/Display.cpp: -------------------------------------------------------------------------------- 1 | #include "Engine.h" 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /spec/test/example_project/System/System.cpp: -------------------------------------------------------------------------------- 1 | #include "System.h" 2 | 3 | 4 | -------------------------------------------------------------------------------- /spec/test/example_project/UI/Display.h: -------------------------------------------------------------------------------- 1 | #include "framework.h" 2 | #include "Engine/Engine.h" 3 | -------------------------------------------------------------------------------- /spec/test/example_project/main/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Display.h" 2 | 3 | int main() 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto -------------------------------------------------------------------------------- /spec/test/example_project/Engine/Engine.cpp: -------------------------------------------------------------------------------- 1 | #include "DataAccess/DA.h" 2 | #include "Engine.h" 3 | 4 | -------------------------------------------------------------------------------- /spec/test/example_project/Engine/Engine.h: -------------------------------------------------------------------------------- 1 | #include "Framework/framework.h" 2 | #include "UI/Display.h" 3 | 4 | -------------------------------------------------------------------------------- /docs/examples/leveldb_overall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/leveldb_overall.png -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CppDependencyGraph 4 | VERSION = '0.4.2' 5 | end 6 | -------------------------------------------------------------------------------- /docs/examples/leveldb_overall_d3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/leveldb_overall_d3.png -------------------------------------------------------------------------------- /spec/test/example_project/Framework/framework.h: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_include.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/rethinkdb_queue_include.png -------------------------------------------------------------------------------- /docs/examples/rocksdb_table_component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/rocksdb_table_component.png -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/rethinkdb_queue_component.png -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_component_d3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/rethinkdb_queue_component_d3.png -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_include_d3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shreyasbharath/cpp-dependency-graph/HEAD/docs/examples/rethinkdb_queue_include_d3.png -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | TargetRubyVersion: 2.7 5 | DisabledByDefault: false 6 | 7 | Layout/LineLength: 8 | Max: 140 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |_| 'https://github.com/shreyasbharath/cpp_dependency_graph' } 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /spec/test/gem_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe CppDependencyGraph do 4 | it 'has a version number' do 5 | expect(CppDependencyGraph::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/config.rb: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/graph_to_graphml_visualiser.rb: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | require 'rubocop/rake_task' 6 | 7 | RuboCop::RakeTask.new 8 | 9 | RSpec::Core::RakeTask.new(:spec) do |t| 10 | t.pattern = Dir.glob('spec/**/*_spec.rb') 11 | end 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "echo", 8 | "type": "shell", 9 | "command": "echo Hello" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /Gemfile.lock 3 | /.yardoc 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | 11 | # rspec failure tracking 12 | .rspec_status 13 | deps.png 14 | deps.dot 15 | deps.html 16 | deps.json 17 | deps.graphml 18 | deps.svg 19 | index.html 20 | cpp_dependency_graph.callgrind.out* 21 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/tsortable_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'tsort' 4 | 5 | # Hash that topologically sorts itself upon insertion of a key 6 | class TsortableHash < Hash 7 | include TSort 8 | 9 | alias tsort_each_node each_key 10 | def tsort_each_child(node, &block) 11 | fetch(node).each(&block) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require 'bundler/setup' 6 | require 'cpp_dependency_graph' 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | # (If you use this, don't forget to add pry to your Gemfile!) 12 | # require "pry" 13 | # Pry.start 14 | 15 | require 'irb' 16 | IRB.start(__FILE__) 17 | -------------------------------------------------------------------------------- /.github/workflows/github-stale-action.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '5 * * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v7 11 | with: 12 | stale-issue-message: 'Message to comment on stale issues. If none provided, will not mark issues stale' 13 | stale-pr-message: 'Message to comment on stale PRs. If none provided, will not mark PRs stale' 14 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/directory_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Utility methods for parsing directories 4 | module DirectoryParser 5 | def fetch_all_dirs(root_dir) 6 | Find.find(root_dir).select { |e| File.directory?(e) && e != root_dir} 7 | end 8 | 9 | def glob_files(path, extensions) 10 | path = File.join(path, File::SEPARATOR, '**', File::SEPARATOR, '*' + extensions) 11 | Dir.glob(path).select { |entry| File.file?(entry) }.compact 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/logging.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | module Logging 4 | def logger 5 | Logging.logger 6 | end 7 | 8 | def self.logger 9 | @logger ||= initialise_logger 10 | end 11 | 12 | def self.initialise_logger 13 | logger = Logger.new(STDOUT, level: :info) 14 | logger.formatter = proc do |severity, datetime, progname, msg| 15 | date_format = datetime.strftime("%Y-%m-%d %H:%M:%S.%L") 16 | "[#{date_format}] #{severity}: #{msg}\n" 17 | end 18 | logger 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'simplecov' 5 | require 'simplecov-console' 6 | require 'cpp_dependency_graph' 7 | 8 | RSpec.configure do |config| 9 | # Enable flags like --only-failures and --next-failure 10 | config.example_status_persistence_file_path = '.rspec_status' 11 | 12 | # Disable RSpec exposing methods globally on `Module` and `main` 13 | config.disable_monkey_patching! 14 | 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | end 19 | 20 | SimpleCov.formatter = SimpleCov.formatter = SimpleCov::Formatter::Console 21 | SimpleCov.start 22 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/circle_packing_visualiser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | # Outputs a `d3 circle packing layout` equipped HTML representation of a hierarchical tree 6 | class CirclePackingVisualiser 7 | def generate(tree, file) 8 | json_tree = JSON.pretty_generate(tree) 9 | template_file = resolve_file_path('views/circle_packing.html.template') 10 | template = File.read(template_file) 11 | contents = format(template, tree: json_tree) 12 | File.write(file, contents) 13 | end 14 | 15 | private 16 | 17 | def resolve_file_path(path) 18 | File.expand_path("../../../#{path}", __FILE__) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/cyclic_link.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'set' 4 | 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 9 | 10 | def initialize(node_a, node_b) 11 | @node_a = node_a 12 | @node_b = node_b 13 | end 14 | 15 | def eql?(other) 16 | (@node_a == other.node_a && @node_b == other.node_b) || 17 | (@node_a == other.node_b && @node_b == other.node_a) 18 | end 19 | 20 | def hash 21 | [@node_a, @node_b].to_set.hash 22 | end 23 | 24 | def to_s 25 | "#{node_a} <-> #{node_b}" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2018-04-08 15:14:25 +1200 using RuboCop version 0.54.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | Metrics/AbcSize: 11 | Max: 19 12 | 13 | # Offense count: 2 14 | # Configuration parameters: CountComments, ExcludedMethods. 15 | Metrics/BlockLength: 16 | Max: 32 17 | 18 | # Offense count: 1 19 | Style/MixinUsage: 20 | Exclude: 21 | - 'exe/cpp_dependency_graph' 22 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/include_file_dependency_graph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'project' 4 | require_relative 'link' 5 | require_relative 'cycle_detector' 6 | 7 | # Returns a hash of individual file include links 8 | class IncludeFileDependencyGraph 9 | def initialize(project) 10 | @project = project 11 | end 12 | 13 | def all_cyclic_links 14 | # TODO: Implement 15 | end 16 | 17 | def links(file_name) 18 | files = @project.source_files.select do |_, file| 19 | file.includes.include?(file_name) 20 | end 21 | files.map do |_, file| 22 | links = [Link.new(file.basename, file_name, false)] 23 | [file.basename, links] 24 | end.to_h 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/bidirectional_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Hash that allows lookup by key and value 4 | class BidirectionalHash 5 | def initialize 6 | @forward = Hash.new { |h, k| h[k] = [] } 7 | @reverse = Hash.new { |h, k| h[k] = [] } 8 | end 9 | 10 | def insert(key, value) 11 | @forward[key].push(value) 12 | @reverse[value].push(key) 13 | value 14 | end 15 | 16 | def fetch(key) 17 | fetch_from(@forward, key) 18 | end 19 | 20 | def rfetch(value) 21 | fetch_from(@reverse, value) 22 | end 23 | 24 | protected 25 | 26 | def fetch_from(hash, key) 27 | return nil unless hash.key?(key) 28 | 29 | v = hash[key] 30 | v.length == 1 ? v.first : v.dup 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/test/cyclic_link_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/cyclic_link' 4 | 5 | RSpec.describe CyclicLink do 6 | it 'returns equal for two objects with the same nodes' do 7 | expect(CyclicLink.new('nodeA', 'nodeB')).to eql(CyclicLink.new('nodeB', 'nodeA')) 8 | end 9 | 10 | it 'returns the same hash for two equivalent objects with the same nodes' do 11 | expect(CyclicLink.new('nodeA', 'nodeB').hash).to eq(CyclicLink.new('nodeB', 'nodeA').hash) 12 | end 13 | 14 | it 'implements eql? operator correctly for usage as a hash key' do 15 | h = {} 16 | c1 = CyclicLink.new('nodeA', 'nodeB') 17 | c2 = CyclicLink.new('nodeB', 'nodeA') 18 | h[c1] = true 19 | h[c2] = false 20 | expect(h[c1]).to eq(false) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/cycle_detector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "cyclic_link" 4 | require_relative "logging" 5 | 6 | # Detects cycles between nodes 7 | class CycleDetector 8 | include Logging 9 | 10 | def initialize(links) 11 | logger.debug "Resolving cyclic dependendencies..." 12 | @cyclic_links = links.flat_map do |source, source_links| 13 | logger.debug "#{source}, #{source_links}" 14 | source_links.select { |target| links.fetch(target, []).include?(source) }.map do |target| 15 | [CyclicLink.new(source, target), true] 16 | end 17 | end.to_h 18 | end 19 | 20 | def cyclic?(source, target) 21 | k = CyclicLink.new(source, target) 22 | @cyclic_links.key?(k) 23 | end 24 | 25 | def to_s 26 | @cyclic_links.keys 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/graph_to_html_visualiser.rb: -------------------------------------------------------------------------------- 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 26 | -------------------------------------------------------------------------------- /spec/test/include_file_dependency_graph_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/include_file_dependency_graph' 4 | require 'cpp_dependency_graph/link' 5 | 6 | RSpec.describe IncludeFileDependencyGraph do 7 | let(:project) { Project.new('spec/test/example_project') } 8 | let(:include_dependency_graph) { IncludeFileDependencyGraph.new(project) } 9 | 10 | it 'returns empty hash for an unknown file' do 11 | expect(include_dependency_graph.links('Unknown').empty?).to eq(true) 12 | end 13 | 14 | it 'returns include links for a specified file' do 15 | expected_links = {} 16 | expected_links['Display.cpp'] = [Link.new('Display.cpp', 'Engine.h', false)] 17 | # expected_links['Display.h'] = [Link.new('Display.h', 'Engine.h', false)] 18 | expected_links['Engine.cpp'] = [Link.new('Engine.cpp', 'Engine.h', false)] 19 | expect(include_dependency_graph.links('Engine.h')).to eq(expected_links) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/source_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'config' 4 | require_relative 'directory_parser' 5 | require_relative 'source_file' 6 | 7 | # Abstracts a source directory containing source files 8 | class SourceComponent 9 | include Config 10 | include DirectoryParser 11 | 12 | attr_reader :path 13 | 14 | def initialize(path) 15 | @path = path 16 | end 17 | 18 | def name 19 | @name ||= File.basename(@path) 20 | end 21 | 22 | def source_files 23 | @source_files ||= parse_source_files(source_file_extensions) 24 | end 25 | 26 | def includes 27 | @includes ||= source_files.flat_map(&:includes).uniq.map { |include| File.basename(include) } 28 | end 29 | 30 | def loc 31 | @loc ||= source_files.inject(0) { |total_loc, file| total_loc + file.loc } 32 | end 33 | 34 | private 35 | 36 | def parse_source_files(extensions) 37 | glob_files(@path, extensions).map { |e| SourceFile.new(e) }.compact 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/link.rb: -------------------------------------------------------------------------------- 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 43 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/graph_to_svg_visualiser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby-graphviz' 4 | 5 | # Outputs a `dot` language representation of a dependency graph 6 | class GraphToSvgVisualiser 7 | def generate(deps, file) 8 | @g = GraphViz.new('dependency_graph') 9 | create_nodes(deps) 10 | connect_nodes(deps) 11 | @g.output(svg: file) 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 | node_names.each do |name| 21 | add_node(name) 22 | end 23 | end 24 | 25 | def add_node(name) 26 | @g.add_node(name, shape: 'box3d') 27 | end 28 | 29 | def connect_nodes(deps) 30 | deps.each do |source, links| 31 | links.each do |link| 32 | if link.cyclic? 33 | @g.add_edges(source, link.target, color: 'red') 34 | else 35 | @g.add_edges(source, link.target) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/test/component_link_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/link' 4 | 5 | RSpec.describe Link do 6 | it 'returns correct source attribute' do 7 | expect(Link.new('source', 'target').source).to eq('source') 8 | end 9 | 10 | it 'returns correct target attribute' do 11 | expect(Link.new('source', 'target').target).to eq('target') 12 | end 13 | 14 | it 'returns default cyclic? attribute as false' do 15 | expect(Link.new('source', 'target').cyclic?).to eq(false) 16 | end 17 | 18 | it 'returns correct cyclic? attribute' do 19 | expect(Link.new('source', 'target', true).cyclic?).to eq(true) 20 | end 21 | 22 | it 'returns equal if another instance has same attributes' do 23 | c1 = Link.new('source', 'target', true) 24 | c2 = Link.new('target', 'source', true) 25 | expect(c1). to eq(c2) 26 | end 27 | 28 | it 'returns not equal if another instance has different attributes' do 29 | c1 = Link.new('source1', 'target1', true) 30 | c2 = Link.new('source2', 'target2', true) 31 | expect(c1).to_not eq(c2) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: "Github Pages for Non-Developers: Build Web Portfolios From Scratch" 4 | --- 5 | 6 | # Cpp Dependency Graph 7 | 8 | Generates useful component dependency visualisations (`dot` or `d3.js`) to study the architecture of C/C++ projects. 9 | 10 | Why do all the other languages have awesome tools to analyse codebases but C/C++ does not? 11 | 12 | It's time to change that. 13 | 14 | This tool aims to - 15 | 16 | - provide multiple views into the architecture of a codebase 17 | - generate views at multiple levels of the architecture 18 | - make the resulting views genuinely useful, rich, dynamic and interactive (static views are boring) 19 | 20 | ## Inspiration 21 | 22 | This tool is inspired by a number of projects [rubrowser](http://www.emadelsaid.com/rubrowser/), [cpp-dependencies](https://github.com/tomtom-international/cpp-dependencies) and [objc-dependency-visualizer](https://github.com/PaulTaykalo/objc-dependency-visualizer). 23 | 24 | The pretty `d3` visualisations are directly copied from `objc-dependency-visualiser`. 25 | 26 | A huge shout out to the people behind these projects. -------------------------------------------------------------------------------- /spec/test/include_component_dependency_graph_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/include_component_dependency_graph' 4 | require 'cpp_dependency_graph/link' 5 | 6 | RSpec.describe IncludeComponentDependencyGraph do 7 | let(:project) { Project.new('spec/test/example_project') } 8 | let(:include_dependency_graph) { IncludeComponentDependencyGraph.new(project) } 9 | 10 | it 'returns empty hash for an unknown component' do 11 | expect(include_dependency_graph.links('Unknown').empty?).to eq(true) 12 | end 13 | 14 | it 'returns include links for a specified component' do 15 | expected_links = {} 16 | expected_links['OldEngine.h'] = [] 17 | expected_links['Engine.h'] = [Link.new('Engine.h', 'Framework/framework.h', false), 18 | Link.new('Engine.h', 'UI/Display.h', false)] 19 | expected_links['Engine.cpp'] = [Link.new('Engine.cpp', 'DataAccess/DA.h', false), 20 | Link.new('Engine.cpp', 'Engine.h', false)] 21 | expect(include_dependency_graph.links('Engine')).to eq(expected_links) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/test/dir_tree_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json' 4 | 5 | require 'cpp_dependency_graph/dir_tree' 6 | 7 | RSpec.describe DirTree do 8 | let(:dir_tree) { DirTree.new('spec/test/example_project') } 9 | 10 | it 'returns empty tree an unknwon directory' do 11 | expect(DirTree.new('asdsadklasd').tree.empty?).to eq(true) 12 | end 13 | 14 | it 'returns all directories and its subdirectories as a tree hash structure' do 15 | # expected_tree = JSON.parse('{ 16 | # "name": "spec/test/example_project", 17 | # "children": [{ 18 | # "name": "DataAccess", 19 | # "children": [] 20 | # }, { 21 | # "name": "Engine", 22 | # "children": [] 23 | # }, { 24 | # "name": "Framework", 25 | # "children": [] 26 | # }, { 27 | # "name": "main", 28 | # "children": [] 29 | # }, { 30 | # "name": "System", 31 | # "children": [] 32 | # }, { 33 | # "name": "UI", 34 | # "children": [] 35 | # }] 36 | # }') 37 | # expect(dir_tree.tree).to eq(expected_tree) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Shreyas Balakrishna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/dir_tree.rb: -------------------------------------------------------------------------------- 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 | 26 | full_path = File.join(path, entry) 27 | next unless File.directory?(full_path) 28 | 29 | next unless source_files_present?(full_path) 30 | 31 | data[:children] << parse_dirs(full_path, entry) 32 | end 33 | data 34 | end 35 | 36 | def source_files_present?(full_path) 37 | files = Dir.glob(File.join(full_path, File.join('**', '*' + source_file_extensions))) 38 | files.size.positive? 39 | end 40 | 41 | def to_s 42 | @tree.to_json 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/include_component_dependency_graph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'project' 4 | require_relative 'link' 5 | require_relative 'cycle_detector' 6 | 7 | # Returns a hash of intra-component include links 8 | class IncludeComponentDependencyGraph 9 | def initialize(project) 10 | @project = project 11 | end 12 | 13 | def all_links 14 | @project.source_files.map do |file| 15 | links = file.includes.map { |inc| Link.new(file.basename, inc, false) } 16 | [file.basename, links] 17 | end.to_h 18 | end 19 | 20 | def all_cyclic_links 21 | # TODO: Implement 22 | end 23 | 24 | def links(component_name) 25 | component = @project.source_component(component_name) 26 | p component 27 | source_files = component.source_files 28 | external_includes = @project.external_includes(component) 29 | source_files.map do |file| 30 | # TODO: Very inefficient 31 | internal_includes = file.includes.reject { |inc| external_includes.any?(inc) } 32 | links = internal_includes.map { |inc| Link.new(file.basename, inc, false) } 33 | [file.basename, links] 34 | end.to_h 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/test/source_component_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/source_component' 4 | 5 | RSpec.describe SourceComponent do 6 | it 'has a name attribute that matches the directory' do 7 | component = SourceComponent.new('spec/test/example_project/Engine') 8 | expect(component.name).to eq('Engine') 9 | end 10 | 11 | it 'has a path attribute that matches the directory' do 12 | component = SourceComponent.new('spec/test/example_project/Engine') 13 | expect(component.path).to eq('spec/test/example_project/Engine') 14 | end 15 | 16 | it 'parses all source files within it' do 17 | source_file_names = SourceComponent.new('spec/test/example_project/Engine').source_files.map(&:basename) 18 | expect(source_file_names).to contain_exactly('Engine.cpp', 'Engine.h', 'OldEngine.h') 19 | end 20 | 21 | it 'has an includes attribute' do 22 | component = SourceComponent.new('spec/test/example_project/Engine') 23 | expect(component.includes).to contain_exactly('framework.h', 'Display.h', 'DA.h', 'Engine.h') 24 | end 25 | 26 | it 'has a loc (lines of code) attribute' do 27 | component = SourceComponent.new('spec/test/example_project/Engine') 28 | expect(component.loc).to be > 0 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/source_file.rb: -------------------------------------------------------------------------------- 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 | includes = file_contents.scan(/#include ["|<](.+)["|>]/) # TODO: use compiler lib to scan includes? llvm/clang? 45 | includes.uniq! 46 | includes.flatten 47 | end 48 | 49 | def file_contents 50 | @file_contents ||= sanitised_file_contents 51 | end 52 | 53 | def sanitised_file_contents 54 | contents = File.read(@path) 55 | return contents if contents.valid_encoding? 56 | 57 | contents.encode('UTF-16be', invalid: :replace, replace: '?').encode('UTF-8') 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/test/source_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/source_file' 4 | 5 | RSpec.describe SourceFile do 6 | it 'has a path attribute that matches the path of the file' do 7 | source_file = SourceFile.new('spec/test/example_project/Engine/Engine.cpp') 8 | expect(source_file.path).to eq('spec/test/example_project/Engine/Engine.cpp') 9 | end 10 | 11 | it 'has a basename attribute that matches the file basename' do 12 | source_file = SourceFile.new('spec/test/example_project/Engine/Engine.cpp') 13 | expect(source_file.basename).to eq('Engine.cpp') 14 | end 15 | 16 | it 'has a basename no extension attribute that matches the file basename without extension' do 17 | source_file = SourceFile.new('spec/test/example_project/Engine/Engine.cpp') 18 | expect(source_file.basename_no_extension).to eq('Engine') 19 | end 20 | 21 | it 'has a parent component attribute that matches the directory the file lives in' do 22 | source_file = SourceFile.new('spec/test/example_project/Engine/Engine.cpp') 23 | expect(source_file.parent_component).to eq('Engine') 24 | end 25 | 26 | it 'has an includes attribute that contains all includes' do 27 | source_file = SourceFile.new('spec/test/example_project/Engine/Engine.cpp') 28 | expect(source_file.includes).to eq(['DataAccess/DA.h', 'Engine.h']) 29 | end 30 | 31 | it 'has a loc (lines of code) attribute' do 32 | source_file = SourceFile.new('spec/test/example_project/Engine/Engine.cpp') 33 | expect(source_file.loc).to be > 0 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/file_dependency_graph.rb: -------------------------------------------------------------------------------- 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 | 24 | links = incoming_links(name) 25 | links.merge!(outgoing_links(name)) 26 | links 27 | end 28 | 29 | private 30 | 31 | def build_hash_links 32 | raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c)] }.to_h 33 | @cycle_detector ||= CycleDetector.new(raw_links) 34 | links = raw_links.map do |source, source_links| 35 | c_links = source_links.map { |target| Link.new(source, target, @cycle_detector.cyclic?(source, target)) } 36 | [source, c_links] 37 | end.to_h 38 | links 39 | end 40 | 41 | def build_cyclic_links 42 | cyclic_links = all_links.select { |_, links| links.any?(&:cyclic?) } 43 | cyclic_links.each_value { |links| links.select!(&:cyclic?) } 44 | end 45 | 46 | def outgoing_links(name) 47 | all_links.slice(name) 48 | end 49 | 50 | def incoming_links(target) 51 | incoming_c_links = all_links.select { |_, c_links| c_links.any? { |link| link.target == target } } 52 | incoming_c_links.map do |source, _| 53 | link = Link.new(source, target, @cycle_detector.cyclic?(source, target)) 54 | [source, [link]] 55 | end.to_h 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/component_dependency_graph.rb: -------------------------------------------------------------------------------- 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 | 24 | links = incoming_links(name) 25 | links.merge!(outgoing_links(name)) 26 | links 27 | end 28 | 29 | private 30 | 31 | def build_hash_links 32 | raw_links = @project.source_components.values.map { |c| [c.name, @project.dependencies(c) || [] ] }.to_h 33 | @cycle_detector ||= CycleDetector.new(raw_links) 34 | links = raw_links.map do |source, source_links| 35 | c_links = source_links.map { |target| Link.new(source, target, @cycle_detector.cyclic?(source, target)) } 36 | [source, c_links] 37 | end.to_h 38 | links 39 | end 40 | 41 | def build_cyclic_links 42 | cyclic_links = all_links.select { |_, links| links.any?(&:cyclic?) } 43 | cyclic_links.each_value { |links| links.select!(&:cyclic?) } 44 | end 45 | 46 | def outgoing_links(name) 47 | all_links.slice(name) 48 | end 49 | 50 | def incoming_links(target) 51 | incoming_c_links = all_links.select { |_, c_links| c_links.any? { |link| link.target == target } } 52 | incoming_c_links.map do |source, _| 53 | link = Link.new(source, target, @cycle_detector.cyclic?(source, target)) 54 | [source, [link]] 55 | end.to_h 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Cpp Dependency Graph 4 | tagline: Visualise C/C++ projects with amazing visualisations 5 | description: Tutorial on how to use Cpp Dependency Graph 6 | --- 7 | 8 | # Cpp Dependency Graph 9 | 10 | Generates useful component dependency visualisations (`dot` or `d3.js`) to study the architecture of C/C++ projects. 11 | 12 | Why do all the other languages have awesome tools to analyse codebases but C/C++ does not? 13 | 14 | It's time to change that. 15 | 16 | This tool aims to - 17 | 18 | - provide multiple views into the architecture of a codebase 19 | - generate views at multiple levels of the architecture 20 | - make the resulting views genuinely useful, rich, dynamic and interactive (static views are boring) 21 | 22 | ## Inspiration 23 | 24 | This tool is inspired by a number of projects [rubrowser](http://www.emadelsaid.com/rubrowser/), [cpp-dependencies](https://github.com/tomtom-international/cpp-dependencies) and [objc-dependency-visualizer](https://github.com/PaulTaykalo/objc-dependency-visualizer). 25 | 26 | The pretty `d3` visualisations are directly copied from `objc-dependency-visualiser`. 27 | 28 | A huge shout out to the people behind these projects. 29 | 30 | ## Usage tutorial 31 | ### Overall component dependency graph 32 | 33 | To generate the overall component dependency graph for a project, use it like so - 34 | 35 | `cpp_dependency_graph visualise -r spec\test\example_project\ -o deps.svg -f svg` 36 | 37 | Below is the overall `dot` and `d3` component dependency visualisations for [leveldb](https://github.com/google/leveldb) 38 | 39 | ![Dot](examples/leveldb_overall.svg) 40 | 41 | ![d3.js visualisation of leveldb](examples/leveldb_overall_d3.svg) 42 | 43 | ![d3.js visualisation of rocksdb](examples/rocksdb_overall_d3.svg) 44 | -------------------------------------------------------------------------------- /cpp_dependency_graph.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | require 'cpp_dependency_graph/version' 7 | 8 | Gem::Specification.new do |s| 9 | s.name = 'cpp_dependency_graph' 10 | s.version = CppDependencyGraph::VERSION 11 | s.authors = ['Shreyas Balakrishna'] 12 | s.email = ['shreyasbharath@gmail.com'] 13 | s.summary = <<-SUMMARY 14 | CppDependencyGraph is a program that generates dependency visualisations (dot, d3.js) to study the architecture of C/C++ projects 15 | SUMMARY 16 | s.description = <<-DESCRIPTION 17 | Generates interactive dependency visualisations (dot, d3.js) to study the architecture of C/C++ projects in detail 18 | DESCRIPTION 19 | s.homepage = 'https://github.com/shreyasbharath/cpp_dependency_graph' 20 | s.licenses = ['MIT'] 21 | 22 | s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } - 23 | %w[.rubocop.yml .travis.yml appveyor.yml] 24 | s.bindir = 'exe' 25 | s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) } 26 | s.require_paths = ['lib'] 27 | 28 | s.required_ruby_version = Gem::Requirement.new('>= 3.1.0') 29 | s.rubygems_version = '3.3.4' 30 | 31 | s.add_runtime_dependency 'docopt' 32 | s.add_runtime_dependency 'json' 33 | s.add_runtime_dependency 'ruby-graphviz' 34 | 35 | s.add_development_dependency 'bundler' 36 | s.add_development_dependency 'rake' 37 | s.add_development_dependency 'rspec' 38 | s.add_development_dependency 'rubocop' 39 | s.add_development_dependency 'rufo' 40 | s.add_development_dependency 'ruby-debug-ide' 41 | s.add_development_dependency 'ruby-prof' 42 | s.add_development_dependency 'simplecov' 43 | s.add_development_dependency 'simplecov-console' 44 | end 45 | -------------------------------------------------------------------------------- /spec/test/project_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/project' 4 | 5 | RSpec.describe Project do 6 | it 'parses overall project component' do 7 | component_names = Project.new('spec/test/example_project').project_component.values.map(&:name) 8 | expect(component_names).to contain_exactly('example_project') 9 | end 10 | 11 | it 'parses all source components' do 12 | component_names = Project.new('spec/test/example_project').source_components.values.map(&:name) 13 | expect(component_names).to contain_exactly('DataAccess', 'Engine', 'Framework', 'System', 'UI', 'main') 14 | end 15 | 16 | it 'returns null component if case does not match' do 17 | component = Project.new('spec/test/example_project').source_component('enGine') 18 | expect(component.name).to eq('NULL') 19 | expect(component.source_files.size).to eq(0) 20 | end 21 | 22 | it 'returns null component if no such component exists' do 23 | component = Project.new('spec/test/example_project').source_component('Unknown') 24 | expect(component.name).to eq('NULL') 25 | expect(component.source_files.size).to eq(0) 26 | end 27 | 28 | it 'returns dependencies of component' do 29 | project = Project.new('spec/test/example_project') 30 | component = project.source_component('Engine') 31 | dependencies = project.dependencies(component) 32 | expect(dependencies).to include('Framework', 'UI', 'DataAccess') 33 | end 34 | 35 | it 'all source files of project' do 36 | project = Project.new('spec/test/example_project') 37 | source_files = project.source_files.values.map(&:basename) 38 | expect(source_files).to contain_exactly('DA.h', 'Display.cpp', 'Display.h', 'Engine.cpp', 'Engine.h', 'Engine.h', 39 | 'OldEngine.h', 'System.cpp', 'System.h', 'framework.h', 'main.cpp') 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | dist: bionic 3 | cache: bundler 4 | sudo: false 5 | rvm: 6 | - 2.7 7 | before_install: 8 | - gem update --system 9 | - gem update --remote bundler 10 | - gem --version 11 | - gem install bundler 12 | - bundle --version 13 | branches: 14 | only: 15 | - master 16 | env: 17 | global: 18 | - CC_TEST_REPORTER_ID=b32bade44482d3debfa79056a4843ba58c77802d3fb5d358be90692389eec70a 19 | jobs: 20 | include: 21 | - stage: test 22 | script: bundle exec rake rubocop 23 | - stage: test 24 | before_script: 25 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 26 | - chmod +x ./cc-test-reporter 27 | - ./cc-test-reporter before-build 28 | script: bundle exec rake spec 29 | after_script: 30 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 31 | - stage: build 32 | script: bundle exec rake build 33 | - stage: gem release 34 | script: echo "Deploying to rubygems.org ..." 35 | deploy: 36 | provider: rubygems 37 | gem: cpp_dependency_graph 38 | on: 39 | tags: true 40 | api_key: 41 | secure: oIzNdzho4Jb3/I9PCjpTpWnlPky00sqVSFtPKz0GPe4XE+nvPymX8ZSbnboHshVjI/4bYkxZPyaa8cjYsdpSzFFEiCM/6T6tWQpualpfXQ/dLsaj4Nlw2GGGfIm3XcL2aEbO9zfc2sroy7Hdy6lL9vZw/sm2YaN4dxn1PwIcgQqsNL4WTuFdejPHk02Lp1cbGcvNhA6GkuNigg0R1x+heEPetL9wL2JW/uEak12o8RgEG/3+AQbw290OCFi4x80ig/StOzmkCgcCMgcOhURfNgsJaXJxjjK/+6eWw/jAv1KG5cqQ8SnOeJUwx4xXu22hO645UUunx0U+qte+ay2RoL95/veD1FZc3FtYwSphzhGKhvlGspW/FlqC8U0TVSRWJFNobZDxHM7iYiK4OB9yTeGKIC/poZMZ5a9Ht4wqISuGa9wBmXtXhKvqfvQ1nrHc1J9bZj4p20DhY1V4JXPlExWXbZLzlXxTeBSKsjH/WGUdho9DpdJPMyiSGOv/YYWdUuUvW+xm8qQ/dRHaiKHRsOdOB9QoIIXUPF2hVN2SplK5cImLQw7KW+DeQFVgqHsdPVDfpjCtMTeDhwxlfHncf5+8nslnNRWOvn0eGZ5vp86NkfNLXOG+JgcptrucUK/6q1hRqFOLHSETOzpgTzQujNhGihMZYsG1QehXasDYxDg= 42 | -------------------------------------------------------------------------------- /spec/test/component_dependency_graph_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cpp_dependency_graph/component_dependency_graph' 4 | require 'cpp_dependency_graph/link' 5 | 6 | RSpec.describe ComponentDependencyGraph do 7 | let(:project) { Project.new('spec/test/example_project') } 8 | let(:dependency_graph) { ComponentDependencyGraph.new(project) } 9 | 10 | it 'returns all links for a project' do 11 | links = dependency_graph.all_links 12 | expect(links['UI']).to contain_exactly(Link.new('UI', 'Engine', true), Link.new('UI', 'Framework', false)) 13 | expect(links['DataAccess']).to contain_exactly(Link.new('DataAccess', 'Framework', false)) 14 | expect(links['main']).to contain_exactly(Link.new('main', 'UI', false)) 15 | expect(links['Framework']).to be_empty 16 | expect(links['System']).to be_empty 17 | expect(links['Engine']).to contain_exactly(Link.new('Engine', 'DataAccess', false), Link.new('Engine', 'Framework', false), 18 | Link.new('Engine', 'UI', true)) 19 | end 20 | 21 | it 'returns empty links for an unknown component of a project' do 22 | expect(dependency_graph.links('Blah').empty?).to eq(true) 23 | end 24 | 25 | it 'returns links for a specified component of a project' do 26 | links = dependency_graph.links('Engine') 27 | expect(links['Engine']).to contain_exactly(Link.new('Engine', 'DataAccess', false), Link.new('Engine', 'Framework', false), 28 | Link.new('Engine', 'UI', true)) 29 | expect(links['UI']).to contain_exactly(Link.new('UI', 'Engine', true)) 30 | end 31 | 32 | it 'returns all cyclic links of a project' do 33 | links = dependency_graph.all_cyclic_links 34 | expect(links['Engine']).to contain_exactly(Link.new('Engine', 'UI', true)) 35 | expect(links['UI']).to contain_exactly(Link.new('UI', 'Engine', true)) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'find' 4 | 5 | require_relative 'directory_parser' 6 | require_relative 'include_to_component_resolver' 7 | require_relative 'source_component' 8 | require_relative 'logging' 9 | 10 | # Parses all components of a project 11 | class Project 12 | include DirectoryParser 13 | include Logging 14 | 15 | def initialize(path) 16 | @path = path 17 | @include_resolver = IncludeToComponentResolver.new(source_components) 18 | end 19 | 20 | def source_components 21 | @source_components ||= build_source_components 22 | end 23 | 24 | def source_component(name) 25 | return SourceComponent.new('NULL') unless source_components.key?(name) 26 | 27 | source_components[name] 28 | end 29 | 30 | def project_component 31 | @project_component ||= build_project_component 32 | end 33 | 34 | def source_files 35 | @source_files ||= build_source_files 36 | end 37 | 38 | def dependencies(component) 39 | # TODO: This is repeating the same work twice! component_for_include is called when calling external_includes 40 | external_includes(component).map { |include| @include_resolver.component_for_include(include) }.reject(&:empty?).uniq 41 | end 42 | 43 | def external_includes(component) 44 | @include_resolver.external_includes(component) 45 | end 46 | 47 | private 48 | 49 | def build_source_files 50 | # TODO: Breaking Demeter's law here 51 | files = project_component.values.flat_map(&:source_files) 52 | files.map do |file| 53 | [file.path, file] 54 | end.to_h 55 | end 56 | 57 | def build_project_component 58 | c = SourceComponent.new(@path) 59 | { c.name => c } 60 | end 61 | 62 | def build_source_components 63 | # TODO: Dealing with source components with same dir name? 64 | dirs = fetch_all_dirs(@path) 65 | components = dirs.map do |dir| 66 | logger.debug "Parsing #{dir}" 67 | c = SourceComponent.new(dir) 68 | [c.name, c] 69 | end.to_h 70 | components.delete_if { |_, v| v.source_files.size.zero? } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph/include_to_component_resolver.rb: -------------------------------------------------------------------------------- 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 | 23 | @component_include_map_cache[include] = component_for_include_private(include) unless @component_include_map_cache.key?(include) 24 | @component_include_map_cache[include] 25 | end 26 | 27 | private 28 | 29 | def external_includes_private(component) 30 | include_components = component.includes.map { |inc| [inc, component_for_include(inc)] }.to_h 31 | external_include_components = include_components.delete_if { |_, c| c == component.name } 32 | external_include_components.keys 33 | end 34 | 35 | def component_for_include_private(include) 36 | header_file = source_files[include] 37 | implementation_files = implementation_files(header_file) 38 | return header_file.parent_component if implementation_files.empty? 39 | 40 | implementation_files[0].parent_component 41 | end 42 | 43 | def implementation_files(header_file) 44 | implementation_files = [] 45 | source_files.each_value do |file| 46 | implementation_files.push(file) if file.basename_no_extension == header_file.basename_no_extension 47 | end 48 | implementation_files.reject! { |file| file.basename == header_file.basename } 49 | end 50 | 51 | def source_files 52 | @source_files ||= build_source_file_map 53 | end 54 | 55 | def build_source_file_map 56 | # TODO: SourceComponent should return a hash for source files which can be merged here 57 | source_files = @components.values.flat_map(&:source_files) 58 | source_files.map { |s| [s.basename, s] }.to_h 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch index.html", 9 | "type": "chrome", 10 | "request": "launch", 11 | "breakOnLoad": true, 12 | "sourceMapPathOverrides": { 13 | "webRoot": "${workspaceRoot}" 14 | }, 15 | "file": "${workspaceRoot}/index.html" 16 | }, 17 | { 18 | "name": "Debug Local File", 19 | "type": "Ruby", 20 | "request": "launch", 21 | "cwd": "${workspaceRoot}", 22 | "program": "${workspaceRoot}/exe/cpp_dependency_graph", 23 | "useBundler": true, 24 | "pathToBundler": "C:/tools/ruby25/bin/bundle.bat", 25 | "pathToRDebugIDE": "C:/tools/ruby25/bin/rdebug-ide.bat", 26 | "showDebuggerOutput": true, 27 | "args": ["visualise_components", "-r", "../TileDB"] 28 | }, 29 | { 30 | "name": "Listen for rdebug-ide", 31 | "type": "Ruby", 32 | "request": "attach", 33 | "cwd": "${workspaceRoot}", 34 | "remoteHost": "127.0.0.1", 35 | "remotePort": "1234", 36 | "remoteWorkspaceRoot": "${workspaceRoot}" 37 | }, 38 | { 39 | "name": "Rails server", 40 | "type": "Ruby", 41 | "request": "launch", 42 | "cwd": "${workspaceRoot}", 43 | "program": "${workspaceRoot}/bin/rails", 44 | "args": [ 45 | "server" 46 | ] 47 | }, 48 | { 49 | "name": "RSpec - all", 50 | "type": "Ruby", 51 | "request": "launch", 52 | "cwd": "${workspaceRoot}", 53 | "program": "C:/tools/ruby25/bin/rspec", 54 | "args": [ 55 | "-I", 56 | "${workspaceRoot}" 57 | ] 58 | }, 59 | { 60 | "name": "RSpec - active spec file only", 61 | "type": "Ruby", 62 | "request": "launch", 63 | "cwd": "${workspaceRoot}", 64 | "program": "C:/tools/ruby25/bin/rspec", 65 | "args": [ 66 | "-I", 67 | "${workspaceRoot}", 68 | "${file}" 69 | ] 70 | }, 71 | { 72 | "name": "Cucumber", 73 | "type": "Ruby", 74 | "request": "launch", 75 | "cwd": "${workspaceRoot}", 76 | "program": "${workspaceRoot}/bin/cucumber" 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at shreyasbharath@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /views/circle_packing.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 31 | 32 | 33 | 106 | -------------------------------------------------------------------------------- /lib/cpp_dependency_graph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "cpp_dependency_graph/circle_packing_visualiser" 4 | require_relative "cpp_dependency_graph/component_dependency_graph" 5 | require_relative "cpp_dependency_graph/dir_tree" 6 | require_relative "cpp_dependency_graph/graph_to_html_visualiser" 7 | require_relative "cpp_dependency_graph/graph_to_svg_visualiser" 8 | require_relative "cpp_dependency_graph/include_component_dependency_graph" 9 | require_relative "cpp_dependency_graph/include_file_dependency_graph" 10 | require_relative "cpp_dependency_graph/project" 11 | require_relative "cpp_dependency_graph/version" 12 | require_relative "cpp_dependency_graph/logging" 13 | 14 | # Generates dependency graphs of a project in various output forms 15 | module CppDependencyGraph 16 | include Logging 17 | 18 | def generate_project_graph(project_dir, format, output_file) 19 | logger.info "Resolving source directories in project..." 20 | project = Project.new(project_dir) 21 | logger.info "Resolving dependencies between components..." 22 | graph = ComponentDependencyGraph.new(project) 23 | logger.info "Generating graph..." 24 | deps = graph.all_links 25 | logger.info "Generating visualisation..." 26 | generate_visualisation(deps, format, output_file) 27 | end 28 | 29 | def generate_component_graph(project_dir, component, format, output_file) 30 | project = Project.new(project_dir) 31 | graph = ComponentDependencyGraph.new(project) 32 | deps = graph.links(component) 33 | generate_visualisation(deps, format, output_file) 34 | end 35 | 36 | def generate_file_include_graph(project_dir, file_name, format, output_file) 37 | project = Project.new(project_dir) 38 | graph = IncludeFileDependencyGraph.new(project) 39 | deps = graph.links(file_name) 40 | generate_visualisation(deps, format, output_file) 41 | end 42 | 43 | def generate_component_include_graph(project_dir, component_name, format, output_file) 44 | project = Project.new(project_dir) 45 | graph = IncludeComponentDependencyGraph.new(project) 46 | deps = graph.links(component_name) 47 | generate_visualisation(deps, format, output_file) 48 | end 49 | 50 | def generate_project_include_graph(project_dir, format, output_file) 51 | project = Project.new(project_dir) 52 | graph = IncludeComponentDependencyGraph.new(project) 53 | deps = graph.all_links 54 | generate_visualisation(deps, format, output_file) 55 | end 56 | 57 | def generate_enclosure_diagram(project_dir, output_file) 58 | dir_tree = DirTree.new(project_dir) 59 | tree = dir_tree.tree 60 | CirclePackingVisualiser.new.generate(tree, output_file) 61 | end 62 | 63 | def generate_cyclic_dependencies(project_dir, format, file) 64 | project = Project.new(project_dir) 65 | graph = ComponentDependencyGraph.new(project) 66 | deps = graph.all_cyclic_links 67 | generate_visualisation(deps, format, file) 68 | end 69 | 70 | def generate_visualisation(deps, format, file) 71 | case format 72 | when "svg" 73 | GraphToSvgVisualiser.new.generate(deps, file) 74 | when "html" 75 | GraphToHtmlVisualiser.new.generate(deps, file) 76 | when "json" 77 | File.write(file, JSON.pretty_generate(deps)) 78 | end 79 | end 80 | 81 | def list_components(project_dir) 82 | logger.info "Resolving source directories in project..." 83 | project = Project.new(project_dir) 84 | project.source_components.each do |name, instance| 85 | logger.info "Component: #{name}, path: #{instance.path}" 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO Items 2 | 3 | - [ ] Documentation 4 | - [ ] Screencast of how to use tool on projects 5 | - [ ] Create a github.io homepage 6 | - [ ] Add interactive `html` or `svg` examples to homepage 7 | - [ ] Add a [CONTRIBUTING.md](https://github.com/nayafia/contributing-template/blob/master/CONTRIBUTING-template.md) 8 | - [ ] Ruby gem docs 9 | - [ ] Add example visualisations for "good" vs "bad" architectures 10 | - [ ] Progress messages 11 | - [ ] Progress bar? 12 | - [x] Allow user to specify a single component and the tool should print only that component 13 | - [x] Print out dependency graphs for all components by default if no single component is specified 14 | - [ ] Use a compiler to get the includes for a file rather than manually scanning the contents 15 | - [ ] Command to generate overall file/class dependency graph if a project doesn't have many components 16 | - [ ] Command to generate individual file/class dependency graph 17 | - [ ] Manual scanning does not work when #includes are #ifdef'ed out for example 18 | - [ ] include vs "project" include (how will this work for third party sources that are not part of the codebase?) 19 | - [ ] Work with any type of include relative includes ('blah.h') vs absolute includes ('/path/blah.h') vs relative with path includes ('blah/blah.h') 20 | - [ ] Look at https://github.com/Sarcasm/compdb to see if it can generate dependencies 21 | - [ ] Parallelise dependency scanning as much as possible to get the best possible performance 22 | - [ ] Use a threadpool? Look at reference implementations 23 | - [ ] Open up the graph automatically after generating an individual graph 24 | - [ ] Work with any sort of project structure 25 | - [ ] Header only projects 26 | - [x] source and header files for component in same directory 27 | - [ ] source files in same directory but header files in a separate `inc` directory 28 | - [x] source files in same directory but header files in a global `inc` directory (all project header files in one place) 29 | - [ ] source files and header files don't have matching names (i.e. there's an `api.h` header file and various source files implement them) 30 | - [ ] Try it out on various small/large C/C++ open source projects 31 | - [ ] Provide coupling/cohesion metrics, lookup metrics from Clean Architecture and [this](https://softwareengineering.stackexchange.com/questions/151004/are-there-metrics-for-cohesion-and-coupling) 32 | - [ ] Ignore list? Some components may not want to be seen 33 | - [ ] Use a yaml config file? A pain to pass a whole heap of arguments every time 34 | - [ ] Make the tool incremental? Only generate parts of the new graph if something has changed. Something to think about 35 | - [ ] Handling `duplicate` component names? Use a unique identifier (perhaps the path?) 36 | - [ ] Visualisation 37 | - [ ] Highlight strongly coupled components (i.e. have lots of outgoing/incoming dependencies). How to visualise strongly coupled components? 38 | - [ ] Interface vs implementation coupling (interface is worse!). Highlighting interface vs implementation coupling between components on graph? 39 | - [ ] Hierarchy diagram for components with no cycles? (https://bl.ocks.org/mbostock/4339184) 40 | - [ ] Pack diagram for just visualising components (https://bl.ocks.org/mbostock/ca5b03a33affa4160321) 41 | - [ ] Look at using subgraphs of the dot/svg language to cluster component dependencies in the graph 42 | - [ ] Create a d3 donut graph with relative sizes of components in project? This'll probably show which components need to be further split up (something like this https://blog.kathyreid.id.au/2016/12/29/linux-australia-expense-breakdown-a-data-visualisation-in-d3-js/) 43 | - [ ] Node size - base it on how many source files (or lines of code) or how many connections going in/out of node? 44 | - [ ] Provide a 'zoom' slider on the visualisation to zoom in/out of the view (high level dependencies to low-level dependencies) 45 | - [ ] Visualise components matching user provided regex only 46 | - [ ] 3D visualisation using something like https://github.com/ggeoffrey/cljs-gravity 47 | -------------------------------------------------------------------------------- /exe/cpp_dependency_graph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | #-- 6 | # Copyright (c) 2018 Shreyas Balakrishna 7 | 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | #++ 26 | 27 | require 'docopt' 28 | require 'cpp_dependency_graph' 29 | 30 | # require 'ruby-prof' 31 | 32 | include CppDependencyGraph 33 | 34 | # RubyProf.start 35 | 36 | doc = < --component [--output_file ] [--output_format ] 41 | cpp_dependency_graph visualise_project --root_dir [--output_file ] [--output_format ] 42 | cpp_dependency_graph visualise_component_includes --root_dir --component [--output_file ] [--output_format ] 43 | cpp_dependency_graph visualise_header_includes --root_dir --header
[--output_file ] [--output_format ] 44 | cpp_dependency_graph visualise_project_includes --root_dir [--output_file ] [--output_format ] 45 | cpp_dependency_graph visualise_components --root_dir [--output_file ] [--output_format ] 46 | cpp_dependency_graph visualise_cyclic_deps --root_dir [--component ] [--output_file ] [--output_format ] 47 | cpp_dependency_graph list_components --root_dir 48 | cpp_dependency_graph -h | --help | -v | --version 49 | 50 | Options: 51 | -h --help show this help message and exit 52 | -v --version show version and exit 53 | -r --root_dir dir top level root directory of C/C++ project 54 | -o --output_file file name of output file to be generated [default: deps.html] 55 | -f --output_format format format of output file (svg, html, graphml, json) [default: html] 56 | --component component component generate visualisation for (case sensitive!) 57 | --header file header file to generate visualisation for (case sensitive!) 58 | DOCOPT 59 | 60 | begin 61 | args = Docopt.docopt(doc) 62 | 63 | if args['--version'] 64 | puts VERSION 65 | Kernel.exit(0) 66 | end 67 | 68 | project_dir = args['--root_dir'].tr('\\', '/') 69 | 70 | unless File.directory?(project_dir) 71 | puts('Not a valid project source directory') 72 | Kernel.exit(1) 73 | end 74 | 75 | if args['visualise_component'] 76 | generate_component_graph(project_dir, args['--component'], args['--output_format'], args['--output_file']) 77 | elsif args['visualise_project'] 78 | generate_project_graph(project_dir, args['--output_format'], args['--output_file']) 79 | elsif args['visualise_component_includes'] 80 | generate_component_include_graph(project_dir, args['--component'], args['--output_format'], args['--output_file']) 81 | elsif args['visualise_header_includes'] 82 | generate_file_include_graph(project_dir, args['--header'], args['--output_format'], args['--output_file']) 83 | elsif args['visualise_project_includes'] 84 | generate_project_include_graph(project_dir, args['--output_format'], args['--output_file']) 85 | elsif args['visualise_components'] 86 | generate_enclosure_diagram(project_dir, args['--output_file']) 87 | elsif args['visualise_cyclic_deps'] 88 | generate_cyclic_dependencies(project_dir, args['--output_format'], args['--output_file']) 89 | elsif args['list_components'] 90 | list_components(project_dir) 91 | end 92 | rescue Docopt::Exit => e 93 | puts e.message 94 | end 95 | 96 | # result = RubyProf.stop 97 | # printer = RubyProf::CallTreePrinter.new(result) 98 | # printer.print(path: '.', profile: 'cpp_dependency_graph') 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cpp Dependency Graph 2 | 3 | [![Join the chat at https://gitter.im/cpp_dependency_graph/Lobby](https://badges.gitter.im/cpp_dependency_graph/Lobby.svg)](https://gitter.im/cpp_dependency_graph/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Build Status](https://travis-ci.org/shreyasbharath/cpp_dependency_graph.svg?branch=master)](https://travis-ci.org/shreyasbharath/cpp_dependency_graph) 7 | [![Maintainability](https://api.codeclimate.com/v1/badges/2a07b587ca6fc8b1b3db/maintainability)](https://codeclimate.com/github/shreyasbharath/cpp_dependency_graph/maintainability) 8 | [![Codacy](https://api.codacy.com/project/badge/Grade/9439dbb7fde44b5380401acba5325e62)](https://www.codacy.com/app/shreyasbharath/cpp_dependency_graph?utm_source=github.com&utm_medium=referral&utm_content=shreyasbharath/cpp_dependency_graph&utm_campaign=Badge_Grade) 9 | [![Test Coverage](https://api.codeclimate.com/v1/badges/2a07b587ca6fc8b1b3db/test_coverage)](https://codeclimate.com/github/shreyasbharath/cpp_dependency_graph/test_coverage) 10 | [![Gem Version](https://badge.fury.io/rb/cpp_dependency_graph.svg)](https://badge.fury.io/rb/cpp_dependency_graph) 11 | 12 | Generates useful component dependency visualisations (`dot` or `d3.js`) to study the architecture of C/C++ projects. 13 | 14 | Why do all the other languages have awesome tools to analyse codebases but C/C++ does not? 15 | 16 | It's time to change that. 17 | 18 | This tool aims to - 19 | 20 | - provide multiple views into the architecture of a codebase 21 | - generate views at multiple levels of the architecture 22 | - make the resulting views genuinely useful, rich, dynamic and interactive (static views are boring) 23 | 24 | ## Inspiration 25 | 26 | This tool is inspired by a number of projects [rubrowser](http://www.emadelsaid.com/rubrowser/), [cpp-dependencies](https://github.com/tomtom-international/cpp-dependencies) and [objc-dependency-visualizer](https://github.com/PaulTaykalo/objc-dependency-visualizer). 27 | 28 | The pretty `d3` visualisations are directly copied from `objc-dependency-visualiser`. 29 | 30 | A huge shout out to the people behind these projects. 31 | 32 | ## Comparison With Other Tools 33 | 34 | ### cpp-dependencies 35 | 36 | ### cinclude2dot 37 | 38 | ### dep-matrix 39 | 40 | ## Usage 41 | 42 | ### Installation 43 | 44 | `gem install cpp_dependency_graph` 45 | 46 | ### Help 47 | 48 | `cpp_dependency_graph -h` 49 | 50 | ### Overall component dependency graph 51 | 52 | To generate the overall component dependency graph for a project, use it like so - 53 | 54 | `cpp_dependency_graph visualise_project -r spec\test\example_project\ -o deps.svg -f svg` 55 | 56 | Below is the overall `dot` and `d3` component dependency visualisations for [leveldb](https://github.com/google/leveldb) 57 | 58 | ![Dot](docs/examples/leveldb_overall.svg) 59 | 60 | ![d3.js visualisation of leveldb](docs/examples/leveldb_overall_d3.svg) 61 | 62 | ![d3.js visualisation of rocksdb](docs/examples/rocksdb_overall_d3.svg) 63 | 64 | **NOTE** - If your project has a large number of components (> 100 and lots of connections between them), then generation (and subsequent rendering) may take some time. 65 | 66 | ### Individual component dependency graph 67 | 68 | This will highlight the dependencies coming in and going out of a specific component. This allows you to filter out extraneous detail and study individual components in more detail. 69 | 70 | `cpp_dependency_graph visualise_component -r spec\test\example_project\ --component Engine -o deps.dot -f dot` 71 | 72 | Here's a component dependency visualisation generated for the `queue` component in [rethinkdb](https://github.com/rethinkdb/rethinkdb) 73 | 74 | ![Queue component dot visualisation](docs/examples/rethinkdb_queue_component.svg) 75 | 76 | ![Queue component d3 visualisation](docs/examples/rethinkdb_queue_component_d3.svg) 77 | 78 | ### Component include dependency graph 79 | 80 | This will highlight dependencies of includes within a specific component 81 | 82 | `cpp_dependency_graph visualise_component_includes -r spec\test\example_project\ --component Engine` 83 | 84 | Here's a component include dependency visualisation generated for the `queue` component in [rethinkdb](https://github.com/rethinkdb/rethinkdb) 85 | 86 | ![Queue include graph dot](docs/examples/rethinkdb_queue_include.svg) 87 | 88 | ![Queue include graph d3](docs/examples/rethinkdb_queue_include_d3.svg) 89 | 90 | ### Header file include dependency graph 91 | 92 | This will highlight include dependencies of header files globally within the project 93 | 94 | `cpp_dependency_graph visualise_header_includes -r spec\test\example_project\ --header Engine.h` 95 | 96 | Here's a component include dependency visualisation generated for the `errors.hpp` header file in [rethinkdb](https://github.com/rethinkdb/rethinkdb) 97 | 98 | ![Errors.hpp include graph dot](docs/examples/rethinkdb_errors_header_include.svg) 99 | 100 | ### Cyclic dependencies only graph 101 | 102 | This will highlight cyclic dependencies between components within a project. This is especially useful for targeted refactoring activities to reduce coupling between components. 103 | 104 | `cpp_dependency_graph visualise_cyclic_deps -r spec\test\example_project\` 105 | 106 | Here's the cyclic dependencies only visualisation generated for [rethinkdb](https://github.com/rethinkdb/rethinkdb) and [leveldb](https://github.com/google/leveldb) 107 | 108 | ![rethinkdb](docs/examples/rethinkdb_cyclic_deps.svg) 109 | 110 | ![leveldb](docs/examples/leveldb_cyclic_deps.svg) 111 | 112 | ## Development 113 | 114 | `bundle exec cpp_dependency_graph -r ...` 115 | 116 | ### Running all unit tests 117 | 118 | `rake spec` 119 | 120 | ### Running a single test 121 | 122 | `rake spec SPEC=` 123 | 124 | ## License 125 | 126 | cpp_dependency_graph is available under the MIT license. 127 | 128 | ## Warranty 129 | 130 | This software is provided "as is" and without any express or implied 131 | warranties, including, without limitation, the implied warranties of 132 | merchantability and fitness for a particular purpose. 133 | -------------------------------------------------------------------------------- /docs/examples/leveldb_cyclic_deps.svg: -------------------------------------------------------------------------------- 1 | dbtableleveldbutilport -------------------------------------------------------------------------------- /docs/examples/leveldb_overall.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependency_graph 11 | 12 | 13 | db 14 | 15 | 16 | 17 | 18 | db 19 | 20 | 21 | util 22 | 23 | 24 | 25 | 26 | util 27 | 28 | 29 | db->util 30 | 31 | 32 | 33 | 34 | leveldb 35 | 36 | 37 | 38 | 39 | leveldb 40 | 41 | 42 | db->leveldb 43 | 44 | 45 | 46 | 47 | table 48 | 49 | 50 | 51 | 52 | table 53 | 54 | 55 | db->table 56 | 57 | 58 | 59 | 60 | port 61 | 62 | 63 | 64 | 65 | port 66 | 67 | 68 | db->port 69 | 70 | 71 | 72 | 73 | win 74 | 75 | 76 | 77 | 78 | win 79 | 80 | 81 | db->win 82 | 83 | 84 | 85 | 86 | util->leveldb 87 | 88 | 89 | 90 | 91 | util->port 92 | 93 | 94 | 95 | 96 | util->win 97 | 98 | 99 | 100 | 101 | leveldb->util 102 | 103 | 104 | 105 | 106 | leveldb->table 107 | 108 | 109 | 110 | 111 | leveldb->win 112 | 113 | 114 | 115 | 116 | table->db 117 | 118 | 119 | 120 | 121 | table->util 122 | 123 | 124 | 125 | 126 | table->leveldb 127 | 128 | 129 | 130 | 131 | table->port 132 | 133 | 134 | 135 | 136 | table->win 137 | 138 | 139 | 140 | 141 | port->util 142 | 143 | 144 | 145 | 146 | port->win 147 | 148 | 149 | 150 | 151 | bench 152 | 153 | 154 | 155 | 156 | bench 157 | 158 | 159 | bench->util 160 | 161 | 162 | 163 | 164 | memenv 165 | 166 | 167 | 168 | 169 | memenv 170 | 171 | 172 | memenv->db 173 | 174 | 175 | 176 | 177 | memenv->util 178 | 179 | 180 | 181 | 182 | memenv->leveldb 183 | 184 | 185 | 186 | 187 | memenv->port 188 | 189 | 190 | 191 | 192 | issues 193 | 194 | 195 | 196 | 197 | issues 198 | 199 | 200 | issues->db 201 | 202 | 203 | 204 | 205 | issues->util 206 | 207 | 208 | 209 | 210 | issues->leveldb 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_component.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependency_graph 11 | 12 | 13 | io 14 | 15 | 16 | 17 | 18 | io 19 | 20 | 21 | queue 22 | 23 | 24 | 25 | 26 | queue 27 | 28 | 29 | io->queue 30 | 31 | 32 | 33 | 34 | concurrency 35 | 36 | 37 | 38 | 39 | concurrency 40 | 41 | 42 | queue->concurrency 43 | 44 | 45 | 46 | 47 | containers 48 | 49 | 50 | 51 | 52 | containers 53 | 54 | 55 | queue->containers 56 | 57 | 58 | 59 | 60 | src 61 | 62 | 63 | 64 | 65 | src 66 | 67 | 68 | queue->src 69 | 70 | 71 | 72 | 73 | archive 74 | 75 | 76 | 77 | 78 | archive 79 | 80 | 81 | queue->archive 82 | 83 | 84 | 85 | 86 | arch 87 | 88 | 89 | 90 | 91 | arch 92 | 93 | 94 | queue->arch 95 | 96 | 97 | 98 | 99 | runtime 100 | 101 | 102 | 103 | 104 | runtime 105 | 106 | 107 | queue->runtime 108 | 109 | 110 | 111 | 112 | perfmon 113 | 114 | 115 | 116 | 117 | perfmon 118 | 119 | 120 | queue->perfmon 121 | 122 | 123 | 124 | 125 | disk 126 | 127 | 128 | 129 | 130 | disk 131 | 132 | 133 | disk->queue 134 | 135 | 136 | 137 | 138 | btree 139 | 140 | 141 | 142 | 143 | btree 144 | 145 | 146 | btree->queue 147 | 148 | 149 | 150 | 151 | client_protocol 152 | 153 | 154 | 155 | 156 | client_protocol 157 | 158 | 159 | client_protocol->queue 160 | 161 | 162 | 163 | 164 | generic 165 | 166 | 167 | 168 | 169 | generic 170 | 171 | 172 | generic->queue 173 | 174 | 175 | 176 | 177 | immediate_consistency 178 | 179 | 180 | 181 | 182 | immediate_consistency 183 | 184 | 185 | immediate_consistency->queue 186 | 187 | 188 | 189 | 190 | concurrency->queue 191 | 192 | 193 | 194 | 195 | rdb_protocol 196 | 197 | 198 | 199 | 200 | rdb_protocol 201 | 202 | 203 | rdb_protocol->queue 204 | 205 | 206 | 207 | 208 | datum_stream 209 | 210 | 211 | 212 | 213 | datum_stream 214 | 215 | 216 | datum_stream->queue 217 | 218 | 219 | 220 | 221 | unittest 222 | 223 | 224 | 225 | 226 | unittest 227 | 228 | 229 | unittest->queue 230 | 231 | 232 | 233 | 234 | 235 | -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_include.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependency_graph 11 | 12 | 13 | accounting.hpp 14 | 15 | 16 | 17 | 18 | accounting.hpp 19 | 20 | 21 | concurrency/queue/passive_producer.hpp 22 | 23 | 24 | 25 | 26 | concurrency/queue/passive_producer.hpp 27 | 28 | 29 | accounting.hpp->concurrency/queue/passive_producer.hpp 30 | 31 | 32 | 33 | 34 | containers/intrusive_list.hpp 35 | 36 | 37 | 38 | 39 | containers/intrusive_list.hpp 40 | 41 | 42 | accounting.hpp->containers/intrusive_list.hpp 43 | 44 | 45 | 46 | 47 | disk_backed_queue_wrapper.hpp 48 | 49 | 50 | 51 | 52 | disk_backed_queue_wrapper.hpp 53 | 54 | 55 | disk_backed_queue_wrapper.hpp->concurrency/queue/passive_producer.hpp 56 | 57 | 58 | 59 | 60 | concurrency/auto_drainer.hpp 61 | 62 | 63 | 64 | 65 | concurrency/auto_drainer.hpp 66 | 67 | 68 | disk_backed_queue_wrapper.hpp->concurrency/auto_drainer.hpp 69 | 70 | 71 | 72 | 73 | concurrency/wait_any.hpp 74 | 75 | 76 | 77 | 78 | concurrency/wait_any.hpp 79 | 80 | 81 | disk_backed_queue_wrapper.hpp->concurrency/wait_any.hpp 82 | 83 | 84 | 85 | 86 | containers/archive/archive.hpp 87 | 88 | 89 | 90 | 91 | containers/archive/archive.hpp 92 | 93 | 94 | disk_backed_queue_wrapper.hpp->containers/archive/archive.hpp 95 | 96 | 97 | 98 | 99 | containers/archive/vector_stream.hpp 100 | 101 | 102 | 103 | 104 | containers/archive/vector_stream.hpp 105 | 106 | 107 | disk_backed_queue_wrapper.hpp->containers/archive/vector_stream.hpp 108 | 109 | 110 | 111 | 112 | containers/disk_backed_queue.hpp 113 | 114 | 115 | 116 | 117 | containers/disk_backed_queue.hpp 118 | 119 | 120 | disk_backed_queue_wrapper.hpp->containers/disk_backed_queue.hpp 121 | 122 | 123 | 124 | 125 | limited_fifo.hpp 126 | 127 | 128 | 129 | 130 | limited_fifo.hpp 131 | 132 | 133 | limited_fifo.hpp->concurrency/queue/passive_producer.hpp 134 | 135 | 136 | 137 | 138 | concurrency/semaphore.hpp 139 | 140 | 141 | 142 | 143 | concurrency/semaphore.hpp 144 | 145 | 146 | limited_fifo.hpp->concurrency/semaphore.hpp 147 | 148 | 149 | 150 | 151 | perfmon/types.hpp 152 | 153 | 154 | 155 | 156 | perfmon/types.hpp 157 | 158 | 159 | limited_fifo.hpp->perfmon/types.hpp 160 | 161 | 162 | 163 | 164 | passive_producer.hpp 165 | 166 | 167 | 168 | 169 | passive_producer.hpp 170 | 171 | 172 | arch/runtime/coroutines.hpp 173 | 174 | 175 | 176 | 177 | arch/runtime/coroutines.hpp 178 | 179 | 180 | passive_producer.hpp->arch/runtime/coroutines.hpp 181 | 182 | 183 | 184 | 185 | unlimited_fifo.hpp 186 | 187 | 188 | 189 | 190 | unlimited_fifo.hpp 191 | 192 | 193 | unlimited_fifo.hpp->concurrency/queue/passive_producer.hpp 194 | 195 | 196 | 197 | 198 | perfmon/perfmon.hpp 199 | 200 | 201 | 202 | 203 | perfmon/perfmon.hpp 204 | 205 | 206 | unlimited_fifo.hpp->perfmon/perfmon.hpp 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/examples/leveldb_overall_d3.svg: -------------------------------------------------------------------------------- 1 | dbutilleveldbtableportwinbenchmemenvissues -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_include_d3.svg: -------------------------------------------------------------------------------- 1 | accounting.hppconcurrency/queue/passive_producer.hppcontainers/intrusive_list.hppdisk_backed_queue_wrapper.hppconcurrency/auto_drainer.hppconcurrency/wait_any.hppcontainers/archive/archive.hppcontainers/archive/vector_stream.hppcontainers/disk_backed_queue.hpplimited_fifo.hppconcurrency/semaphore.hppperfmon/types.hpppassive_producer.hpparch/runtime/coroutines.hppunlimited_fifo.hppperfmon/perfmon.hpp -------------------------------------------------------------------------------- /docs/examples/rethinkdb_queue_component_d3.svg: -------------------------------------------------------------------------------- 1 | ioqueuediskbtreeclient_protocolgenericimmediate_consistencyconcurrencyrdb_protocoldatum_streamunittestcontainerssrcarchivearchruntimeperfmon --------------------------------------------------------------------------------