taski 0.3.0 → 0.4.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
  4. data/.gem_rbs_collection/minitest/5.25/.rbs_meta.yaml +9 -0
  5. data/.gem_rbs_collection/minitest/5.25/minitest/abstract_reporter.rbs +52 -0
  6. data/.gem_rbs_collection/minitest/5.25/minitest/assertion.rbs +17 -0
  7. data/.gem_rbs_collection/minitest/5.25/minitest/assertions.rbs +590 -0
  8. data/.gem_rbs_collection/minitest/5.25/minitest/backtrace_filter.rbs +23 -0
  9. data/.gem_rbs_collection/minitest/5.25/minitest/bench_spec.rbs +102 -0
  10. data/.gem_rbs_collection/minitest/5.25/minitest/benchmark.rbs +259 -0
  11. data/.gem_rbs_collection/minitest/5.25/minitest/composite_reporter.rbs +25 -0
  12. data/.gem_rbs_collection/minitest/5.25/minitest/compress.rbs +13 -0
  13. data/.gem_rbs_collection/minitest/5.25/minitest/error_on_warning.rbs +3 -0
  14. data/.gem_rbs_collection/minitest/5.25/minitest/expectation.rbs +2 -0
  15. data/.gem_rbs_collection/minitest/5.25/minitest/expectations.rbs +21 -0
  16. data/.gem_rbs_collection/minitest/5.25/minitest/guard.rbs +64 -0
  17. data/.gem_rbs_collection/minitest/5.25/minitest/mock.rbs +64 -0
  18. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/executor.rbs +46 -0
  19. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test/class_methods.rbs +5 -0
  20. data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test.rbs +3 -0
  21. data/.gem_rbs_collection/minitest/5.25/minitest/parallel.rbs +2 -0
  22. data/.gem_rbs_collection/minitest/5.25/minitest/pride_io.rbs +62 -0
  23. data/.gem_rbs_collection/minitest/5.25/minitest/pride_lol.rbs +19 -0
  24. data/.gem_rbs_collection/minitest/5.25/minitest/progress_reporter.rbs +11 -0
  25. data/.gem_rbs_collection/minitest/5.25/minitest/reportable.rbs +53 -0
  26. data/.gem_rbs_collection/minitest/5.25/minitest/reporter.rbs +5 -0
  27. data/.gem_rbs_collection/minitest/5.25/minitest/result.rbs +28 -0
  28. data/.gem_rbs_collection/minitest/5.25/minitest/runnable.rbs +163 -0
  29. data/.gem_rbs_collection/minitest/5.25/minitest/skip.rbs +6 -0
  30. data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl/instance_methods.rbs +48 -0
  31. data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl.rbs +129 -0
  32. data/.gem_rbs_collection/minitest/5.25/minitest/spec.rbs +11 -0
  33. data/.gem_rbs_collection/minitest/5.25/minitest/statistics_reporter.rbs +81 -0
  34. data/.gem_rbs_collection/minitest/5.25/minitest/summary_reporter.rbs +18 -0
  35. data/.gem_rbs_collection/minitest/5.25/minitest/test/lifecycle_hooks.rbs +92 -0
  36. data/.gem_rbs_collection/minitest/5.25/minitest/test.rbs +69 -0
  37. data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_error.rbs +12 -0
  38. data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_warning.rbs +6 -0
  39. data/.gem_rbs_collection/minitest/5.25/minitest/unit/test_case.rbs +3 -0
  40. data/.gem_rbs_collection/minitest/5.25/minitest/unit.rbs +4 -0
  41. data/.gem_rbs_collection/minitest/5.25/minitest.rbs +115 -0
  42. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  43. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  44. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
  45. data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
  46. data/.gem_rbs_collection/parser/3.2/parser.rbs +193 -0
  47. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
  48. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
  49. data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
  50. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
  51. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
  52. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  53. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  54. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  55. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
  56. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
  57. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
  58. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +129 -0
  59. data/.gem_rbs_collection/rubocop-ast/1.30/.rbs_meta.yaml +9 -0
  60. data/.gem_rbs_collection/rubocop-ast/1.30/rubocop-ast.rbs +771 -0
  61. data/.gem_rbs_collection/simplecov/0.22/.rbs_meta.yaml +9 -0
  62. data/.gem_rbs_collection/simplecov/0.22/simplecov.rbs +54 -0
  63. data/README.md +137 -248
  64. data/Steepfile +19 -0
  65. data/docs/advanced-features.md +625 -0
  66. data/docs/api-guide.md +509 -0
  67. data/docs/error-handling.md +684 -0
  68. data/examples/README.md +95 -42
  69. data/examples/context_demo.rb +112 -0
  70. data/examples/data_pipeline_demo.rb +231 -0
  71. data/examples/parallel_progress_demo.rb +72 -0
  72. data/examples/quick_start.rb +4 -4
  73. data/examples/reexecution_demo.rb +127 -0
  74. data/examples/{section_configuration.rb → section_demo.rb} +49 -66
  75. data/lib/taski/context.rb +52 -0
  76. data/lib/taski/execution/coordinator.rb +63 -0
  77. data/lib/taski/execution/parallel_progress_display.rb +201 -0
  78. data/lib/taski/execution/registry.rb +72 -0
  79. data/lib/taski/execution/task_wrapper.rb +255 -0
  80. data/lib/taski/section.rb +26 -250
  81. data/lib/taski/static_analysis/analyzer.rb +34 -0
  82. data/lib/taski/static_analysis/dependency_graph.rb +90 -0
  83. data/lib/taski/static_analysis/visitor.rb +114 -0
  84. data/lib/taski/task.rb +173 -0
  85. data/lib/taski/version.rb +1 -1
  86. data/lib/taski.rb +45 -39
  87. data/rbs_collection.lock.yaml +116 -0
  88. data/rbs_collection.yaml +19 -0
  89. data/sig/taski.rbs +269 -62
  90. metadata +97 -32
  91. data/examples/advanced_patterns.rb +0 -119
  92. data/examples/progress_demo.rb +0 -166
  93. data/examples/tree_demo.rb +0 -205
  94. data/lib/taski/dependency_analyzer.rb +0 -231
  95. data/lib/taski/exceptions.rb +0 -17
  96. data/lib/taski/logger.rb +0 -158
  97. data/lib/taski/logging/formatter_factory.rb +0 -34
  98. data/lib/taski/logging/formatter_interface.rb +0 -19
  99. data/lib/taski/logging/json_formatter.rb +0 -26
  100. data/lib/taski/logging/simple_formatter.rb +0 -16
  101. data/lib/taski/logging/structured_formatter.rb +0 -44
  102. data/lib/taski/progress/display_colors.rb +0 -17
  103. data/lib/taski/progress/display_manager.rb +0 -115
  104. data/lib/taski/progress/output_capture.rb +0 -105
  105. data/lib/taski/progress/spinner_animation.rb +0 -46
  106. data/lib/taski/progress/task_formatter.rb +0 -25
  107. data/lib/taski/progress/task_status.rb +0 -38
  108. data/lib/taski/progress/terminal_controller.rb +0 -35
  109. data/lib/taski/progress_display.rb +0 -59
  110. data/lib/taski/reference.rb +0 -40
  111. data/lib/taski/task/base.rb +0 -90
  112. data/lib/taski/task/define_api.rb +0 -154
  113. data/lib/taski/task/dependency_resolver.rb +0 -73
  114. data/lib/taski/task/exports_api.rb +0 -31
  115. data/lib/taski/task/instance_management.rb +0 -203
  116. data/lib/taski/tree_colors.rb +0 -91
  117. data/lib/taski/utils/dependency_resolver_helper.rb +0 -85
  118. data/lib/taski/utils/tree_display_helper.rb +0 -71
  119. data/lib/taski/utils.rb +0 -107
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Taski
6
+ module Execution
7
+ class Registry
8
+ def initialize
9
+ @tasks = {}
10
+ @threads = []
11
+ @monitor = Monitor.new
12
+ @abort_requested = false
13
+ end
14
+
15
+ # @param task_class [Class] The task class
16
+ # @yield Block to create the task instance if it doesn't exist
17
+ # @return [Object] The task instance
18
+ def get_or_create(task_class)
19
+ @tasks[task_class] ||= yield
20
+ end
21
+
22
+ # @param task_class [Class] The task class
23
+ # @return [Object] The task instance
24
+ # @raise [RuntimeError] If the task is not registered
25
+ def get_task(task_class)
26
+ @tasks.fetch(task_class) do
27
+ raise "Task #{task_class} not registered"
28
+ end
29
+ end
30
+
31
+ # @param thread [Thread] The thread to register
32
+ def register_thread(thread)
33
+ @monitor.synchronize { @threads << thread }
34
+ end
35
+
36
+ def wait_all
37
+ threads = @monitor.synchronize { @threads.dup }
38
+ threads.each(&:join)
39
+ end
40
+
41
+ def reset!
42
+ @monitor.synchronize do
43
+ @tasks.clear
44
+ @threads.clear
45
+ @abort_requested = false
46
+ end
47
+ end
48
+
49
+ def request_abort!
50
+ @monitor.synchronize { @abort_requested = true }
51
+ end
52
+
53
+ # @return [Boolean] true if abort has been requested
54
+ def abort_requested?
55
+ @monitor.synchronize { @abort_requested }
56
+ end
57
+
58
+ # @param task_class [Class] The task class to run
59
+ # @param exported_methods [Array<Symbol>] Methods to call to trigger execution
60
+ # @return [Object] The result of the task execution
61
+ def run(task_class, exported_methods)
62
+ exported_methods.each do |method|
63
+ task_class.public_send(method)
64
+ end
65
+
66
+ wait_all
67
+
68
+ get_task(task_class).result
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Taski
6
+ module Execution
7
+ class TaskTiming < Data.define(:start_time, :end_time)
8
+ # @return [Float, nil] Duration in milliseconds or nil if not available
9
+ def duration_ms
10
+ return nil unless start_time && end_time
11
+ ((end_time - start_time) * 1000).round(1)
12
+ end
13
+
14
+ # @return [TaskTiming] New timing with current time as start
15
+ def self.start_now
16
+ new(start_time: Time.now, end_time: nil)
17
+ end
18
+
19
+ # @return [TaskTiming] New timing with current time as end
20
+ def with_end_now
21
+ with(end_time: Time.now)
22
+ end
23
+ end
24
+
25
+ class TaskWrapper
26
+ attr_reader :task, :result
27
+
28
+ STATE_PENDING = :pending
29
+ STATE_RUNNING = :running
30
+ STATE_COMPLETED = :completed
31
+
32
+ def initialize(task, registry:, coordinator:)
33
+ @task = task
34
+ @registry = registry
35
+ @coordinator = coordinator
36
+ @result = nil
37
+ @clean_result = nil
38
+ @error = nil
39
+ @monitor = Monitor.new
40
+ @condition = @monitor.new_cond
41
+ @clean_condition = @monitor.new_cond
42
+ @state = STATE_PENDING
43
+ @clean_state = STATE_PENDING
44
+ @timing = nil
45
+
46
+ register_with_progress_display
47
+ end
48
+
49
+ # @return [Object] The result of task execution
50
+ def run
51
+ execute_task_if_needed
52
+ raise @error if @error # steep:ignore
53
+ @result
54
+ end
55
+
56
+ # @return [Object] The result of cleanup
57
+ def clean
58
+ execute_clean_if_needed
59
+ @clean_result
60
+ end
61
+
62
+ # @param method_name [Symbol] The name of the exported method
63
+ # @return [Object] The exported value
64
+ def get_exported_value(method_name)
65
+ execute_task_if_needed
66
+ raise @error if @error # steep:ignore
67
+ @task.public_send(method_name)
68
+ end
69
+
70
+ private
71
+
72
+ def start_thread_with(&block)
73
+ thread = Thread.new(&block)
74
+ @registry.register_thread(thread)
75
+ end
76
+
77
+ # Thread-safe state machine that ensures operations are executed exactly once.
78
+ # Uses pattern matching for exhaustive state handling.
79
+ def execute_with_state_pattern(state_getter:, starter:, waiter:, pre_start_check: nil)
80
+ @monitor.synchronize do
81
+ case state_getter.call
82
+ in STATE_PENDING
83
+ pre_start_check&.call
84
+ starter.call
85
+ waiter.call
86
+ in STATE_RUNNING
87
+ waiter.call
88
+ in STATE_COMPLETED
89
+ return
90
+ end
91
+ end
92
+ end
93
+
94
+ def execute_task_if_needed
95
+ execute_with_state_pattern(
96
+ state_getter: -> { @state },
97
+ starter: -> { start_async_execution },
98
+ waiter: -> { wait_for_completion },
99
+ pre_start_check: -> {
100
+ if @registry.abort_requested?
101
+ raise Taski::TaskAbortException, "Execution aborted - no new tasks will start"
102
+ end
103
+ }
104
+ )
105
+ end
106
+
107
+ def start_async_execution
108
+ @state = STATE_RUNNING
109
+ @timing = TaskTiming.start_now
110
+ update_progress(:running)
111
+ start_thread_with { execute_task }
112
+ end
113
+
114
+ def execute_task
115
+ if @registry.abort_requested?
116
+ @error = Taski::TaskAbortException.new("Execution aborted - no new tasks will start")
117
+ mark_completed
118
+ return
119
+ end
120
+
121
+ log_start
122
+ @coordinator.start_dependencies(@task.class)
123
+ wait_for_dependencies
124
+ @result = @task.run
125
+ mark_completed
126
+ log_completion
127
+ rescue Taski::TaskAbortException => e
128
+ @registry.request_abort!
129
+ @error = e
130
+ mark_completed
131
+ rescue => e
132
+ @error = e
133
+ mark_completed
134
+ end
135
+
136
+ def wait_for_dependencies
137
+ dependencies = @task.class.cached_dependencies
138
+ return if dependencies.empty?
139
+
140
+ dependencies.each do |dep_class|
141
+ dep_class.exported_methods.each do |method|
142
+ dep_class.public_send(method)
143
+ end
144
+ end
145
+ end
146
+
147
+ def execute_clean_if_needed
148
+ execute_with_state_pattern(
149
+ state_getter: -> { @clean_state },
150
+ starter: -> { start_async_clean },
151
+ waiter: -> { wait_for_clean_completion }
152
+ )
153
+ end
154
+
155
+ def start_async_clean
156
+ @clean_state = STATE_RUNNING
157
+ start_thread_with { execute_clean }
158
+ end
159
+
160
+ def execute_clean
161
+ log_clean_start
162
+ @clean_result = @task.clean
163
+ wait_for_clean_dependencies
164
+ mark_clean_completed
165
+ log_clean_completion
166
+ end
167
+
168
+ def wait_for_clean_dependencies
169
+ dependencies = @task.class.cached_dependencies
170
+ return if dependencies.empty?
171
+
172
+ wait_threads = dependencies.map do |dep_class|
173
+ Thread.new do
174
+ dep_class.public_send(:clean)
175
+ end
176
+ end
177
+
178
+ wait_threads.each(&:join)
179
+ end
180
+
181
+ def mark_completed
182
+ @timing = @timing&.with_end_now
183
+ @monitor.synchronize do
184
+ @state = STATE_COMPLETED
185
+ @condition.broadcast
186
+ end
187
+
188
+ if @error
189
+ update_progress(:failed, error: @error)
190
+ else
191
+ update_progress(:completed, duration: @timing&.duration_ms)
192
+ end
193
+ end
194
+
195
+ def mark_clean_completed
196
+ @monitor.synchronize do
197
+ @clean_state = STATE_COMPLETED
198
+ @clean_condition.broadcast
199
+ end
200
+ end
201
+
202
+ def wait_for_completion
203
+ @condition.wait_until { @state == STATE_COMPLETED }
204
+ end
205
+
206
+ def wait_for_clean_completion
207
+ @clean_condition.wait_until { @clean_state == STATE_COMPLETED }
208
+ end
209
+
210
+ def debug_log(message)
211
+ return unless ENV["TASKI_DEBUG"]
212
+ puts message
213
+ end
214
+
215
+ def log_start
216
+ debug_log("Invoking #{@task.class} wrapper in thread #{Thread.current.object_id}...")
217
+ end
218
+
219
+ def log_completion
220
+ debug_log("Wrapper #{@task.class} completed in thread #{Thread.current.object_id}.")
221
+ end
222
+
223
+ def log_clean_start
224
+ debug_log("Cleaning #{@task.class} in thread #{Thread.current.object_id}...")
225
+ end
226
+
227
+ def log_clean_completion
228
+ debug_log("Clean #{@task.class} completed in thread #{Thread.current.object_id}.")
229
+ end
230
+
231
+ def register_with_progress_display
232
+ Taski.progress_display&.register_task(@task.class)
233
+ end
234
+
235
+ # @param state [Symbol] The new state
236
+ # @param duration [Float, nil] Duration in milliseconds
237
+ # @param error [Exception, nil] Error object
238
+ def update_progress(state, duration: nil, error: nil)
239
+ Taski.progress_display&.update_task(@task.class, state: state, duration: duration, error: error)
240
+ end
241
+
242
+ def method_missing(method_name, *args, &block)
243
+ if @task.class.method_defined?(method_name)
244
+ get_exported_value(method_name)
245
+ else
246
+ super
247
+ end
248
+ end
249
+
250
+ def respond_to_missing?(method_name, include_private = false)
251
+ @task.class.method_defined?(method_name) || super
252
+ end
253
+ end
254
+ end
255
+ end
data/lib/taski/section.rb CHANGED
@@ -1,268 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "dependency_analyzer"
4
- require_relative "utils"
5
- require_relative "utils/tree_display_helper"
6
- require_relative "utils/dependency_resolver_helper"
3
+ require_relative "task"
7
4
 
