diver_down 0.0.1.alpha1

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