diver_down 0.0.1.alpha2 → 0.0.1.alpha3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcbd27bf351ca574fcdcd4c01766efed1355a466244da2e7edd44afaf01a01f8
4
- data.tar.gz: 436603ba87645fa2cdccb025d3d1eafba4a17b6287f59abac416ed2d555a85a7
3
+ metadata.gz: 4a41c67665f8ed7f67dc439661e5ec64ef575663daefad90bfdedaddc62a0f5f
4
+ data.tar.gz: 0c4e4bbc20cc18aa0b2ce247eeede23a08e20ca0ff5105815cc1e45148e59116
5
5
  SHA512:
6
- metadata.gz: a97b9d87ce9b77a67f9638de411d362adcd697857ae66ca10926eb719d9daa086e89db4e00738fe8c7fc578784f9cc9a450ae82a1d6304dfe19f3dbe077f295f
7
- data.tar.gz: d1675fc5858fffd12fcc21f8b7264b0b1199e099402a8c4f9d55af15c10f5b59b2570b1b206318283a6caeddf7fed1109dc3d7af89f61b50371b313343f47210
6
+ metadata.gz: 82c2e82cd6a89e009630a43052e12df11e77dcdb01adeedccf06f48a7954cf7f29f7543a0c99c79c65b51b8a82933e8d213a70a14394d4b5015f3778b1a34b80
7
+ data.tar.gz: 25e11165390606b135111e238027294472ac3773d7f82e4495c0a100092fd28f700c25b0be10fc49e43f68f8e818bb4a15606f1f7bad7a87ecb2f3f340caf08a
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # DiverDown
2
2
 
3
- `divertdown` is a tool that dynamically analyzes application dependencies and creates a dependency map.
4
- This tool was created to analyze Ruby applications for use in large-scale refactoring such as moduler monolith.
3
+ `DiverDown` is a tool designed to dynamically analyze application dependencies and generate a comprehensive dependency map. It is particularly useful for analyzing Ruby applications, aiding significantly in large-scale refactoring projects or transitions towards a modular monolith architecture.
5
4
 
6
- The results of the analysis can be viewed the dependency map graph, and and since the file-by-file association can be categorized into specific groups, you can deepen your architectural consideration.
5
+ ## Features
7
6
 
8
- ## Installation
7
+ - **Dependency Mapping**: Analyze and generate an application dependencies.
8
+ - **Module Categorization**: Organizes file-by-file associations into specific groups, facilitating deeper modular monolith architectural analysis and understanding.
9
+
10
+ ## Getting Started
9
11
 
10
12
  Add this line to your application's Gemfile:
11
13
 
@@ -27,7 +29,7 @@ Or install it yourself as:
27
29
 
28
30
  ### `DiverDown::Trace`
29
31
 
30
- Analyzes the processing of ruby code and outputs the analysis results as `DiverDown::Definition`.
32
+ The `DiverDown::Trace` module analyzes the execution of Ruby code and outputs the results as `DiverDown::Definition` objects.
31
33
 
32
34
  ```ruby
33
35
  tracer = DiverDown::Trace::Tracer.new
@@ -57,8 +59,49 @@ tracer.trace(title: 'title', definition_group: 'group name') do
57
59
  end
58
60
  ```
59
61
 