8
5
  module Taski
9
- # Section provides an interface abstraction layer for dynamic implementation selection
10
- # while maintaining static analysis capabilities
11
- class Section
6
+ class Section < Task
12
7
  class << self
13
- # === Dependency Resolution ===
14
-
15
- # Resolve method for dependency graph (called by resolve_dependencies)
16
- # @param queue [Array] Queue of tasks to process
17
- # @param resolved [Array] Array of resolved tasks
18
- # @return [self] Returns self for method chaining
19
- def resolve(queue, resolved)
20
- resolve_common(queue, resolved)
21
- end
22
-
23
- # Resolve all dependencies in topological order
24
- # @return [Array<Class>] Array of tasks in dependency order
25
- def resolve_dependencies
26
- resolve_dependencies_common
27
- end
28
-
29
- # Analyze dependencies when accessing interface methods
30
- def analyze_dependencies_for_interfaces
31
- interface_exports.each do |interface_method|
32
- dependencies = gather_static_dependencies_for_interface(interface_method)
33
- add_unique_dependencies(dependencies)
34
- end
35
- end
36
-
37
- private
38
-
39
- # Gather dependencies from interface method implementation
40
- def gather_static_dependencies_for_interface(interface_method)
41
- # For sections, we analyze the impl method
42
- # Try instance method first, then class method
43
- if impl_defined?
44
- # For instance method, we can't analyze dependencies statically
45
- # So we return empty array
46
- []
47
- else
48
- DependencyAnalyzer.analyze_method(self, :impl)
49
- end
50
- end
51
-
52
- # Add dependencies that don't already exist
53
- def add_unique_dependencies(dep_classes)
54
- dep_classes.each do |dep_class|
55
- next if dep_class == self || dependency_exists?(dep_class)
56
- add_dependency(dep_class)
57
- end
58
- end
59
-
60
- # Add a single dependency
61
- def add_dependency(dep_class)
62
- @dependencies ||= []
63
- @dependencies << {klass: dep_class}
64
- end
65
-
66
- # Check if dependency already exists
67
- def dependency_exists?(dep_class)
68
- (@dependencies || []).any? { |d| d[:klass] == dep_class }
69
- end
70
-
71
- # Extract class from dependency specification
72
- def extract_class(task)
73
- case task
74
- when Class
75
- task
76
- when Hash
77
- task[:klass]
78
- else
79
- task
80
- end
81
- end
82
-
83
- public
84
-
85
- # === Instance Management (minimal for Section) ===
86
-
87
- # Ensure section is available (no actual building needed)
88
- # @return [self] Returns self for compatibility with Task interface
89
- def ensure_instance_built
90
- self
91
- end
92
-
93
- # Build method for compatibility (Section doesn't build instances)
94
- # @param args [Hash] Optional arguments (ignored for sections)
95
- # @return [self] Returns self
96
- def build(**args)
97
- self
98
- end
99
-
100
- # Reset method for compatibility (Section doesn't have state to reset)
101
- # @return [self] Returns self
102
- def reset!
103
- self
104
- end
105
-
106
- # Display dependency tree for this section
107
- # @param prefix [String] Current indentation prefix
108
- # @param visited [Set] Set of visited classes to prevent infinite loops
109
- # @param color [Boolean] Whether to use color output
110
- # @return [String] Formatted dependency tree
111
- def tree(prefix = "", visited = Set.new, color: TreeColors.enabled?)
112
- should_return_early, early_result, new_visited = handle_circular_dependency_check(visited, self, prefix)
113
- return early_result if should_return_early
114
-
115
- # Get section name with fallback for anonymous classes
116
- section_name = name || to_s
117
- colored_section_name = color ? TreeColors.section(section_name) : section_name
118
- result = "#{prefix}#{colored_section_name}\n"
119
-
120
- # Add possible implementations (一般化 - detect from nested Task classes)
121
- possible_implementations = find_possible_implementations
122
- if possible_implementations.any?
123
- impl_names = possible_implementations.map { |impl| extract_implementation_name(impl) }
124
- impl_text = "[One of: #{impl_names.join(", ")}]"
125
- colored_impl_text = color ? TreeColors.implementations(impl_text) : impl_text
126
- connector = color ? TreeColors.connector("└── ") : "└── "
127
- result += "#{prefix}#{connector}#{colored_impl_text}\n"
128
- end
129
-
130
- dependencies = @dependencies || []
131
- result += render_dependencies_tree(dependencies, prefix, new_visited, color)
132
-
133
- result
134
- end
135
-
136
- # Define interface methods for this section
137
- def interface(*names)
138
- if names.empty?
139
- raise ArgumentError, "interface requires at least one method name"
140
- end
141
-
142
- @interface_exports = names
143
-
144
- # Create accessor methods for each interface name
145
- names.each do |name|
146
- define_singleton_method(name) do
147
- # Get implementation class
148
- implementation_class = get_implementation_class
149
-
150
- # Check if implementation is nil
151
- if implementation_class.nil?
152
- raise SectionImplementationError,
153
- "impl returned nil. " \
154
- "Make sure impl returns a Task class."
155
- end
156
-
157
- # Validate that it's a Task class
158
- unless implementation_class.is_a?(Class) && implementation_class < Taski::Task
159
- raise SectionImplementationError,
160
- "impl must return a Task class, got #{implementation_class.class}. " \
161
- "Make sure impl returns a class that inherits from Taski::Task."
162
- end
163
-
164
- # Build the implementation and call the method
165
- implementation = implementation_class.build
166
-
167
- begin
168
- implementation.send(name)
169
- rescue NoMethodError
170
- raise SectionImplementationError,
171
- "Implementation does not provide required method '#{name}'. " \
172
- "Make sure the implementation class has a '#{name}' method or " \
173
- "exports :#{name} declaration."
174
- end
175
- end
176
- end
177
- end
178
-
179
- # Get the interface exports for this section
180
- def interface_exports
181
- @interface_exports || []
182
- end
183
-
184
- # Check if impl method is defined (as instance method)
185
- def impl_defined?
186
- instance_methods(false).include?(:impl)
187
- end
188
-
189
- # Get implementation class from instance method
190
- def get_implementation_class
191
- if impl_defined?
192
- # Create a temporary instance to call impl method
193
- allocate.impl
194
- else
195
- # Fall back to class method if exists
196
- impl
197
- end
198
- end
199
-
200
- # Override const_set to auto-add exports to nested Task classes
201
- def const_set(name, value)
202
- result = super
203
-
204
- # If the constant is a Task class and we have interface exports,
205
- # automatically add exports to avoid duplication
206
- if value.is_a?(Class) && value < Taski::Task && !interface_exports.empty?
207
- # Add exports declaration to the nested task
208
- exports_list = interface_exports
209
- value.class_eval do
210
- exports(*exports_list)
211
- end
212
- end
213
-
214
- result
215
- end
216
-
217
- # Apply auto-exports to all nested Task classes
218
- # Call this method after defining nested Task classes to automatically add exports
219
- def apply_auto_exports
220
- constants.each do |const_name|
221
- const_value = const_get(const_name)
222
- if const_value.is_a?(Class) && const_value < Taski::Task && !interface_exports.empty?
223
- exports_list = interface_exports
224
- const_value.class_eval do
225
- exports(*exports_list) unless @exports_defined
226
- @exports_defined = true
227
- end
228
- end
229
- end
8
+ # @param interface_methods [Array<Symbol>] Names of interface methods
9
+ def interfaces(*interface_methods)
10
+ exports(*interface_methods)
230
11
  end
