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,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "layer/registry"
4
+ require_relative "visualizer/color"
5
+
6
+ module DepsGrapher
7
+ class Layer
8
+ class << self
9
+ def fetch(file_path)
10
+ Registry.fetch file_path
11
+ end
12
+
13
+ def names
14
+ Registry.all.to_set(&:name)
15
+ end
16
+
17
+ def visible_names
18
+ Registry.all.select(&:visible).to_set(&:name)
19
+ end
20
+
21
+ def exist?(file_path)
22
+ Registry.exist? file_path
23
+ end
24
+ end
25
+
26
+ attr_accessor :name, :visible
27
+
28
+ def initialize(&block)
29
+ @visible = true
30
+ DSL.new(self).instance_eval(&block)
31
+
32
+ assert!
33
+
34
+ return if default?
35
+
36
+ source.files.each do |file|
37
+ Registry.register file, self
38
+ end
39
+
40
+ SourceCache.register! name, source
41
+ end
42
+
43
+ def source(&block)
44
+ @source_defined = true if block
45
+ @source ||= Source.new(name, &block)
46
+ end
47
+
48
+ def color(&block)
49
+ @color_defined = true if block
50
+ Visualizer::Color.new(name, &block)
51
+ end
52
+
53
+ private
54
+
55
+ def assert!
56
+ raise ArgumentError, "layer: no `name` given" unless name
57
+ raise ArgumentError, "layer `#{name}` has no `source` block" unless default? || @source_defined
58
+ raise ArgumentError, "layer `#{name}` has no `color` block" unless @color_defined
59
+ end
60
+
61
+ def default?
62
+ name == :__default
63
+ end
64
+
65
+ Default = (new do
66
+ name :__default
67
+ visible true
68
+ color do
69
+ background "#BDBDBD"
70
+ border "#9E9E9E"
71
+ font "#BDBDBD"
72
+ end
73
+ end).freeze
74
+ end
75
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ module Logging
5
+ def info(&block)
6
+ DepsGrapher.logger.info(&block)
7
+ end
8
+
9
+ def warn(&block)
10
+ DepsGrapher.logger.warn(&block)
11
+ end
12
+
13
+ def error(&block)
14
+ DepsGrapher.logger.error(&block)
15
+ end
16
+
17
+ def verbose(&block)
18
+ DepsGrapher.logger.info(&block) if DepsGrapher.config.verbose
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Matcher
5
+ def initialize(pattern)
6
+ @regexp = convert_to_regexp pattern
7
+ end
8
+
9
+ def match?(value)
10
+ @regexp.match? value
11
+ end
12
+
13
+ def to_s
14
+ @regexp.inspect
15
+ end
16
+ alias inspect to_s
17
+
18
+ private
19
+
20
+ def convert_to_regexp(pattern)
21
+ if pattern.is_a?(Regexp)
22
+ pattern
23
+ else
24
+ Regexp.new("\\A#{pattern.gsub(/\.?\*/, ".*")}\\z")
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Node
5
+ class << self
6
+ def guid
7
+ @guid ||= 0
8
+ @guid += 1
9
+ end
10
+
11
+ def fetch(class_name)
12
+ registry[class_name]
13
+ end
14
+
15
+ def add(class_name, location)
16
+ return nil if class_name.nil? || location.nil?
17
+
18
+ registry[class_name] ||= new class_name, location
19
+ end
20
+
21
+ def all
22
+ registry.values
23
+ end
24
+
25
+ private
26
+
27
+ def registry
28
+ @registry ||= {}
29
+ end
30
+ end
31
+
32
+ private_class_method :new
33
+
34
+ attr_accessor :id, :class_name, :location, :parent, :deps_count
35
+
36
+ def initialize(class_name, location)
37
+ @id = "n#{self.class.guid}"
38
+ @class_name = class_name
39
+ @location = location
40
+ @parent = nil
41
+ @deps_count = 0
42
+ end
43
+
44
+ def label
45
+ deps_count.positive? ? "#{class_name} (#{deps_count})" : class_name
46
+ end
47
+
48
+ def layer
49
+ @layer ||= Layer.fetch(location).name
50
+ end
51
+
52
+ def increment_deps_count!
53
+ @deps_count += 1
54
+ end
55
+
56
+ def eql?(other)
57
+ other.is_a?(self.class) && id == other.id
58
+ end
59
+ alias == eql?
60
+
61
+ def hash
62
+ id.hash
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ module PluginDSL
5
+ def with_plugin(&block)
6
+ plugin_dir = DepsGrapher.config.plugin_dir
7
+ return if plugin_dir.blank? || !File.directory?(plugin_dir)
8
+
9
+ block.call plugin_dir
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class PluginLoader
5
+ class << self
6
+ def load!(plugin_dir)
7
+ new(plugin_dir).load!
8
+ end
9
+ end
10
+
11
+ private_class_method :new
12
+
13
+ def initialize(plugin_dir)
14
+ @plugin_dir = plugin_dir
15
+ end
16
+
17
+ def load!
18
+ return if plugin_dir.blank? || !Dir.exist?(plugin_dir)
19
+
20
+ Dir.glob(File.join(plugin_dir, "**", "*.rb")).sort.each do |file|
21
+ require file
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :plugin_dir
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class Source
5
+ attr_accessor :root, :glob_pattern
6
+
7
+ def initialize(name, &block)
8
+ @name = name
9
+ @include_pattern = nil
10
+ @exclude_pattern = nil
11
+
12
+ DSL.new(self).instance_eval(&block)
13
+
14
+ assert!
15
+
16
+ @glob_pattern = Array(glob_pattern.presence || File.join("**", "*.rb")).each_with_object([]) do |pattern, array|
17
+ array << File.join(root, pattern)
18
+ end
19
+ end
20
+
21
+ def files
22
+ Dir.glob(glob_pattern).sort.uniq.each_with_object([]) do |file, files|
23
+ next if include_pattern && !include_pattern.match?(file)
24
+ next if exclude_pattern&.match?(file)
25
+
26
+ files << file
27
+ end
28
+ end
29
+
30
+ def include_pattern=(pattern)
31
+ @include_pattern = Matcher.new pattern
32
+ end
33
+
34
+ def exclude_pattern=(pattern)
35
+ @exclude_pattern = Matcher.new pattern
36
+ end
37
+
38
+ def to_s
39
+ "glob_pattern: #{glob_pattern.inspect}, include_pattern: #{include_pattern.inspect}, exclude_pattern: #{exclude_pattern.inspect}"
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :include_pattern, :exclude_pattern
45
+
46
+ def assert!
47
+ raise ArgumentError, "source: no `root` given" if root.blank?
48
+ raise ArgumentError, "source: directory not found `#{root}`" unless Dir.exist?(root)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class SourceCache
5
+ class ClassNameExtractor < Parser::AST::Processor
6
+ class << self
7
+ def cache
8
+ @cache ||= Set.new
9
+ end
10
+ end
11
+
12
+ def initialize(&block)
13
+ super()
14
+ @block = block
15
+ end
16
+
17
+ def extract!(file_path)
18
+ source_buffer = Parser::Source::Buffer.new(file_path)
19
+ parser = Parser::CurrentRuby.new
20
+ process parser.parse(source_buffer.read)
21
+ end
22
+
23
+ def on_module(node)
24
+ const_node = node.children[0]
25
+ name = extract_const_name const_node
26
+
27
+ @namespace_stack ||= []
28
+ @namespace_stack.push name
29
+
30
+ super
31
+
32
+ const_name = @namespace_stack.join("::")
33
+
34
+ unless self.class.cache.include? const_name
35
+ self.class.cache << const_name
36
+ @block.call const_name, node.location.name.source_buffer.name
37
+ end
38
+
39
+ @namespace_stack.pop
40
+ end
41
+ alias on_class on_module
42
+
43
+ private
44
+
45
+ def extract_const_name(node)
46
+ case node.type
47
+ when :const
48
+ base, name = *node
49
+ if base.nil?
50
+ name.to_s
51
+ else
52
+ [extract_const_name(base), name].compact.join "::"
53
+ end
54
+ else
55
+ raise "unexpected node type: #{node.type}"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class SourceCache
5
+ module Registry
6
+ class << self
7
+ def fetch(name)
8
+ registry.fetch name
9
+ rescue KeyError
10
+ raise SourceCacheNotFound, "source cache not found: #{name}"
11
+ end
12
+ alias [] fetch
13
+
14
+ def key?(name)
15
+ registry.key? name
16
+ end
17
+
18
+ def register(by_const_name, by_location)
19
+ by_const_name.each do |const_name, location|
20
+ registry[const_name] ||= location
21
+ end
22
+
23
+ by_location.each do |location, const_name|
24
+ registry[location] ||= const_name
25
+ end
26
+ end
27
+
28
+ def with_cache(key)
29
+ restore_cache! key
30
+ yield
31
+ persist_cache! key
32
+ end
33
+
34
+ def persist_cache!(key)
35
+ cache_file = DepsGrapher.cache_file key
36
+ cache_file.write @registry
37
+ end
38
+
39
+ def restore_cache!(key)
40
+ cache_file = DepsGrapher.cache_file key
41
+ loaded = cache_file.read
42
+
43
+ unless loaded
44
+ @restored_cache = false
45
+ return
46
+ end
47
+
48
+ @registry = loaded
49
+ @restored_cache = true
50
+ end
51
+
52
+ def restored_cache?
53
+ @restored_cache
54
+ end
55
+
56
+ private
57
+
58
+ def registry
59
+ @registry ||= {}
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ class SourceCache
5
+ include Logging
6
+
7
+ class << self
8
+ def register!(name, source)
9
+ new(name, source).register!
10
+ end
11
+ end
12
+
13
+ attr_reader :root
14
+
15
+ def initialize(name, source)
16
+ @name = name
17
+ @source = source
18
+ @root = @source.root
19
+ @cache_by_const_name = {}
20
+ @cache_by_location = {}
21
+ end
22
+
23
+ def register!
24
+ return if Registry.restored_cache?
25
+
26
+ class_name_extractor = ClassNameExtractor.new do |class_name, location|
27
+ @cache_by_const_name[class_name] = location
28
+ @cache_by_location[location] ||= class_name
29
+ end
30
+
31
+ verbose { "Collecting `#{@name}` layer by #{@source}" }
32
+
33
+ start = Time.now
34
+
35
+ @source.files.each do |file|
36
+ class_name_extractor.extract! file
37
+ end
38
+
39
+ info do
40
+ "Found #{@cache_by_const_name.size} modules/classes, #{@cache_by_location.size} locations in `#{@name}` layer (#{Time.now - start} sec)"
41
+ end
42
+ verbose { "" }
43
+
44
+ Registry.register @cache_by_const_name, @cache_by_location
45
+
46
+ ClassNameExtractor.cache.clear
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DepsGrapher
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../vis"
4
+
5
+ module DepsGrapher
6
+ class Vis
7
+ class Box < self
8
+ command_option "vis:box"
9
+
10
+ private
11
+
12
+ def font_color(_)
13
+ "#fff"
14
+ end
15
+
16
+ def shape
17
+ :box
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../vis"
4
+
5
+ module DepsGrapher
6
+ class Vis
7
+ class Dot < self
8
+ command_option "vis:dot"
9
+
10
+ private
11
+
12
+ def shape
13
+ :dot
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Network vis <%= shape %></title>
5
+ <meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no″>
6
+ <meta charset="utf-8">
7
+ <style>
8
+ #network {
9
+ width: 100vw;
10
+ height: 100vh;
11
+ }
12
+ </style>
13
+ <% required_js.each do |js| -%>
14
+ <script src="<%= File.basename js %>"></script>
15
+ <% end -%>
16
+ </head>
17
+ <body>
18
+ <div id="network"></div>
19
+ <script>
20
+ const json = <%= data.to_json %>;
21
+ const nodes = new vis.DataSet(json['nodes']);
22
+ const edges = new vis.DataSet(json['edges']);
23
+ const container = document.getElementById('network');
24
+ const data = {
25
+ nodes: nodes,
26
+ edges: edges
27
+ };
28
+ const options = {
29
+ layout: {
30
+ improvedLayout: false
31
+ },
32
+ };
33
+ const network = new vis.Network(container, data, options);
34
+ </script>
35
+ </body>
36
+ </html>
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "visualizer/base"
4
+
5
+ module DepsGrapher
6
+ class Vis < Visualizer::Base
7
+ private
8
+
9
+ def required_js
10
+ ["https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"]
11
+ end
12
+
13
+ def template_path
14
+ File.expand_path File.join("vis", "template.erb"), __dir__
15
+ end
16
+
17
+ def shape
18
+ raise NotImplementedError
19
+ end
20
+
21
+ def data
22
+ nodes = @nodes.each_with_object([]) do |node, array|
23
+ next if skip_node?(node)
24
+
25
+ array << convert_node(node)
26
+ end
27
+
28
+ edges = @edges.each_with_object([]) do |edge, array|
29
+ next if skip_edge?(edge)
30
+
31
+ array << convert_edge(edge)
32
+ end
33
+
34
+ { nodes: nodes, edges: edges }
35
+ end
36
+
37
+ def convert_node(node)
38
+ root_node = node.parent.nil?
39
+
40
+ {
41
+ id: node.id,
42
+ label: node.label,
43
+ shape: shape,
44
+ size: (root_node ? 10 : 5) + [1.5 * node.deps_count, 20].min,
45
+ font: {
46
+ size: (root_node ? 8 : 5) + [1.2 * node.deps_count, 5].min,
47
+ color: font_color(node.layer)
48
+ },
49
+ color: color_settings(node.layer).except(:font)
50
+ }
51
+ end
52
+
53
+ def convert_edge(edge)
54
+ {
55
+ from: edge.from.id,
56
+ to: edge.to.id,
57
+ arrows: :to
58
+ }
59
+ end
60
+
61
+ def skip_node?(node)
62
+ options[:layers].present? && !options[:layers].include?(node.layer)
63
+ end
64
+
65
+ def skip_edge?(edge)
66
+ skip_node?(edge.to)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "command_option"
5
+ require_relative "color"
6
+
7
+ module DepsGrapher
8
+ module Visualizer
9
+ class Base
10
+ class << self
11
+ def command_option(name, default: false)
12
+ Registry.register self, CommandOption.new(name, default)
13
+ end
14
+ end
15
+
16
+ attr_reader :options
17
+
18
+ def initialize(downloader, options)
19
+ @downloader = downloader
20
+ @options = options
21
+ @nodes = []
22
+ @edges = []
23
+ end
24
+
25
+ def accept!(nodes, edges)
26
+ @nodes = nodes
27
+ @edges = edges
28
+ self
29
+ end
30
+
31
+ def render
32
+ required_js.each do |url|
33
+ @downloader.download url
34
+ end
35
+
36
+ ERB.new(File.read(template_path), trim_mode: "-").result(binding)
37
+ end
38
+
39
+ private
40
+
41
+ def required_js
42
+ []
43
+ end
44
+
45
+ def template_path
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def color(layer_name)
50
+ Color[layer_name]
51
+ end
52
+
53
+ def color_map(type)
54
+ Color.generate_map(type)
55
+ end
56
+
57
+ def arrow_color(layer_name)
58
+ color(layer_name).arrow || background_color(layer_name)
59
+ end
60
+
61
+ def background_color(layer_name)
62
+ color(layer_name).background
63
+ end
64
+
65
+ def font_color(layer_name)
66
+ color(layer_name).font
67
+ end
68
+
69
+ def color_settings(layer_name)
70
+ color(layer_name).settings
71
+ end
72
+ end
73
+ end
74
+ end