diver_down 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +132 -0
  5. data/exe/diver_down_web +55 -0
  6. data/lib/diver_down/definition/dependency.rb +107 -0
  7. data/lib/diver_down/definition/method_id.rb +83 -0
  8. data/lib/diver_down/definition/source.rb +90 -0
  9. data/lib/diver_down/definition.rb +112 -0
  10. data/lib/diver_down/helper.rb +81 -0
  11. data/lib/diver_down/trace/call_stack.rb +45 -0
  12. data/lib/diver_down/trace/ignored_method_ids.rb +136 -0
  13. data/lib/diver_down/trace/module_set/array_module_set.rb +31 -0
  14. data/lib/diver_down/trace/module_set/const_source_location_module_set.rb +28 -0
  15. data/lib/diver_down/trace/module_set.rb +78 -0
  16. data/lib/diver_down/trace/redefine_ruby_methods.rb +64 -0
  17. data/lib/diver_down/trace/tracer/session.rb +121 -0
  18. data/lib/diver_down/trace/tracer.rb +96 -0
  19. data/lib/diver_down/trace.rb +27 -0
  20. data/lib/diver_down/version.rb +5 -0
  21. data/lib/diver_down/web/action.rb +344 -0
  22. data/lib/diver_down/web/bit_id.rb +41 -0
  23. data/lib/diver_down/web/definition_enumerator.rb +54 -0
  24. data/lib/diver_down/web/definition_loader.rb +37 -0
  25. data/lib/diver_down/web/definition_store.rb +89 -0
  26. data/lib/diver_down/web/definition_to_dot.rb +399 -0
  27. data/lib/diver_down/web/dev_server_middleware.rb +72 -0
  28. data/lib/diver_down/web/indented_string_io.rb +59 -0
  29. data/lib/diver_down/web/module_store.rb +59 -0
  30. data/lib/diver_down/web.rb +101 -0
  31. data/lib/diver_down-trace.rb +4 -0
  32. data/lib/diver_down-web.rb +4 -0
  33. data/lib/diver_down.rb +14 -0
  34. data/web/assets/CjLq7LhZ.css +1 -0
  35. data/web/assets/bundle.js +978 -0
  36. data/web/index.html +13 -0
  37. metadata +122 -0
