deps_grapher 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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