60
- The analysis results should be output to a specific directory.
61
- Files saved in `.json` or `.yaml` can be read by `DiverDown::Web`.
62
+ **Options**
63
+
64
+ When analyzing user applications, it is recommended to specify the option.
65
+
66
+ |name|type|description|example|default|
67
+ | --- | --- | --- | --- | --- |
68
+ | `module_set` | Hash{ <br> modules: Array<Module, String> \| Set<Module, String> \| nil,<br> paths: Array\<String> \| Set\<String> \| nil <br>}<br>\| DiverDown::Trace::ModuleSet | Specify the class/module to be included in the analysis results.<br><br>If you know the module name:<br>`{ modules: [ModA, ModB] }`<br><br>If you know module path:<br>`{ paths: ['/path/to/rails/app/models/mod_a.rb'] }` | `{ paths: Dir["app/**/*.rb"] }` | `nil`. All class/modul are target. |
69
+ | `caller_paths` | `Array<String> \| nil` | Specifies a list of allowed paths as caller paths. By specifying the user application path in this list of paths and excluding paths under the gem, the caller path is identified back to the user application path. | `Dir["app/**/*.rb"]` | `nil`. All paths are target. |
70
+ | `filter_method_id_path` | `#call \| nil` | lambda to convert the caller path. | `->(path) { path.remove(Rails.root) }` | `nil`. No conversion. |
71
+
72
+ **Example**
73
+
74
+ ```ruby
75
+ # Your rails application paths
76
+ application_paths = [
77
+ *Dir['app/**/*.rb'],
78
+ *Dir['lib/**/*.rb'],
79
+ ].map { File.expand_path(_1) }
80
+
81
+ ignored_application_paths = [
82
+ 'app/models/application_record.rb',
83
+ ].map { File.expand_path(_1) }
84
+
85
+ module_set = DiverDown::Trace::ModuleSet.new(modules: modules - ignored_modules)
86
+
87
+ filter_method_id_path = ->(path) { path.remove("#{Rails.root}/") }
88
+
89
+ tracer = DiverDown::Trace::Tracer.new(
90
+ caller_paths: application_paths,
91
+ module_set: {
92
+ paths: (application_paths - ignored_application_paths)
93
+ },
94
+ filter_method_id_path:
95
+ )
96
+
97
+ definition = tracer.trace do
98
+ # do something
99
+ end
100
+ ```
101
+
102
+ #### Output Results
103
+
104
+ The analysis results are intended to be saved to a specific directory in either `.json` or `.yaml` format. These files are compatible with `DiverDown::Web`, which can read and display the results.
62
105
 
63
106
  ```ruby
64
107
  dir = 'tmp/diver_down'
@@ -71,19 +114,14 @@ File.write(File.join(dir, "#{definition.title}.json"), definition.to_h.to_json)
71
114
  File.write(File.join(dir, "#{definition.title}.yaml"), definition.to_h.to_yaml)
72
115
  ```
73
116
 
74
- **Options**
75
-
76
- TODO
77
-
78
117
  ### `DiverDown::Web`
79
118
 
80
119
  View the analysis results in a browser.
81
120
 
82
- This gem is designed to consider large application with a modular monolithic architecture.
83
- Each file in the analysis can be specified to belong to a module you specify on the browser.
121
+ This gem is specifically designed to analyze large applications with a modular monolithic architecture. It allows users to categorize each analyzed file into specified modules directly through the web interface.
84
122
 
85
- - `--definition-dir` specifies the directory where the analysis results are stored.
86
- - `--module-store-path` will store the results specifying which module each file belongs to. If not specified, the specified results are stored in tempfile.
123
+ - `--definition-dir` Specifies the directory where the analysis results are stored.
124
+ - `--module-store-path` Designates a path to save the results that include details on which module each file belongs to. If this option is not specified, the results will be temporarily stored in a default temporary file.
87
125
 
88
126
  ```sh
89
127
  bundle exec diver_down_web --definition-dir tmp/diver_down --module-store-path tmp/module_store.yml
@@ -102,7 +140,7 @@ open http://localhost:8080
102
140
  - Ruby: `bundle exec rspec`, `bundle exec rubocop`
103
141
  - TypeScript: `pnpm run test`, `pnpm run lint`
104
142
 
105
- ### Development DiverDown::Web
143
+ ### Development `DiverDown::Web`
106
144
 
107
145
  If you want to develop `DiverDown::Web` locally, set up a server for development.
108
146
 
@@ -113,8 +151,6 @@ $ pnpm run dev
113
151
 
114
152
  # Start server for backend
115
153
  $ bundle install
116
- # DIVER_DOWN_DIR specifies the directory where the analysis results are stored.
117
- # DIVER_DOWN_MODULE_STORE specifies a yaml file that defines which module the file belongs to, but this file is newly created, so it works even if the file does not exist.
118
154
  $ DIVER_DOWN_DIR=/path/to/definitions_dir DIVER_DOWN_MODULE_STORE=/path/to/module_store.yml bundle exec puma
119
155
  ```
120
156
 
@@ -62,20 +62,5 @@ module DiverDown
62
62
  def self.constantize(str)
63
63
  ::ActiveSupport::Inflector.constantize(str)
64
64
  end
65
-
66
- # @param hash [Hash]
67
- # @return [Hash]
68
- def self.deep_symbolize_keys(object)
69
- case object
70
- when Hash
71
- object.each_with_object({}) do |(key, value), memo|
72
- memo[key.to_sym] = deep_symbolize_keys(value)
73
- end
74
- when Array
75
- object.map { deep_symbolize_keys(_1) }
76
- else
77
- object
78
- end
79
- end
80
65
  end