@@ -0,0 +1,344 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module DiverDown
6
+ class Web
7
+ class Action
8
+ Pagination = Data.define(
9
+ :current_page,
10
+ :total_pages,
11
+ :total_count,
12
+ :per
13
+ )
14
+
15
+ # @param store [DiverDown::Definition::Store]
16
+ # @param module_store [DiverDown::Web::ModuleStore]
17
+ # @param request [Rack::Request]
18
+ def initialize(store:, module_store:, request:)
19
+ @store = store
20
+ @module_store = module_store
21
+ @request = request
22
+ end
23
+
24
+ # GET /api/sources.json
25
+ def sources
26
+ source_names = Set.new
27
+
28
+ # rubocop:disable Style/HashEachMethods
29
+ @store.each do |_, definition|
30
+ definition.sources.each do |source|
31
+ source_names.add(source.source_name)
32
+ end
33
+ end
34
+ # rubocop:enable Style/HashEachMethods
35
+
36
+ json(
37
+ sources: source_names.sort.map do |source_name|
38
+ {
39
+ source_name:,
40
+ }
41
+ end
42
+ )
43
+ end
44
+
45
+ # GET /api/modules.json
46
+ def modules
47
+ # Hash{ DiverDown::Definition::Modulee => Set<Integer> }
48
+ module_set = Set.new
49
+
50
+ # rubocop:disable Style/HashEachMethods
51
+ @store.each do |_, definition|
52
+ definition.sources.each do |source|
53
+ modules = @module_store.get(source.source_name)
54
+ module_set.add(modules) unless modules.empty?
55
+ end
56
+ end
57
+ # rubocop:enable Style/HashEachMethods
58
+
59
+ json(
60
+ modules: module_set.sort.map do
61
+ _1.map do |module_name|
62
+ {
63
+ module_name:,
64
+ }
65
+ end
66
+ end
67
+ )
68
+ end
69
+
70
+ # GET /api/modules/:module_name.json
71
+ # @param module_names [Array<String>]
72
+ def module(module_names)
73
+ # Hash{ DiverDown::Definition::Modulee => Set<Integer> }
74
+ related_definition_store_ids = Set.new
75
+ source_names = Set.new
76
+
77
+ # rubocop:disable Style/HashEachMethods
78
+ @store.each do |_, definition|
79
+ definition.sources.each do |source|
80
+ source_module_names = @module_store.get(source.source_name)
81
+
82
+ next unless source_module_names[0..module_names.size - 1] == module_names
83
+
84
+ source_names.add(source.source_name)
85
+ related_definition_store_ids.add(definition.store_id)
86
+ end
87
+ end
88
+ # rubocop:enable Style/HashEachMethods
89
+
90
+ if related_definition_store_ids.empty?
91
+ return not_found
92
+ end
93
+
94
+ related_definitions = related_definition_store_ids.map { @store.get(_1) }
95
+
96
+ json(
97
+ modules: module_names.map do
98
+ {
99
+ module_name: _1,
100
+ }
101
+ end,
102
+ sources: source_names.sort.map do |source_name|
103
+ {
104
+ source_name:,
105
+ }
106
+ end,
107
+ related_definitions: related_definitions.map do |definition|
108
+ {
109
+ id: definition.store_id,
110
+ title: definition.title,
111
+ }
112
+ end
113
+ )
114
+ end
115
+
116
+ # GET /api/definitions.json
117
+ #
118
+ # @param page [Integer]
119
+ # @param per [Integer]
120
+ # @param title [String]
121
+ # @param source [String]
122
+ def definitions(page:, per:, title:, source:)
123
+ definition_enumerator = DiverDown::Web::DefinitionEnumerator.new(@store, title:, source:)
124
+ definitions, pagination = paginate(definition_enumerator, page, per)
125
+
126
+ json(
127
+ definitions: definitions.map do |definition|
128
+ {
129
+ id: definition.store_id,
130
+ definition_group: definition.definition_group,
131
+ title: definition.title,
132
+ }
133
+ end,
134
+ pagination: pagination.to_h
135
+ )
136
+ end
137
+
138
+ # GET /api/initialization_status.json
139
+ def initialization_status(total)
140
+ json(
141
+ total:,
142
+ loaded: @store.size
143
+ )
144
+ end
145
+
146
+ # GET /api/pid.json
147
+ def pid
148
+ json(
149
+ pid: Process.pid
150
+ )
151
+ end
152
+
153
+ # GET /api/definitions/:bit_id.json
154
+ #
155
+ # @param bit_id [Integer]
156
+ # @param compound [Boolean]
157
+ # @param concentrate [Boolean]
158
+ # @param only_module [Boolean]
159
+ def combine_definitions(bit_id, compound, concentrate, only_module)
160
+ ids = DiverDown::Web::BitId.bit_id_to_ids(bit_id)
161
+
162
+ valid_ids = ids.select do
163
+ @store.key?(_1)
164
+ end
165
+
166
+ definition, titles = case valid_ids.length
167
+ when 0
168
+ return not_found
169
+ when 1
170
+ definition = @store.get(ids[0])
171
+ [definition, [definition.title]]
172
+ else
173
+ definitions = valid_ids.map { @store.get(_1) }
174
+ definition = DiverDown::Definition.combine(definition_group: nil, title: 'combined', definitions:)
175
+ [definition, definitions.map(&:title)]
176
+ end
177
+
178
+ if definition
179
+ definition_to_dot = DiverDown::Web::DefinitionToDot.new(definition, @module_store, compound:, concentrate:, only_module:)
180
+
181
+ json(
182
+ titles:,
183
+ bit_id: DiverDown::Web::BitId.ids_to_bit_id(valid_ids).to_s,
184
+ dot: definition_to_dot.to_s,
185
+ dot_metadata: definition_to_dot.metadata,
186
+ sources: definition.sources.map do
187
+ {
188
+ source_name: _1.source_name,
189
+ modules: @module_store.get(_1.source_name).map do |module_name|
190
+ { module_name: }
191
+ end,
192
+ }
193
+ end
194
+ )
195
+ else
196
+ not_found
197
+ end
198
+ end
199
+
200
+ # GET /api/sources/:source_name.json
201
+ #
202
+ # @param source_name [String]
203
+ def source(source_name)
204
+ found_sources = []
205
+ related_definitions = []
206
+ reverse_dependencies = Hash.new { |h, dependency_source_name| h[dependency_source_name] = DiverDown::Definition::Dependency.new(source_name: dependency_source_name) }
207
+
208
+ @store.each do |id, definition|
209
+ found_source = nil
210
+
211
+ definition.sources.each do |definition_source|
212
+ found_source = definition_source if definition_source.source_name == source_name
213
+
214
+ found_reverse_dependencies = definition_source.dependencies.select do |dependency|
215
+ dependency.source_name == source_name
216
+ end
217
+
218
+ found_reverse_dependencies.each do |dependency|
219
+ reverse_dependency = reverse_dependencies[dependency.source_name]
220
+
221
+ dependency.method_ids.each do |method_id|
222
+ reverse_method_id = reverse_dependency.find_or_build_method_id(name: method_id.name, context: method_id.context)
223
+ reverse_method_id.paths.merge(method_id.paths)
224
+ end
225
+ end
226
+ end
227
+
228
+ if found_source
229
+ found_sources << found_source
230
+ related_definitions << [id, definition]
231
+ end
232
+ end
233
+
234
+ return not_found if related_definitions.empty?
235
+
236
+ module_names = if found_sources.empty?
237
+ []
238
+ else
239
+ source = DiverDown::Definition::Source.combine(*found_sources)
240
+ @module_store.get(source.source_name)
241
+ end
242
+
243
+ json(
244
+ source_name:,
245
+ modules: module_names.map do
246
+ { module_name: _1 }
247
+ end,
248
+ related_definitions: related_definitions.map do |id, definition|
249
+ {
250
+ id:,
251
+ title: definition.title,
252
+ }
253
+ end,
254
+ reverse_dependencies: reverse_dependencies.values.map { |reverse_dependency|
255
+ {
256
+ source_name: reverse_dependency.source_name,
257
+ method_ids: reverse_dependency.method_ids.sort.map do |method_id|
258
+ {
259
+ context: method_id.context,
260
+ name: method_id.name,
261
+ paths: method_id.paths.sort,
262
+ }
263
+ end,
264
+ }
265
+ }
266
+ )
267
+ end
268
+
269
+ # POST /api/sources/:source_name/modules.json
270
+ #
271
+ # @param source_name [String]
272
+ # @param modules [Array<String>]
273
+ def set_modules(source_name, modules)
274
+ found_source = @store.any? do |_, definition|
275
+ definition.sources.any? do |source|
276
+ source.source_name == source_name
277
+ end
278
+ end
279
+
280
+ if found_source
281
+ @module_store.set(source_name, modules)
282
+ @module_store.flush
283
+
284
+ json({})
285
+ else
286
+ not_found
287
+ end
288
+ end
289
+
290
+ # @return [Array[Integer, Hash, Array]]
291
+ def not_found
292
+ [404, { 'content-type' => 'text/plain' }, ['not found']]
293
+ end
294
+
295
+ private
296
+
297
+ attr_reader :request, :store
298
+
299
+ def build_method_id_key(dependency, method_id)
300
+ "#{dependency.source_name}-#{method_id.context}-#{method_id.name}"
301
+ end
302
+
303
+ def paginate(enumerator, page, per)
304
+ start_index = (page - 1) * per
305
+ end_index = start_index + per - 1
306
+
307
+ items = []
308
+ enumerator.each_with_index do |item, index|
309
+ next if index < start_index
310
+ break if index > end_index
311
+
312
+ items.push(item)
313
+ end
314
+
315
+ total_count = enumerator.size
316
+
317
+ [
318
+ items,
319
+ Pagination.new(
320
+ current_page: page,
321
+ total_pages: [(total_count.to_f / per).ceil, 1].max,
322
+ total_count:,
323
+ per:
324
+ ),
325
+ ]
326
+ end
327
+
328
+ def fetch_definition(ids)
329
+ case ids.length
330
+ when 0
331
+ nil
332
+ when 1
333
+ @store.get(ids[0])
334
+ else
335
+ combine_ids_definitions(ids)
336
+ end
337
+ end
338
+
339
+ def json(data)
340
+ [200, { 'content-type' => 'application/json' }, [data.to_json]]
341
+ end
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiverDown
4
+ class Web
5
+ # Use bitflag for a lightweight representation of a list of bound definitions on a URL.
6
+ # Each definition is related with a unique bitflag, and when merging, the bitflag is calculated with `OR` to represent the merged definitions in a lightweight and fast.
7
+ module BitId
8
+ class << self
9
+ # @param ids [Array<Integer>]
10
+ # @return [Integer]
11
+ def ids_to_bit_id(ids)
12
+ ids.inject(0) { _1 | id_to_bit_id(_2) }
13
+ end
14
+
15
+ # @param id [Integer]
16
+ # @return [Array<Integer>]
17
+ def bit_id_to_ids(bit_id)
18
+ ids = []
19
+ shift = 0
20
+ while bit_id.positive?
21
+ if (bit_id & 1) == 1
22
+ ids.push(shift + 1)
23
+ end
24
+
25
+ bit_id >>= 1
26
+ shift += 1
27
+ end
28
+ ids
29
+ end
30
+
31
+ private
32
+
33
+ # @param id [Integer]
34
+ # @return [Integer]
35
+ def id_to_bit_id(id)
36
+ 1 << (id - 1)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiverDown
4
+ class Web
5
+ class DefinitionEnumerator
6
+ include Enumerable
7
+
8
+ # @param store [DiverDown::Definition::Store]
9
+ # @param query [String]
10
+ def initialize(store, title: '', source: '')
11
+ @store = store
12
+ @title = title
13
+ @source = source
14
+ end
15
+
16
+ # @yield [parent_bit_id, bit_id, definition]
17
+ def each
18
+ return enum_for(__method__) unless block_given?
19
+
20
+ definition_groups = @store.definition_groups
21
+ definition_groups.each do |definition_group|
22
+ definitions = @store.filter_by_definition_group(definition_group)
23
+ definitions.each do
24
+ next unless match_definition?(_1)
25
+
26
+ yield(_1)
27
+ end
28
+ end
29
+ end
30
+
31
+ # @return [Integer]
32
+ def size
33
+ @store.size
34
+ end
35
+ alias length size
36
+
37
+ private
38
+
39
+ def match_definition?(definition)
40
+ matched = true
41
+
42
+ unless @title.empty?
43
+ matched &&= definition.title.include?(@title)
44
+ end
45
+
46
+ unless @source.empty?
47
+ matched &&= definition.sources.any? { _1.source_name.include?(@source) }
48
+ end
49
+
50
+ matched
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiverDown
4
+ class Web
5
+ class DefinitionLoader
6
+ # @param path [String]
7
+ def load_file(path)
8
+ hash = case File.extname(path)
9
+ when '.yaml', '.yml'
10
+ from_yaml(path)
11
+ when '.msgpack'
12
+ from_msgpack(path)
13
+ when '.json'
14
+ from_json(path)
15
+ else
16
+ raise ArgumentError, "Unsupported file type: #{path}"
17
+ end
18
+
19
+ DiverDown::Definition.from_hash(hash)
20
+ end
21
+
22
+ private
23
+
24
+ def from_json(path)
25
+ JSON.parse(File.read(path))
26
+ end
27
+
28
+ def from_yaml(path)
29
+ YAML.load_file(path)
30
+ end
31
+
32
+ def from_msgpack(path)
33
+ MessagePack.unpack(File.binread(path))
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiverDown
4
+ class Web
5
+ class DefinitionStore
6
+ include Enumerable
7
+
8
+ attr_reader :bit_id
9
+
10
+ def initialize
11
+ # Hash{ Integer(unique bit flag) => DiverDown::Definition }
12
+ @definitions = []
13
+ @definition_group_store = Hash.new { |h, k| h[k] = [] }
14
+ end
15
+
16
+ # @param id [Integer]
17
+ # @raise [KeyError] if the id is not found
18
+ # @return [DiverDown::Definition]
19
+ def get(id)
20
+ index = id - 1
21
+
22
+ raise(KeyError, "id not found: #{id}") if id <= 0 || @definitions.size < id
23
+
24
+ @definitions.fetch(index)
25
+ end
26
+
27
+ # @param definitions [Array<DiverDown::Definition>]
28
+ # @return [Array<Integer>] ids of the definitions
29
+ def set(*definitions)
30
+ definitions.map do
31
+ raise(ArgumentError, 'definition already set') if _1.store_id
32
+
33
+ _1.store_id = @definitions.size + 1
34
+
35
+ @definitions.push(_1)
36
+ @definition_group_store[_1.definition_group] << _1
37
+
38
+ _1.store_id
39
+ end
40
+ end
41
+
42
+ # @return [Array<String, nil>]
43
+ def definition_groups
44
+ keys = @definition_group_store.keys
45
+
46
+ # Sort keys with nil at the end
47
+ with_nil = keys.include?(nil)
48
+ keys.delete(nil) if with_nil
49
+ keys.sort!
50
+ keys.push(nil) if with_nil
51
+
52
+ keys
53
+ end
54
+
55
+ # @param definition_group [String, nil]
56
+ # @return [Array<DiverDown::Definition>]
57
+ def filter_by_definition_group(definition_group)
58
+ @definition_group_store.fetch(definition_group, [])
59
+ end
60
+
61
+ # @param id [Integer]
62
+ # @return [Boolean]
63
+ def key?(id)
64
+ id.positive? && id <= @definitions.size
65
+ end
66
+
67
+ # @return [Integer]
68
+ def length
69
+ @definitions.length
70
+ end
71
+ alias size length
72
+
73
+ # @return [Boolean]
74
+ def empty?
75
+ @definitions.empty?
76
+ end
77
+
78
+ # @yield [DiverDown::Definition]
79
+ def each
80
+ return enum_for(__method__) unless block_given?
81
+
82
+ # NOTE: To allow values to be rewritten during #each, duplicate the value through #to_a.
83
+ @definitions.each.with_index(1) do |definition, id|
84
+ yield(id, definition)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end