12
+ end
231
13
 
232
- # Find possible implementation classes by scanning nested Task classes
233
- def find_possible_implementations
234
- task_classes = []
235
- constants.each do |const_name|
236
- const_value = const_get(const_name)
237
- if task_class?(const_value)
238
- task_classes << const_value
239
- end
240
- end
241
- task_classes
14
+ def run
15
+ implementation_class = impl
16
+ unless implementation_class
17
+ raise "Section #{self.class} does not have an implementation. Override 'impl' method."
242
18
  end
243
19
 
244
- # Extract readable name from implementation class
245
- def extract_implementation_name(impl_class)
246
- class_name = impl_class.name
247
- return impl_class.to_s unless class_name&.include?("::")
20
+ apply_interface_to_implementation(implementation_class)
248
21
 
249
- class_name.split("::").last
22
+ self.class.exported_methods.each do |method|
23
+ value = implementation_class.public_send(method)
24
+ instance_variable_set("@#{method}", value)
250
25
  end
26
+ end
251
27
 
252
- # Check if a constant value is a Task class
253
- def task_class?(const_value)
254
- const_value.is_a?(Class) && const_value < Taski::Task
255
- end
28
+ # @return [Class] The implementation task class
29
+ # @raise [NotImplementedError] If not implemented by subclass
30
+ def impl
31
+ raise NotImplementedError, "Subclasses must implement the impl method to return implementation class"
32
+ end
256
33
 
