deps_grapher 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/Gemfile +16 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +327 -0
  6. data/Rakefile +12 -0
  7. data/bin/console +15 -0
  8. data/bin/deps_grapher +96 -0
  9. data/bin/setup +8 -0
  10. data/deps_grapher.gemspec +34 -0
  11. data/lefthook.yml +14 -0
  12. data/lib/deps_grapher/ast_processor.rb +211 -0
  13. data/lib/deps_grapher/ast_processor_policy.rb +45 -0
  14. data/lib/deps_grapher/cache_file.rb +48 -0
  15. data/lib/deps_grapher/cli.rb +29 -0
  16. data/lib/deps_grapher/command/analyzer.rb +89 -0
  17. data/lib/deps_grapher/command/init.rb +49 -0
  18. data/lib/deps_grapher/configuration.rb +99 -0
  19. data/lib/deps_grapher/context.rb +48 -0
  20. data/lib/deps_grapher/cytoscape/cose.rb +36 -0
  21. data/lib/deps_grapher/cytoscape/fcose.rb +36 -0
  22. data/lib/deps_grapher/cytoscape/klay.rb +27 -0
  23. data/lib/deps_grapher/cytoscape/template.erb +61 -0
  24. data/lib/deps_grapher/cytoscape.rb +88 -0
  25. data/lib/deps_grapher/dsl.rb +50 -0
  26. data/lib/deps_grapher/edge.rb +58 -0
  27. data/lib/deps_grapher/errors.rb +8 -0
  28. data/lib/deps_grapher/event.rb +63 -0
  29. data/lib/deps_grapher/graph.rb +71 -0
  30. data/lib/deps_grapher/graphile/generator.rb +42 -0
  31. data/lib/deps_grapher/graphile/graphile.erb +119 -0
  32. data/lib/deps_grapher/graphile/graphile.temp.erb +23 -0
  33. data/lib/deps_grapher/html_writer.rb +16 -0
  34. data/lib/deps_grapher/input.rb +40 -0
  35. data/lib/deps_grapher/layer/registry.rb +32 -0
  36. data/lib/deps_grapher/layer.rb +75 -0
  37. data/lib/deps_grapher/logging.rb +21 -0
  38. data/lib/deps_grapher/matcher.rb +28 -0
  39. data/lib/deps_grapher/node.rb +65 -0
  40. data/lib/deps_grapher/plugin_dsl.rb +12 -0
  41. data/lib/deps_grapher/plugin_loader.rb +29 -0
  42. data/lib/deps_grapher/source.rb +51 -0
  43. data/lib/deps_grapher/source_cache/class_name_extractor.rb +60 -0
  44. data/lib/deps_grapher/source_cache/registry.rb +64 -0
  45. data/lib/deps_grapher/source_cache.rb +49 -0
  46. data/lib/deps_grapher/version.rb +5 -0
  47. data/lib/deps_grapher/vis/box.rb +21 -0
  48. data/lib/deps_grapher/vis/dot.rb +17 -0
  49. data/lib/deps_grapher/vis/template.erb +36 -0
  50. data/lib/deps_grapher/vis.rb +69 -0
  51. data/lib/deps_grapher/visualizer/base.rb +74 -0
  52. data/lib/deps_grapher/visualizer/color/registry.rb +30 -0
  53. data/lib/deps_grapher/visualizer/color.rb +62 -0
  54. data/lib/deps_grapher/visualizer/command_option.rb +22 -0
  55. data/lib/deps_grapher/visualizer/downloader.rb +42 -0
  56. data/lib/deps_grapher/visualizer/js_option.rb +39 -0
  57. data/lib/deps_grapher/visualizer/registry.rb +37 -0
  58. data/lib/deps_grapher/visualizer.rb +11 -0
  59. data/lib/deps_grapher.rb +42 -0
  60. metadata +135 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../cytoscape"