81
66
  end
@@ -12,32 +12,47 @@ module DiverDown
12
12
  attr_reader :stack_size
13
13
 
14
14
  def initialize
15
+ @ignored_stack_size = nil
15
16
  @stack_size = 0
16
- @stack = {}
17
+ @context_stack = {}
17
18
  end
18
19
 
19
20
  # @return [Boolean]
20
- def empty?
21
- @stack.empty?
21
+ def empty_context_stack?
22
+ @context_stack.empty?
23
+ end
24
+
25
+ # @return [Array<Integer>]
26
+ def context_stack_size
27
+ @context_stack.keys
22
28
  end
23
29
 
24
30
  # @return [Array<Object>]
25
- def stack
26
- @stack.values
31
+ def context_stack
32
+ @context_stack.values
27
33
  end
28
34
 
29
35
  # @param context [Object, nil] User defined stack context.
30
36
  # @return [void]
31
- def push(context = nil)
37
+ def push(context = nil, ignored: false)
32
38
  @stack_size += 1
33
- @stack[@stack_size] = context unless context.nil?
39
+ @context_stack[@stack_size] = context unless context.nil?
40
+ @ignored_stack_size ||= @stack_size if ignored
41
+ end
42
+
43
+ # If a stack is not already a tracing target, some conditions are unnecessary so that it can be easily ignored.
44
+ #
45
+ # @return [Boolean]
46
+ def ignored?
47
+ !@ignored_stack_size.nil?
34
48
  end
35
49
 
36
50
  # @return [void]
37
51
  def pop
38
52
  raise StackEmptyError if @stack_size.zero?
39
53
 
40
- @stack.delete(@stack_size) if @stack.key?(@stack_size)
54
+ @context_stack.delete(@stack_size) if @context_stack.key?(@stack_size)
55
+ @ignored_stack_size = nil if @ignored_stack_size && @ignored_stack_size == @stack_size
41
56
  @stack_size -= 1
42
57
  end
43
58
  end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiverDown
