deps_grapher 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +327 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/deps_grapher +96 -0
- data/bin/setup +8 -0
- data/deps_grapher.gemspec +34 -0
- data/lefthook.yml +14 -0
- data/lib/deps_grapher/ast_processor.rb +211 -0
- data/lib/deps_grapher/ast_processor_policy.rb +45 -0
- data/lib/deps_grapher/cache_file.rb +48 -0
- data/lib/deps_grapher/cli.rb +29 -0
- data/lib/deps_grapher/command/analyzer.rb +89 -0
- data/lib/deps_grapher/command/init.rb +49 -0
- data/lib/deps_grapher/configuration.rb +99 -0
- data/lib/deps_grapher/context.rb +48 -0
- data/lib/deps_grapher/cytoscape/cose.rb +36 -0
- data/lib/deps_grapher/cytoscape/fcose.rb +36 -0
- data/lib/deps_grapher/cytoscape/klay.rb +27 -0
- data/lib/deps_grapher/cytoscape/template.erb +61 -0
- data/lib/deps_grapher/cytoscape.rb +88 -0
- data/lib/deps_grapher/dsl.rb +50 -0
- data/lib/deps_grapher/edge.rb +58 -0
- data/lib/deps_grapher/errors.rb +8 -0
- data/lib/deps_grapher/event.rb +63 -0
- data/lib/deps_grapher/graph.rb +71 -0
- data/lib/deps_grapher/graphile/generator.rb +42 -0
- data/lib/deps_grapher/graphile/graphile.erb +119 -0
- data/lib/deps_grapher/graphile/graphile.temp.erb +23 -0
- data/lib/deps_grapher/html_writer.rb +16 -0
- data/lib/deps_grapher/input.rb +40 -0
- data/lib/deps_grapher/layer/registry.rb +32 -0
- data/lib/deps_grapher/layer.rb +75 -0
- data/lib/deps_grapher/logging.rb +21 -0
- data/lib/deps_grapher/matcher.rb +28 -0
- data/lib/deps_grapher/node.rb +65 -0
- data/lib/deps_grapher/plugin_dsl.rb +12 -0
- data/lib/deps_grapher/plugin_loader.rb +29 -0
- data/lib/deps_grapher/source.rb +51 -0
- data/lib/deps_grapher/source_cache/class_name_extractor.rb +60 -0
- data/lib/deps_grapher/source_cache/registry.rb +64 -0
- data/lib/deps_grapher/source_cache.rb +49 -0
- data/lib/deps_grapher/version.rb +5 -0
- data/lib/deps_grapher/vis/box.rb +21 -0
- data/lib/deps_grapher/vis/dot.rb +17 -0
- data/lib/deps_grapher/vis/template.erb +36 -0
- data/lib/deps_grapher/vis.rb +69 -0
- data/lib/deps_grapher/visualizer/base.rb +74 -0
- data/lib/deps_grapher/visualizer/color/registry.rb +30 -0
- data/lib/deps_grapher/visualizer/color.rb +62 -0
- data/lib/deps_grapher/visualizer/command_option.rb +22 -0
- data/lib/deps_grapher/visualizer/downloader.rb +42 -0
- data/lib/deps_grapher/visualizer/js_option.rb +39 -0
- data/lib/deps_grapher/visualizer/registry.rb +37 -0
- data/lib/deps_grapher/visualizer.rb +11 -0
- data/lib/deps_grapher.rb +42 -0
- metadata +135 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
require_relative "source_cache"
|
5
|
+
require_relative "node"
|
6
|
+
require_relative "edge"
|
7
|
+
|
8
|
+
module DepsGrapher
|
9
|
+
class AstProcessor < Parser::AST::Processor
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def processed
|
14
|
+
@processed ||= Set.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def event_processed
|
18
|
+
@event_processed ||= Set.new
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_writer :depth
|
22
|
+
|
23
|
+
def depth
|
24
|
+
@depth ||= 0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(file_path, graph, event_processors, advanced_const_resolver, ignore_errors)
|
29
|
+
super()
|
30
|
+
@file_path = file_path
|
31
|
+
@target = File.basename(file_path, ".*").camelize
|
32
|
+
@graph = graph
|
33
|
+
@event_processors = event_processors
|
34
|
+
@advanced_const_resolver = advanced_const_resolver
|
35
|
+
@ignore_errors = ignore_errors
|
36
|
+
end
|
37
|
+
|
38
|
+
def call
|
39
|
+
return if self.class.processed.include?(@file_path)
|
40
|
+
|
41
|
+
log do
|
42
|
+
source_buffer = Parser::Source::Buffer.new(@file_path)
|
43
|
+
parser = Parser::CurrentRuby.new
|
44
|
+
process parser.parse(source_buffer.read)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def on_module(ast_node)
|
49
|
+
const_node = ast_node.children[0]
|
50
|
+
name = extract_const_name const_node
|
51
|
+
|
52
|
+
@namespace_stack ||= []
|
53
|
+
@namespace_stack.push name
|
54
|
+
|
55
|
+
if name == @target
|
56
|
+
fully_qualified_class_name = @namespace_stack.join "::"
|
57
|
+
@current_node = Node.add fully_qualified_class_name, @file_path
|
58
|
+
self.class.processed << @file_path
|
59
|
+
end
|
60
|
+
|
61
|
+
super
|
62
|
+
|
63
|
+
@namespace_stack.pop
|
64
|
+
end
|
65
|
+
alias on_class on_module
|
66
|
+
|
67
|
+
def on_const(ast_node)
|
68
|
+
const_name = extract_const_name ast_node
|
69
|
+
|
70
|
+
process_recursively! const_name, ast_node do
|
71
|
+
Event.add name: :const_found, const_name: _1, location: _2
|
72
|
+
end
|
73
|
+
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_send(ast_node)
|
78
|
+
if @current_node
|
79
|
+
receiver_node, method_name, = *ast_node
|
80
|
+
|
81
|
+
const_name = call_advanced_const_resolver ast_node
|
82
|
+
const_name ||= extract_const_name receiver_node
|
83
|
+
|
84
|
+
process_recursively! const_name, receiver_node do
|
85
|
+
Event.add name: :method_found, const_name: _1, location: _2, method: method_name
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
super
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def call_event_processor(event)
|
95
|
+
return if event.blank? || @event_processors.blank?
|
96
|
+
return if self.class.event_processed.include?(event.key)
|
97
|
+
|
98
|
+
self.class.event_processed << event.key
|
99
|
+
|
100
|
+
@event_processors.each do |matcher, (prop, event_processor)|
|
101
|
+
if matcher.is_a?(Regexp)
|
102
|
+
next unless matcher.match? event.send(prop)
|
103
|
+
else
|
104
|
+
next unless matcher == event.send(prop)
|
105
|
+
end
|
106
|
+
|
107
|
+
event_processor.call event
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def call_advanced_const_resolver(ast_node)
|
112
|
+
return nil unless @advanced_const_resolver.respond_to?(:call)
|
113
|
+
|
114
|
+
@advanced_const_resolver.call ast_node
|
115
|
+
end
|
116
|
+
|
117
|
+
def process_recursively!(const_name, ast_node)
|
118
|
+
return unless @current_node
|
119
|
+
|
120
|
+
begin
|
121
|
+
const_name, location = if ast_node
|
122
|
+
guess_source_location(const_name, ast_node)
|
123
|
+
else
|
124
|
+
find_source_location!(const_name)
|
125
|
+
end
|
126
|
+
rescue SourceLocationNotFound => e
|
127
|
+
raise e unless @ignore_errors
|
128
|
+
end
|
129
|
+
|
130
|
+
return unless const_name && location
|
131
|
+
|
132
|
+
event = yield const_name, location
|
133
|
+
call_event_processor event
|
134
|
+
|
135
|
+
return unless event&.processing?
|
136
|
+
|
137
|
+
node = Node.add const_name, location
|
138
|
+
edge = Edge.add @current_node, node
|
139
|
+
|
140
|
+
return unless edge
|
141
|
+
|
142
|
+
@graph.add_edge edge
|
143
|
+
|
144
|
+
self.class.new(
|
145
|
+
node.location,
|
146
|
+
@graph,
|
147
|
+
@event_processors,
|
148
|
+
@advanced_const_resolver,
|
149
|
+
@ignore_errors
|
150
|
+
).call
|
151
|
+
end
|
152
|
+
|
153
|
+
def extract_const_name(ast_node)
|
154
|
+
return nil if ast_node.nil?
|
155
|
+
|
156
|
+
return nil unless ast_node.type == :const
|
157
|
+
|
158
|
+
base, name = *ast_node
|
159
|
+
if base.nil?
|
160
|
+
name.to_s
|
161
|
+
else
|
162
|
+
[extract_const_name(base), name].compact.join "::"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def guess_namespace(file_path)
|
167
|
+
const_name = SourceCache::Registry.fetch(file_path)
|
168
|
+
namespace = const_name.split("::")
|
169
|
+
parts = namespace.last
|
170
|
+
while parts.present?
|
171
|
+
return if yield(namespace)
|
172
|
+
|
173
|
+
parts = namespace.pop
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def guess_source_location(const_name, ast_node)
|
178
|
+
return if const_name.blank?
|
179
|
+
|
180
|
+
find_source_location! const_name
|
181
|
+
rescue SourceLocationNotFound
|
182
|
+
guess_namespace(ast_node.location.name.source_buffer.name) do |ns|
|
183
|
+
return find_source_location! [*ns, const_name].compact.join("::")
|
184
|
+
rescue SourceLocationNotFound
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def find_source_location!(const_name)
|
190
|
+
return if const_name.blank?
|
191
|
+
|
192
|
+
location = SourceCache::Registry.fetch const_name
|
193
|
+
[const_name, location]
|
194
|
+
rescue SourceCacheNotFound
|
195
|
+
raise SourceLocationNotFound, "source location not found for #{const_name}"
|
196
|
+
end
|
197
|
+
|
198
|
+
def log
|
199
|
+
verbose do
|
200
|
+
indent = "*" * self.class.depth
|
201
|
+
indent << " " if indent.present?
|
202
|
+
"Analyzing #{indent}#{@file_path}"
|
203
|
+
end
|
204
|
+
|
205
|
+
self.class.depth += 1
|
206
|
+
yield
|
207
|
+
ensure
|
208
|
+
self.class.depth -= 1
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DepsGrapher
|
4
|
+
class AstProcessorPolicy
|
5
|
+
def initialize(context, &block)
|
6
|
+
@context = context
|
7
|
+
DSL.new(self).instance_eval(&block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def const(matcher, &block)
|
11
|
+
event_processor matcher, :const_name, &block
|
12
|
+
end
|
13
|
+
|
14
|
+
def include_const(matcher)
|
15
|
+
const matcher, &:processing!
|
16
|
+
end
|
17
|
+
|
18
|
+
def exclude_const(matcher)
|
19
|
+
const matcher, &:skip_processing!
|
20
|
+
end
|
21
|
+
|
22
|
+
def location(matcher, &block)
|
23
|
+
event_processor matcher, :location, &block
|
24
|
+
end
|
25
|
+
|
26
|
+
def include_location(matcher)
|
27
|
+
location matcher, &:processing!
|
28
|
+
end
|
29
|
+
|
30
|
+
def exclude_location(matcher)
|
31
|
+
location matcher, &:skip_processing!
|
32
|
+
end
|
33
|
+
|
34
|
+
def event_processor(matcher, prop, &block)
|
35
|
+
@context.event_processors[matcher] = [prop, block]
|
36
|
+
end
|
37
|
+
|
38
|
+
def advanced_const_resolver(callable = nil, &block)
|
39
|
+
raise ArgumentError, "You must provide a block or callable" unless block || callable
|
40
|
+
raise ArgumentError, "The provided object must respond to #call" if callable && !callable.respond_to?(:call)
|
41
|
+
|
42
|
+
@context.advanced_const_resolver = block || callable
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DepsGrapher
|
4
|
+
class CacheFile
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
def initialize(file:, ttl:)
|
8
|
+
@file = file
|
9
|
+
@ttl = ttl
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(target)
|
13
|
+
return if File.exist?(@file)
|
14
|
+
|
15
|
+
FileUtils.mkdir_p File.dirname(@file)
|
16
|
+
|
17
|
+
info { "Writing cache to #{@file}" }
|
18
|
+
|
19
|
+
File.open(@file, "w") do |f|
|
20
|
+
Marshal.dump target, f
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def read
|
25
|
+
return nil unless File.exist?(@file)
|
26
|
+
|
27
|
+
info { "Reading cache from #{@file} (#{File.size(@file)} bytes)" }
|
28
|
+
|
29
|
+
File.open(@file) do |f|
|
30
|
+
return Marshal.load f
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def stale?
|
35
|
+
return false unless File.exist?(@file)
|
36
|
+
|
37
|
+
File.mtime(@file).to_i < (Time.now.to_i - @ttl)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clean!(force: false)
|
41
|
+
return unless File.exist?(@file)
|
42
|
+
return unless force || stale?
|
43
|
+
|
44
|
+
FileUtils.rm_f @file
|
45
|
+
info { "Removed stale cache file: #{@file}" }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DepsGrapher
|
4
|
+
class Cli
|
5
|
+
include Logging
|
6
|
+
|
7
|
+
STATUS_SUCCESS = 0
|
8
|
+
STATUS_FAILURE = 1
|
9
|
+
private_constant :STATUS_SUCCESS, :STATUS_FAILURE
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def run!(command)
|
13
|
+
new(command).run!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(command)
|
18
|
+
@command = command
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!
|
22
|
+
@command.run!
|
23
|
+
STATUS_SUCCESS
|
24
|
+
rescue StandardError => e
|
25
|
+
error { e.backtrace.unshift(e.message).join("\n") }
|
26
|
+
STATUS_FAILURE
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DepsGrapher
|
4
|
+
module Command
|
5
|
+
class Analyzer
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
def initialize(file_paths, context)
|
9
|
+
@file_paths = file_paths
|
10
|
+
@context = context
|
11
|
+
end
|
12
|
+
|
13
|
+
def run!
|
14
|
+
if file_paths.empty?
|
15
|
+
warn { "No files to analyze" }
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
clean_dir!
|
20
|
+
analyze!
|
21
|
+
visualize!
|
22
|
+
rescue TargetNodeNotFound => e
|
23
|
+
info { e.message }
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :file_paths, :context
|
29
|
+
|
30
|
+
def clean_dir!
|
31
|
+
context.clean_dir!
|
32
|
+
end
|
33
|
+
|
34
|
+
def analyze!
|
35
|
+
file_paths.each do |file_path|
|
36
|
+
file_path = File.expand_path file_path
|
37
|
+
|
38
|
+
unless File.exist?(file_path)
|
39
|
+
warn { "Skipping #{file_path}" }
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
ast_processor = AstProcessor.new(
|
44
|
+
file_path,
|
45
|
+
graph,
|
46
|
+
context.event_processors,
|
47
|
+
context.advanced_const_resolver,
|
48
|
+
context.ignore_errors
|
49
|
+
)
|
50
|
+
|
51
|
+
ast_processor.call
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def visualize!
|
56
|
+
writer = context.create_writer
|
57
|
+
visualizer = context.create_visualizer
|
58
|
+
visualizer.accept!(*extract_graph_elements)
|
59
|
+
bytesize = writer.write visualizer.render
|
60
|
+
|
61
|
+
info { "Writing to #{writer.path} (#{bytesize} bytes)" }
|
62
|
+
info { "Run `open #{writer.path}` to view the graph" }
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_graph_elements
|
66
|
+
if context.target_path
|
67
|
+
source_path_matcher = nil
|
68
|
+
source_path_matcher = Matcher.new(context.source_path) if context.source_path.present?
|
69
|
+
|
70
|
+
target_path_matcher = Matcher.new(context.target_path)
|
71
|
+
|
72
|
+
message = []
|
73
|
+
message << "Searching paths"
|
74
|
+
message << "from `#{source_path_matcher}`" if source_path_matcher
|
75
|
+
message << "to `#{target_path_matcher}`"
|
76
|
+
|
77
|
+
info { message.join(" ") }
|
78
|
+
graph.find_path source_path_matcher, target_path_matcher
|
79
|
+
else
|
80
|
+
[Node.all, Edge.all]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def graph
|
85
|
+
@graph ||= context.create_graph
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "readline"
|
4
|
+
require "fileutils"
|
5
|
+
|
6
|
+
module DepsGrapher
|
7
|
+
module Command
|
8
|
+
class Init
|
9
|
+
include Logging
|
10
|
+
|
11
|
+
def initialize(context)
|
12
|
+
@context = context
|
13
|
+
end
|
14
|
+
|
15
|
+
def run!
|
16
|
+
dest = File.expand_path("graphile.rb")
|
17
|
+
|
18
|
+
return if File.exist?(dest) && !ask_yes_no("Overwrite `#{dest}`?")
|
19
|
+
|
20
|
+
context.generate_graphile dest
|
21
|
+
|
22
|
+
info { "\n`#{dest}` was created." }
|
23
|
+
info { "Please edit the configuration file." }
|
24
|
+
info { "Run `bundle exec deps_grapher -c #{File.basename(dest)}`." }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :context
|
30
|
+
|
31
|
+
def ask_yes_no(question)
|
32
|
+
stty_save = `stty -g`.chomp
|
33
|
+
trap("INT") do
|
34
|
+
system "stty", stty_save
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
|
38
|
+
while (input = Readline.readline("#{question} (y/n): ", true))
|
39
|
+
case input
|
40
|
+
when "y"
|
41
|
+
return true
|
42
|
+
when "n"
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DepsGrapher
|
4
|
+
class Configuration
|
5
|
+
include PluginDSL
|
6
|
+
|
7
|
+
class_attribute :path
|
8
|
+
class_attribute :root_dir, default: File.expand_path(File.join("..", ".."), __dir__)
|
9
|
+
class_attribute :visualizer
|
10
|
+
class_attribute :visualizer_options, default: { layers: [] }
|
11
|
+
class_attribute :source_path # source class name on graph
|
12
|
+
class_attribute :target_path # target class name on graph
|
13
|
+
class_attribute :clean, default: false
|
14
|
+
class_attribute :logger
|
15
|
+
class_attribute :cache_dir, default: File.expand_path(File.join("..", "..", "tmp", "deps_grapher", "cache"), __dir__)
|
16
|
+
class_attribute :cache_ttl, default: 60 * 5 # 5 minutes
|
17
|
+
class_attribute :output_dir, default: File.expand_path(File.join("..", "..", "tmp", "deps_grapher", "graph"), __dir__)
|
18
|
+
class_attribute :ignore_errors, default: false
|
19
|
+
class_attribute :verbose, default: false
|
20
|
+
class_attribute :dump, default: false
|
21
|
+
|
22
|
+
attr_accessor :layers
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
self.logger = Logger.new($stderr).tap do
|
26
|
+
_1.formatter = ->(_, _, _, msg) { "#{msg}\n" }
|
27
|
+
end
|
28
|
+
|
29
|
+
self.visualizer = Visualizer::Registry.default_visualizer
|
30
|
+
|
31
|
+
@layers = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def available_visualizers
|
35
|
+
Visualizer::Registry.available_visualizers
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_plugin!
|
39
|
+
PluginLoader.load! plugin_dir
|
40
|
+
end
|
41
|
+
|
42
|
+
def merge!(options)
|
43
|
+
options.each do |key, value|
|
44
|
+
send "#{key}=", value if respond_to?("#{key}=") && !value.nil?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def input
|
49
|
+
@input ||= Input.new(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def layer(&block)
|
53
|
+
Layer.new(&block).tap do |layer|
|
54
|
+
@layers[layer.name] = layer
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def ast_processor_policy(&block)
|
59
|
+
AstProcessorPolicy.new context, &block
|
60
|
+
end
|
61
|
+
|
62
|
+
def plugin_dir(dir = nil)
|
63
|
+
return @plugin_dir unless dir
|
64
|
+
|
65
|
+
@plugin_dir = dir
|
66
|
+
end
|
67
|
+
|
68
|
+
def load!(file)
|
69
|
+
return unless file
|
70
|
+
|
71
|
+
file = File.expand_path file
|
72
|
+
raise ArgumentError, "no such file: #{file}" unless File.exist?(file)
|
73
|
+
|
74
|
+
self.path = file
|
75
|
+
|
76
|
+
content = File.read file
|
77
|
+
|
78
|
+
cache_key = Digest::MD5.hexdigest(content)
|
79
|
+
|
80
|
+
SourceCache::Registry.with_cache cache_key do
|
81
|
+
DSL.new(self).instance_eval content
|
82
|
+
end
|
83
|
+
|
84
|
+
return unless dump
|
85
|
+
|
86
|
+
at_exit do
|
87
|
+
warn ""
|
88
|
+
warn "=============================="
|
89
|
+
warn " Configuration"
|
90
|
+
warn "=============================="
|
91
|
+
puts content
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def context
|
96
|
+
@context ||= Context.new self
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module DepsGrapher
|
6
|
+
class Context
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@config,
|
10
|
+
:ignore_errors,
|
11
|
+
:source_path,
|
12
|
+
:target_path
|
13
|
+
|
14
|
+
attr_accessor :event_processors, :advanced_const_resolver
|
15
|
+
|
16
|
+
def initialize(config)
|
17
|
+
@config = config
|
18
|
+
@event_processors = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def clean_dir!
|
22
|
+
return unless @config.clean
|
23
|
+
|
24
|
+
FileUtils.rm_rf @config.output_dir
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_graphile(dest)
|
28
|
+
Graphile::Generator.new(@config).call dest
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_temp_graphile
|
32
|
+
generate_graphile nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_writer
|
36
|
+
HtmlWriter.new @config.output_dir
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_visualizer
|
40
|
+
downloader = Visualizer::Downloader.new @config.output_dir
|
41
|
+
Visualizer.fetch(@config.visualizer).new downloader, @config.visualizer_options
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_graph
|
45
|
+
target_path ? Graph.new : NullGraph.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../cytoscape"
|
4
|
+
|
5
|
+
module DepsGrapher
|
6
|
+
class Cytoscape
|
7
|
+
class Cose < self
|
8
|
+
command_option "cy:cose"
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def required_js
|
13
|
+
super.tap do |js|
|
14
|
+
js << "https://unpkg.com/cose-base/cose-base.js"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def layout_options
|
19
|
+
option = Visualizer::JsOption.new(
|
20
|
+
name: :cose,
|
21
|
+
directed: true,
|
22
|
+
padding: 10,
|
23
|
+
nodeOverlap: 20,
|
24
|
+
refresh: 20,
|
25
|
+
numIter: 1000,
|
26
|
+
initialTemp: 200,
|
27
|
+
coolingFactor: 0.95,
|
28
|
+
minTemp: 1.0
|
29
|
+
)
|
30
|
+
option.add_function(name: :nodeRepulsion, args: :edge, body: "return 100000")
|
31
|
+
option.add_function(name: :idealEdgeLength, args: :edge, body: "return 100")
|
32
|
+
option.add_function(name: :edgeElasticity, args: :edge, body: "return 32")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "cose"
|
4
|
+
|
5
|
+
module DepsGrapher
|
6
|
+
class Cytoscape
|
7
|
+
class Fcose < Cose
|
8
|
+
command_option "cy:fcose"
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def required_js
|
13
|
+
super.tap do |js|
|
14
|
+
js << "https://unpkg.com/cytoscape-fcose/cytoscape-fcose.js"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def layout_options
|
19
|
+
option = Visualizer::JsOption.new(
|
20
|
+
name: :fcose,
|
21
|
+
directed: true,
|
22
|
+
padding: 10,
|
23
|
+
nodeOverlap: 20,
|
24
|
+
refresh: 20,
|
25
|
+
numIter: 1000,
|
26
|
+
initialTemp: 200,
|
27
|
+
coolingFactor: 0.95,
|
28
|
+
minTemp: 1.0
|
29
|
+
)
|
30
|
+
option.add_function(name: :nodeRepulsion, args: :edge, body: "return 100000")
|
31
|
+
option.add_function(name: :idealEdgeLength, args: :edge, body: "return 100")
|
32
|
+
option.add_function(name: :edgeElasticity, args: :edge, body: "return 32")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|