4
+
5
+ module DepsGrapher
6
+ class Cytoscape
7
+ class Klay < self
8
+ command_option "cy:klay", default: true
9
+
10
+ private
11
+
12
+ def required_js
13
+ super.tap do |js|
14
+ js << "https://unpkg.com/klayjs/klay.js"
15
+ js << "https://raw.githubusercontent.com/cytoscape/cytoscape.js-klay/master/cytoscape-klay.js"
16
+ end
17
+ end
18
+
19
+ def layout_options
20
+ Visualizer::JsOption.new(
21
+ name: :klay,
22
+ nodeDimensionsIncludeLabels: true
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Cytoscape</title>
6
+ <meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no″>
7
+ <% required_js.each do |js| -%>
8
+ <script src="<%= File.basename js %>"></script>
9
+ <% end -%>
10
+ <style>
11
+ #cy {
12
+ width: 100vw;
13
+ height: 100vh;
14
+ }
15
+ </style>
16
+ <script>
17
+ const backgroundColorMap = <%= color_map(:background).to_json %>;
18
+ document.addEventListener('DOMContentLoaded', function () {
19
+ const cy = cytoscape({
20
+ container: document.getElementById('cy'),
21
+ elements: <%= data.to_json %>,
22
+ layout: <%= layout_options %>,
23
+ style: [
24
+ {
25
+ selector: 'node',
26
+ style: {
27
+ 'background-color': function(ele) {
28
+ return backgroundColorMap[ele.data('layer')];
29
+ },
30
+ 'label': 'data(label)',
31
+ 'font-size': '6%',
32
+ 'width': function(ele) {
33
+ return Math.max(<%= min_width %>, ele.data('deps_count') * <%= coefficient %>);
34
+ },
35
+ 'height': function(ele) {
36
+ return Math.max(<%= min_height %>, ele.data('deps_count') * <%= coefficient %>);
37
+ }
38
+ }
39
+ },
40
+ <% Layer.names.each do |layer_name| -%>
41
+ {
42
+ selector: 'edge[layer="<%= layer_name %>"]',
43
+ style: {
44
+ 'width': 1,
45
+ 'line-color': '<%= arrow_color(layer_name) %>',
46
+ 'target-arrow-color': '<%= arrow_color(layer_name) %>',
47
+ 'target-arrow-shape': 'triangle',
48
+ 'curve-style': 'bezier'
49
+ }
50
+ },
51
+ <% end -%>
52
+ ],
53
+ });
54
+ <%= advanced_render %>
55
+ });
56
+ </script>
57
+ </head>
58
+ <body>
59
+ <div id="cy"></div>
60
+ </body>
61
+ </html>
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "visualizer/base"
4
+
5
+ module DepsGrapher
6
+ class Cytoscape < Visualizer::Base
7
+ def required_js
8
+ [
9
+ "https://unpkg.com/cytoscape/dist/cytoscape.min.js",
10
+ "https://unpkg.com/layout-base/layout-base.js"
11
+ ]
12
+ end
13
+
14
+ private
15
+
16
+ def template_path
17
+ File.expand_path File.join("cytoscape", "template.erb"), __dir__
18
+ end
19
+
20
+ def min_width
21
+ "10"
22
+ end
23
+
24
+ def min_height
25
+ "10"
26
+ end
27
+
28
+ def coefficient
29
+ "0.2 * 10"
30
+ end
31
+
32
+ def layout_options
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def advanced_render
37
+ ""
38
+ end
39
+
40
+ def data
41
+ [].tap do |array|
42
+ @nodes.each do |node|
43
+ next if skip_node?(node)
44
+
45
+ array << convert_node(node)
46
+ end
47
+
48
+ @edges.each do |edge|
49
+ next if skip_edge?(edge)
50
+
51
+ array << convert_edge(edge)
52
+ end
53
+ end
54
+ end
55
+
56
+ def convert_node(node)
57
+ {
58
+ group: :nodes,
59
+ data: {
60
+ id: node.id,
61
+ layer: node.layer,
62
+ label: node.label,
63
+ deps_count: node.deps_count
64
+ }
65
+ }
66
+ end
67
+
68
+ def convert_edge(edge)
69
+ {
70
+ group: :edges,
71
+ data: {
72
+ id: edge.id,
73
+ source: edge.from.id,
74
+ target: edge.to.id,
75
+ layer: edge.from.layer
76
+ }
77
+ }
78
+ end
79
+
80
+ def skip_node?(node)
81
+ options[:layers].present? && !options[:layers].include?(node.layer)
82
+ end
83
+
84
+ def skip_edge?(edge)
85
+ skip_node?(edge.from) || skip_node?(edge.to)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class DSL
5
+ def initialize(obj)
6
+ @obj = obj
7
+ end
8
+
9
+ def respond_to_missing?(symbol, include_private = false)
10
+ ["#{symbol}=", symbol].each do |method_name|
11
+ return true if @obj.respond_to?(method_name)
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ private
18
+
19
+ def method_missing(symbol, *args, &block)
20
+ ["#{symbol}=", symbol].each do |method_name|
21
+ next unless @obj.respond_to?(method_name)
22
+
23
+ parameters = @obj.method(method_name).parameters
24
+
25
+ return @obj.send(method_name) if parameters.empty?
26
+
27
+ if parameters.size == 1
28
+ parameter = parameters.first
29
+ case parameter[0]
30
+ when :req, :opt
31
+ return @obj.send(method_name, args.first.freeze)
32
+ when :block
33
+ return @obj.send(method_name, &block)
34
+ else
35
+ raise ArgumentError, "unsupported parameter type: #{parameter[0]}"
36
+ end
37
+ end
38
+
39
+ next unless parameters.size == 2
40
+
41
+ arg_type_1st, arg_type_2nd = parameters.flat_map(&:first)
42
+ return @obj.send(method_name, args.first.freeze, &block) if %i[req opt].include?(arg_type_1st) && arg_type_2nd == :block
43
+
44
+ raise ArgumentError, "unsupported parameter types: #{arg_type_1st}, #{arg_type_2nd}"
45
+ end
46
+
47
+ super
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Edge
5
+ class << self
6
+ def fetch(from, to)
7
+ id = generate_id from, to
8
+ registry.fetch id
9
+ end
10
+
11
+ def add(from, to)
12
+ return nil if from.nil? || to.nil?
13
+
14
+ id = generate_id from, to
15
+
16
+ return nil if registry.key?(id) || from.id == to.id
17
+
18
+ to.parent = from.id
19
+ to.increment_deps_count!
20
+
21
+ registry[id] = new id, from, to
22
+ end
23
+
24
+ def all
25
+ registry.values
26
+ end
27
+
28
+ private
29
+
30
+ def generate_id(from, to)
31
+ "e:#{from.id}:#{to.id}"
32
+ end
33
+
34
+ def registry
35
+ @registry ||= {}
36
+ end
37
+ end
38
+
39
+ private_class_method :new
40
+
41
+ attr_accessor :id, :from, :to
42
+
43
+ def initialize(id, from, to)
44
+ @id = id
45
+ @from = from
46
+ @to = to
47
+ end
48
+
49
+ def eql?(other)
50
+ other.is_a?(self.class) && id == other.id
51
+ end
52
+ alias == eql?
53
+
54
+ def hash
55
+ id.hash
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Error < StandardError; end
5
+ class TargetNodeNotFound < Error; end
6
+ class SourceCacheNotFound < Error; end
7
+ class SourceLocationNotFound < Error; end
8
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Event
5
+ class << self
6
+ def generate_key(name, const_name, method)
7
+ return nil if const_name.nil? && method.nil?
8
+
9
+ key = name.to_s.dup
10
+ key << ".#{const_name}" if const_name
11
+ key << ".#{method}" if method
12
+ key
13
+ end
14
+
15
+ def add(name:, const_name:, location:, method: nil)
16
+ key = generate_key name, const_name, method
17
+
18
+ return nil if key.nil?
19
+
20
+ registry[key] ||= new(
21
+ name: name,
22
+ const_name: const_name,
23
+ location: location,
24
+ method: method
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ def registry
31
+ @registry ||= {}
32
+ end
33
+ end
34
+
35
+ private_class_method :new
36
+
37
+ attr_accessor :name, :const_name, :location, :method
38
+
39
+ def initialize(name:, const_name:, location:, method:)
40
+ @name = name
41
+ @const_name = const_name
42
+ @location = location
43
+ @method = method
44
+ @skip_processing = false
45
+ end
46
+
47
+ def key
48
+ self.class.generate_key name, const_name, method
49
+ end
50
+
51
+ def processing!
52
+ @skip_processing = false
53
+ end
54
+
55
+ def skip_processing!
56
+ @skip_processing = true
57
+ end
58
+
59
+ def processing?
60
+ !@skip_processing
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class NullGraph
5
+ def add_edge(_); end
6
+ end
7
+
8
+ class Graph
9
+ def initialize
10
+ @graph = {}
11
+ end
12
+
13
+ def add_edge(edge)
14
+ @graph[edge.from] ||= []
15
+ @graph[edge.from] << edge.to
16
+ end
17
+
18
+ def find_path(source_path_matcher, target_path_matcher)
19
+ paths = []
20
+ visited = {}
21
+
22
+ @graph.each_key do |start_node|
23
+ next if source_path_matcher && !source_path_matcher.match?(start_node.class_name)
24
+
25
+ dfs start_node, target_path_matcher, visited, [start_node], paths
26
+ end
27
+
28
+ nodes = Set.new
29
+ edges = Set.new
30
+
31
+ paths.each do |path|
32
+ collect! path, nodes, edges
33
+ end
34
+
35
+ [nodes, edges]
36
+ end
37
+
38
+ private
39
+
40
+ def dfs(current, target_path_matcher, visited, path, paths)
41
+ return if visited[current]
42
+
43
+ visited[current] = true
44
+
45
+ if target_path_matcher.match?(current.class_name)
46
+ paths << path
47
+ else
48
+ @graph[current]&.each do |node|
49
+ dfs node, target_path_matcher, visited, path + [node], paths
50
+ end
51
+ end
52
+
53
+ visited[current] = false
54
+ end
55
+
56
+ def collect!(path, nodes, edges)
57
+ current_node = nil
58
+ path.each do |node|
59
+ nodes << node
60
+
61
+ if current_node.nil?
62
+ current_node = node
63
+ next
64
+ end
65
+
66
+ edges << Edge.fetch(current_node, node)
67
+ current_node = node
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ module Graphile
5
+ class Generator
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def call(dest)
11
+ if dest
12
+ File.write dest, read_template(temp: false)
13
+ return dest
14
+ end
15
+
16
+ path = nil
17
+
18
+ # unlink this temp file by GC
19
+ Tempfile.open("graphile_generator", config.cache_dir) do |f|
20
+ f.write read_template(temp: true)
21
+ path = f.path
22
+ end
23
+
24
+ path
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :config
30
+
31
+ def read_template(temp:)
32
+ b = config.instance_eval { binding }
33
+ ERB.new(File.read(resolve_path(temp)), trim_mode: "-").result(b)
34
+ end
35
+
36
+ def resolve_path(temp)
37
+ file_name = temp ? "graphile.temp.erb" : "graphile.erb"
38
+ File.expand_path(file_name, __dir__)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # directory settings
4
+ # customize for your project
5
+ output_dir File.expand_path File.join("tmp", "deps_grapher", "graph")
6
+ cache_dir File.expand_path File.join("tmp", "deps_grapher", "cache")
7
+
8
+ root_dir = "<%= root_dir %>"
9
+ lib_root_dir = File.join(root_dir, "lib")
10
+ layer_root_dir = File.join(lib_root_dir, "deps_grapher")
11
+
12
+ # default visualizer setting
13
+ visualizer "<%= visualizer %>"
14
+
15
+ ast_processor_policy do
16
+ exclude_const /\ADepsGrapher\z/
17
+
18
+ # advanced_const_resolver do |ast_node|
19
+ # # some advanced logic using ast_node of parser gem
20
+ # # this block should return a string of const name or nil
21
+ # # if return nil, the const name will be resolved by default logic
22
+ # end
23
+ end
24
+
25
+ layer do
26
+ name :deps_grapher
27
+ visible true
28
+
29
+ source do
30
+ root lib_root_dir
31
+ exclude_pattern %r{/(command|cytoscape|vis|visualizer)}
32
+ end
33
+
34
+ color do
35
+ background "#FF5252"
36
+ border "#EF5350"
37
+ font "#FF5252"
38
+ end
39
+ end
40
+
41
+ layer do
42
+ name :command
43
+ visible true
44
+
45
+ source do
46
+ root File.join(layer_root_dir, "command")
47
+ end
48
+
49
+ color do
50
+ background "#512DA8"
51
+ border "#673AB7"
52
+ font "#512DA8"
53
+ end
54
+ end
55
+
56
+ layer do
57
+ name :cytoscape
58
+ visible true
59
+
60
+ source do
61
+ root layer_root_dir
62
+ glob_pattern ["cytoscape.rb", "cytoscape/**/*.rb"]
63
+ end
64
+
65
+ color do
66
+ background "#448AFF"
67
+ border "#42A5F5"
68
+ font "#448AFF"
69
+ end
70
+ end
71
+
72
+ layer do
73
+ name :vis
74
+ visible true
75
+
76
+ source do
77
+ root layer_root_dir
78
+ glob_pattern ["vis.rb", "vis/**/*.rb"]
79
+ end
80
+
81
+ color do
82
+ background "#00B8D4"
83
+ border "#00ACC1"
84
+ font "#00B8D4"
85
+ end
86
+ end
87
+
88
+ layer do
89
+ name :visualizer
90
+ visible true
91
+
92
+ source do
93
+ root layer_root_dir
94
+ glob_pattern ["visualizer.rb", "visualizer/**/*.rb"]
95
+ end
96
+
97
+ color do
98
+ background "#00C853"
99
+ border "#4CAF50"
100
+ font "#00C853"
101
+ end
102
+ end
103
+
104
+ with_plugin do |plugin_dir|
105
+ layer do
106
+ name :plugin
107
+ visible true
108
+
109
+ source do
110
+ root plugin_dir
111
+ end
112
+
113
+ color do
114
+ background "#607D8B"
115
+ border "#78909C"
116
+ font "#607D8B"
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ output_dir File.join(Dir.home, ".deps_grapher", "graph")
4
+ cache_dir File.join(Dir.home, ".deps_grapher", "cache")
5
+ cache_ttl 60 * 5 # 5 minutes
6
+
7
+ root_dir = "<%= root_dir %>"
8
+
9
+ layer do
10
+ name :<%= File.basename(root_dir) %>
11
+ visible true
12
+
13
+ source do
14
+ root root_dir
15
+ exclude_pattern %r{/(?:vendor|spec)/}
16
+ end
17
+
18
+ color do
19
+ background "#448AFF"
20
+ border "#42A5F5"
21
+ font "#448AFF"
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class HtmlWriter
5
+ attr_reader :path
6
+
7
+ def initialize(output_dir)
8
+ FileUtils.mkdir_p output_dir
9
+ @path = File.join output_dir, "index.html"
10
+ end
11
+
12
+ def write(content)
13
+ File.write path, content
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Input
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ def files
10
+ FileUtils.rm_rf File.dirname(config.cache_dir) if config.clean
11
+
12
+ set_layers_visibility!
13
+ extract_files_from_layers
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :config
19
+
20
+ def set_layers_visibility!
21
+ layer_visibility = config.visualizer_options[:layers]
22
+
23
+ config.layers.each_value do |layer|
24
+ layer.visible = layer_visibility.include? layer.name
25
+ end
26
+ end
27
+
28
+ def extract_files_from_layers
29
+ config.layers.values.select(&:visible).each_with_object([]) do |layer, files|
30
+ source = layer.source
31
+
32
+ source.files.each do |file|
33
+ next if file == config.path
34
+
35
+ files << file
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Layer
5
+ module Registry
6
+ class << self
7
+ def fetch(file_path)
8
+ registry[file_path] || Layer::Default
9
+ end
10
+ alias [] fetch
11
+
12
+ def register(file_path, layer)
13
+ registry[file_path] ||= layer
14
+ end
15
+
16
+ def exist?(file_path)
17
+ registry.key? file_path
18
+ end
19
+
20
+ def all
21
+ registry.values
22
+ end
23
+
24
+ private
25
+
26
+ def registry
27
+ @registry ||= {}
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end