4
+ module Trace
5
+ class Session
6
+ StackContext = Data.define(
7
+ :source,
8
+ :method_id,
9
+ :path,
10
+ :lineno
11
+ )
12
+
13
+ attr_reader :definition
14
+
15
+ # @param [DiverDown::Trace::ModuleSet, nil] module_set
16
+ # @param [DiverDown::Trace::IgnoredMethodIds, nil] ignored_method_ids
17
+ # @param [Set<String>, nil] List of paths to finish traversing when searching for a caller. If nil, all paths are finished.
18
+ # @param [#call, nil] filter_method_id_path
19
+ def initialize(module_set: DiverDown::Trace::ModuleSet.new, ignored_method_ids: nil, caller_paths: nil, filter_method_id_path: nil, definition: DiverDown::Definition.new)
20
+ @module_set = module_set
21
+ @ignored_method_ids = ignored_method_ids
22
+ @caller_paths = caller_paths
23
+ @filter_method_id_path = filter_method_id_path
24
+ @definition = definition
25
+ @trace_point = build_trace_point
26
+ end
27
+
28
+ # @return [void]
29
+ def start
30
+ @trace_point.enable
31
+ end
32
+
33
+ # @return [void]
34
+ def stop
35
+ @trace_point.disable
36
+ end
37
+
38
+ private
39
+
40
+ def build_trace_point
41
+ call_stack = DiverDown::Trace::CallStack.new
42
+
43
+ TracePoint.new(*DiverDown::Trace::Tracer.trace_events) do |tp|
44
+ # Skip the trace of the library itself
45
+ next if tp.path&.start_with?(DiverDown::LIB_DIR)
46
+ next if TracePoint == tp.defined_class
47
+
48
+ case tp.event
49
+ when :call, :c_call
50
+ # puts "#{tp.method_id} #{tp.path}:#{tp.lineno}"
51
+ if call_stack.ignored?
52
+ call_stack.push
53
+ next
54
+ end
55
+
56
+ mod = DiverDown::Helper.resolve_module(tp.self)
57
+
58
+ if mod.nil?
59
+ call_stack.push
60
+ next
61
+ end
62
+
63
+ if !@ignored_method_ids.nil? && @ignored_method_ids.ignored?(mod, DiverDown::Helper.module?(tp.self), tp.method_id)
64
+ # If this method is ignored, the call stack is ignored until the method returns.
65
+ call_stack.push(ignored: true)
66
+ next
67
+ end
68
+
69
+ source_name = DiverDown::Helper.normalize_module_name(mod) if @module_set.include?(mod)
70
+ pushed = false
71
+
72
+ unless source_name.nil?
73
+ # If the call stack contains a call to a module to be traced
74
+ # `@ignored_call_stack` is not nil means the call stack contains a call to a module to be ignored
75
+ unless call_stack.empty_context_stack?
76
+ # Add dependency to called source
77
+ called_stack_context = call_stack.context_stack[-1]
78
+ called_source = called_stack_context.source
79
+ dependency = called_source.find_or_build_dependency(source_name)
80
+
81
+ # `dependency.nil?` means source_name equals to called_source.source.
82
+ # self-references are not tracked because it is not "dependency".
83
+ if dependency
84
+ context = DiverDown::Helper.module?(tp.self) ? 'class' : 'instance'
85
+ method_id = dependency.find_or_build_method_id(name: tp.method_id, context:)
86
+ method_id_path = "#{called_stack_context.path}:#{called_stack_context.lineno}"
87
+ method_id_path = @filter_method_id_path.call(method_id_path) if @filter_method_id_path
88
+ method_id.add_path(method_id_path)
89
+ end
90
+ end
91
+
92
+ # Search is a heavy process and should be terminated early.
93
+ # The position of the most recently found caller or the start of trace is used as the maximum value.
94
+ maximum_back_stack_size = if call_stack.empty_context_stack?
95
+ call_stack.stack_size
96
+ else
97
+ call_stack.stack_size - call_stack.context_stack_size[-1]
98
+ end
99
+
100
+ caller_location = find_neast_caller_location(maximum_back_stack_size)
101
+
102
+ # `caller_location` is nil if it is filtered by caller_paths
103
+ if caller_location
104
+ pushed = true
105
+ source = @definition.find_or_build_source(source_name)
106
+
107
+ call_stack.push(
108
+ StackContext.new(
109
+ source:,
110
+ method_id: tp.method_id,
111
+ path: caller_location.path,
112
+ lineno: caller_location.lineno
113
+ )
114
+ )
115
+ end
116
+ end
117
+
118
+ call_stack.push unless pushed
119
+ when :return, :c_return
120
+ call_stack.pop
121
+ end
122
+ rescue StandardError
123
+ tp.disable
124
+ raise
125
+ end
126
+ end
127
+
128
+ def find_neast_caller_location(stack_size)
129
+ excluded_frame_size = 1 # Ignore neast caller because it is stack of #find_neast_caller_location.
130
+ finished_frame_size = excluded_frame_size + stack_size + 1
131
+ frame_pos = 1
132
+
133
+ # If @caller_paths is nil, return the caller location.
134
+ return caller_locations(excluded_frame_size + 1, excluded_frame_size + 1)[0] if @caller_paths.nil?
135
+
136
+ Thread.each_caller_location do
137
+ break if finished_frame_size < frame_pos
138
+ return _1 if @caller_paths.include?(_1.path)
139
+
140
+ frame_pos += 1
141
+ end
142
+
143
+ nil
144
+ end
145
+ end
146
+ end
147
+ end
@@ -3,12 +3,6 @@
3
3
  module DiverDown
4
4
  module Trace
5
5
  class Tracer
6
- StackContext = Data.define(
7
- :source,
8
- :method_id,
9
- :caller_location
10
- )
11
-
12
6
  # @return [Array<Symbol>]
13
7
  def self.trace_events
14
8
  @trace_events || %i[call c_call return c_return]
@@ -20,13 +14,13 @@ module DiverDown
20
14
  end
21
15
 
22
16
  # @param module_set [DiverDown::Trace::ModuleSet, Array<Module, String>]
23
- # @param target_files [Array<String>, nil] if nil, trace all files
17
+ # @param caller_paths [Array<String>, nil] if nil, trace all files
24
18
  # @param ignored_method_ids [Array<String>]
25
19
  # @param filter_method_id_path [#call, nil] filter method_id.path
26
20
  # @param module_set [DiverDown::Trace::ModuleSet, nil] for optimization
