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,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