257
- private
34
+ private
258
35
 
259
- include Utils::TreeDisplayHelper
260
- include Utils::DependencyResolverHelper
36
+ # @param implementation_class [Class] The implementation task class
37
+ def apply_interface_to_implementation(implementation_class)
38
+ interface_methods = self.class.exported_methods
39
+ return if interface_methods.empty?
261
40
 
262
- # Subclasses should override this method to select appropriate implementation
263
- def impl
264
- raise NotImplementedError, "Subclass must implement impl"
265
- end
41
+ implementation_class.exports(*interface_methods)
266
42
  end
267
43
  end
268
44
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "visitor"
5
+
6
+ module Taski
7
+ module StaticAnalysis
8
+ class Analyzer
9
+ # @param task_class [Class] The task class to analyze
10
+ # @return [Set<Class>] Set of task classes that are dependencies
11
+ def self.analyze(task_class)
12
+ source_location = extract_run_method_location(task_class)
13
+ return Set.new unless source_location
14
+
15
+ file_path, _line_number = source_location
16
+ parse_result = Prism.parse_file(file_path)
17
+
18
+ visitor = Visitor.new(task_class)
19
+ visitor.visit(parse_result.value)
20
+ visitor.dependencies
21
+ end
22
+
23
+ # @param task_class [Class] The task class
24
+ # @return [Array<String, Integer>, nil] File path and line number, or nil
25
+ def self.extract_run_method_location(task_class)
26
+ task_class.instance_method(:run).source_location
27
+ rescue NameError
28
+ nil
29
+ end
30
+
31
+ private_class_method :extract_run_method_location
32
+ end
33
+ end
34
+ end