27
- def initialize(module_set: {}, target_files: nil, ignored_method_ids: nil, filter_method_id_path: nil)
28
- if target_files && !target_files.all? { Pathname.new(_1).absolute? }
29
- raise ArgumentError, "target_files must be absolute path(#{target_files})"
21
+ def initialize(module_set: {}, caller_paths: nil, ignored_method_ids: nil, filter_method_id_path: nil)
22
+ if caller_paths && !caller_paths.all? { Pathname.new(_1).absolute? }
23
+ raise ArgumentError, "caller_paths must be absolute path(#{caller_paths})"
30
24
  end
31
25
 
32
26
  @module_set = if module_set.is_a?(DiverDown::Trace::ModuleSet)
@@ -52,7 +46,7 @@ module DiverDown
52
46
  DiverDown::Trace::IgnoredMethodIds.new(ignored_method_ids)
53
47
  end
54
48
 
55
- @target_file_set = target_files&.to_set
49
+ @caller_paths = caller_paths&.to_set
56
50
  @filter_method_id_path = filter_method_id_path
57
51
  end
58
52
 
@@ -80,10 +74,10 @@ module DiverDown
80
74
  #
81
75
  # @return [TracePoint]
82
76
  def new_session(title: SecureRandom.uuid, definition_group: nil)
83
- DiverDown::Trace::Tracer::Session.new(
77
+ DiverDown::Trace::Session.new(
84
78
  module_set: @module_set,
85
79
  ignored_method_ids: @ignored_method_ids,
86
- target_file_set: @target_file_set,
80
+ caller_paths: @caller_paths,
87
81
  filter_method_id_path: @filter_method_id_path,
88
82
  definition: DiverDown::Definition.new(
89
83
  title:,
@@ -5,7 +5,7 @@ require 'active_support/inflector'
5
5
  module DiverDown
6
6
  module Trace
7
7
  require 'diver_down/trace/tracer'
8
- require 'diver_down/trace/tracer/session'
8
+ require 'diver_down/trace/session'
9
9
  require 'diver_down/trace/call_stack'
10
10
  require 'diver_down/trace/module_set'
11
11
  require 'diver_down/trace/redefine_ruby_methods'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DiverDown
4
- VERSION = '0.0.1.alpha2'
4
+ VERSION = '0.0.1.alpha3'
5
5
  end
@@ -119,8 +119,9 @@ module DiverDown
119
119
  # @param per [Integer]
120
120
  # @param title [String]
121
121
  # @param source [String]
122
- def definitions(page:, per:, title:, source:)
123
- definition_enumerator = DiverDown::Web::DefinitionEnumerator.new(@store, title:, source:)
122
+ # @param definition_group [String]
123
+ def definitions(page:, per:, title:, source:, definition_group:)
124
+ definition_enumerator = DiverDown::Web::DefinitionEnumerator.new(@store, title:, source:, definition_group:)
124
125
  definitions, pagination = paginate(definition_enumerator, page, per)
125
126
 
126
127
  json(
@@ -7,10 +7,12 @@ module DiverDown
7
7
 
8
8
  # @param store [DiverDown::Definition::Store]
9
9
  # @param query [String]
10
- def initialize(store, title: '', source: '')
10
+ # @param definition_group [String]
11
+ def initialize(store, title: '', source: '', definition_group: '')
11
12
  @store = store
12
13
  @title = title
13
14
  @source = source
15
+ @definition_group = definition_group
14
16
  end
15
17
 
16
18
  # @yield [parent_bit_id, bit_id, definition]
@@ -47,6 +49,10 @@ module DiverDown
47
49
  matched &&= definition.sources.any? { _1.source_name.include?(@source) }
48
50
  end
49
51
 
52
+ unless @definition_group.empty?
53
+ matched &&= definition.definition_group.to_s.include?(@definition_group)
54
+ end
55
+
50
56
  matched
51
57
  end
52
58
  end
@@ -45,7 +45,8 @@ module DiverDown
45
45
  page: request.params['page']&.to_i || 1,
46
46
  per: request.params['per']&.to_i || 100,
47
47
  title: request.params['title'] || '',
48
- source: request.params['source'] || ''
48
+ source: request.params['source'] || '',
49
+ definition_group: request.params['definition_group'] || ''
49
50
  )
50
51
  in ['GET', %r{\A/api/sources\.json\z}]
51
52
  action.sources