taski 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +295 -30
- data/Steepfile +20 -0
- data/examples/complex_example.rb +109 -0
- data/examples/readme_example.rb +30 -0
- data/lib/taski/dependency_analyzer.rb +162 -0
- data/lib/taski/exceptions.rb +14 -0
- data/lib/taski/reference.rb +40 -0
- data/lib/taski/task/base.rb +60 -0
- data/lib/taski/task/define_api.rb +125 -0
- data/lib/taski/task/dependency_resolver.rb +105 -0
- data/lib/taski/task/exports_api.rb +31 -0
- data/lib/taski/task/instance_management.rb +135 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +35 -138
- data/sig/taski.rbs +60 -2
- metadata +28 -4
- data/lib/taski/utils.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b66b9afd145af8c5a07839b1886212336ffbdcb935897a278afae10315162e8
|
4
|
+
data.tar.gz: 7f43e874932671c2a41adc776514022e7d44d9ec0018935e2bd2e7e4d4537543
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8175467340490afdd0e9f1d4ba5c2e3db257e7d4c62850f88a138676056ea140753e2c38042803dd46c6c071731245314c2b597af83eb8c237be28d20e998e60
|
7
|
+
data.tar.gz: d1695488ca402cdb18800bd4b8623c64ed3c681a5dd6039c651c117c99793045fa59cca85f82cde0fde311b0ada0d81b8a66ba4d8cbb9b576ebe00fe71e760cf
|
data/README.md
CHANGED
@@ -1,68 +1,333 @@
|
|
1
1
|
# Taski
|
2
2
|
|
3
|
-
|
4
|
-
In Taski, you define tasks as Ruby classes that expose named values through `define`. Dependencies between tasks are established automatically when one task references the result of another—no need for explicit dependency declarations.
|
3
|
+
> **🚧 Development Status:** Taski is currently under active development and the API may change. Not yet recommended for production use.
|
5
4
|
|
6
|
-
|
5
|
+
**Taski** is a powerful Ruby framework for building task dependency graphs with automatic resolution and execution. It provides two complementary APIs for different use cases: static dependencies through exports and dynamic dependencies through define.
|
7
6
|
|
8
|
-
|
7
|
+
## 🎯 Key Features
|
9
8
|
|
10
|
-
|
9
|
+
- **Automatic Dependency Resolution**: Dependencies are detected automatically through static analysis and runtime evaluation
|
10
|
+
- **Two Complementary APIs**: Choose the right approach for your use case
|
11
|
+
- **Exports API**: For simple, static dependencies
|
12
|
+
- **Define API**: For complex, dynamic dependencies based on runtime conditions
|
13
|
+
- **Thread-Safe Execution**: Safe for concurrent access with Monitor-based synchronization
|
14
|
+
- **Circular Dependency Detection**: Prevents infinite loops with clear error messages
|
15
|
+
- **Memory Leak Prevention**: Built-in reset mechanisms for long-running applications
|
16
|
+
- **Topological Execution**: Tasks execute in correct dependency order automatically
|
17
|
+
- **Reverse Cleanup**: Clean operations run in reverse dependency order
|
11
18
|
|
12
|
-
|
19
|
+
## 🚀 Quick Start
|
13
20
|
|
14
|
-
|
21
|
+
```ruby
|
22
|
+
require 'taski'
|
23
|
+
|
24
|
+
# Simple static dependency using Exports API
|
25
|
+
class DatabaseSetup < Taski::Task
|
26
|
+
exports :connection_string
|
27
|
+
|
28
|
+
def build
|
29
|
+
@connection_string = "postgresql://localhost/myapp"
|
30
|
+
puts "Database configured"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class APIServer < Taski::Task
|
35
|
+
exports :port
|
36
|
+
|
37
|
+
def build
|
38
|
+
# Automatic dependency: DatabaseSetup will be built first
|
39
|
+
puts "Starting API with #{DatabaseSetup.connection_string}"
|
40
|
+
@port = 3000
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Execute - dependencies are resolved automatically
|
45
|
+
APIServer.build
|
46
|
+
# => Database configured
|
47
|
+
# => Starting API with postgresql://localhost/myapp
|
48
|
+
```
|
49
|
+
|
50
|
+
## 📚 API Guide
|
51
|
+
|
52
|
+
### Exports API - Static Dependencies
|
53
|
+
|
54
|
+
Use the **Exports API** when you have simple, predictable dependencies:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class ConfigLoader < Taski::Task
|
58
|
+
exports :app_name, :version
|
59
|
+
|
60
|
+
def build
|
61
|
+
@app_name = "MyApp"
|
62
|
+
@version = "1.0.0"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Deployment < Taski::Task
|
67
|
+
exports :deploy_url
|
68
|
+
|
69
|
+
def build
|
70
|
+
# Static dependency - always uses ConfigLoader
|
71
|
+
@deploy_url = "https://#{ConfigLoader.app_name}.example.com"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### Define API - Dynamic Dependencies
|
77
|
+
|
78
|
+
Use the **Define API** when dependencies change based on runtime conditions:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class EnvironmentConfig < Taski::Task
|
82
|
+
define :database_service, -> {
|
83
|
+
# Dynamic dependency based on environment
|
84
|
+
case ENV['RAILS_ENV']
|
85
|
+
when 'production'
|
86
|
+
ProductionDatabase.setup
|
87
|
+
when 'staging'
|
88
|
+
StagingDatabase.setup
|
89
|
+
else
|
90
|
+
DevelopmentDatabase.setup
|
91
|
+
end
|
92
|
+
}
|
93
|
+
|
94
|
+
define :cache_strategy, -> {
|
95
|
+
# Dynamic dependency based on feature flags
|
96
|
+
if FeatureFlag.enabled?(:redis_cache)
|
97
|
+
RedisCache.configure
|
98
|
+
else
|
99
|
+
MemoryCache.configure
|
100
|
+
end
|
101
|
+
}
|
102
|
+
|
103
|
+
def build
|
104
|
+
puts "Using #{database_service}"
|
105
|
+
puts "Cache: #{cache_strategy}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
> **⚠️ Note:** The `define` API uses dynamic method definition, which may generate Ruby warnings about method redefinition. This is expected behavior due to the dependency resolution mechanism and does not affect functionality.
|
15
111
|
|
16
|
-
|
17
|
-
- Implicit dependencies via reference to other task outputs
|
18
|
-
- Topological execution order
|
19
|
-
- Reverse execution for cleanup
|
20
|
-
- Built entirely in Ruby
|
112
|
+
### When to Use Each API
|
21
113
|
|
22
|
-
|
114
|
+
| Use Case | Recommended API | Example |
|
115
|
+
|----------|----------------|---------|
|
116
|
+
| Simple value exports | Exports API | Configuration values, file paths |
|
117
|
+
| Environment-specific logic | Define API | Different services per environment |
|
118
|
+
| Feature flag dependencies | Define API | Optional components based on flags |
|
119
|
+
| Conditional processing | Define API | Different algorithms based on input |
|
120
|
+
| Static file dependencies | Exports API | Build artifacts, compiled assets |
|
121
|
+
|
122
|
+
## 🔧 Advanced Features
|
123
|
+
|
124
|
+
### Thread Safety
|
125
|
+
|
126
|
+
Taski is thread-safe and handles concurrent access gracefully:
|
23
127
|
|
24
128
|
```ruby
|
25
|
-
|
26
|
-
|
129
|
+
# Multiple threads can safely access the same task
|
130
|
+
threads = 5.times.map do
|
131
|
+
Thread.new { MyTask.some_value }
|
132
|
+
end
|
133
|
+
|
134
|
+
# All threads get the same instance - built only once
|
135
|
+
results = threads.map(&:value)
|
136
|
+
```
|
137
|
+
|
138
|
+
### Error Handling
|
139
|
+
|
140
|
+
Comprehensive error handling with custom exception types:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
begin
|
144
|
+
TaskWithCircularDep.build
|
145
|
+
rescue Taski::CircularDependencyError => e
|
146
|
+
puts "Circular dependency detected: #{e.message}"
|
147
|
+
rescue Taski::TaskBuildError => e
|
148
|
+
puts "Build failed: #{e.message}"
|
149
|
+
end
|
150
|
+
```
|
151
|
+
|
152
|
+
### Lifecycle Management
|
27
153
|
|
154
|
+
Full control over task lifecycle:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
class ProcessingTask < Taski::Task
|
28
158
|
def build
|
29
|
-
|
159
|
+
# Setup and processing logic
|
160
|
+
puts "Processing data..."
|
161
|
+
end
|
162
|
+
|
163
|
+
def clean
|
164
|
+
# Cleanup logic (runs in reverse dependency order)
|
165
|
+
puts "Cleaning up temporary files..."
|
30
166
|
end
|
31
167
|
end
|
32
168
|
|
33
|
-
|
34
|
-
|
169
|
+
# Build dependencies in correct order
|
170
|
+
ProcessingTask.build
|
171
|
+
|
172
|
+
# Clean in reverse order
|
173
|
+
ProcessingTask.clean
|
174
|
+
```
|
175
|
+
|
176
|
+
## 🏗️ Complex Example
|
177
|
+
|
178
|
+
Here's a realistic example showing both APIs working together:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
# Environment configuration using Define API
|
182
|
+
class Environment < Taski::Task
|
183
|
+
define :database_url, -> {
|
184
|
+
case ENV['RAILS_ENV']
|
185
|
+
when 'production'
|
186
|
+
ProductionDB.connection_string
|
187
|
+
when 'test'
|
188
|
+
TestDB.connection_string
|
189
|
+
else
|
190
|
+
"sqlite3://development.db"
|
191
|
+
end
|
192
|
+
}
|
193
|
+
|
194
|
+
define :redis_config, -> {
|
195
|
+
if FeatureFlag.enabled?(:redis_cache)
|
196
|
+
RedisService.configuration
|
197
|
+
else
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# Static configuration using Exports API
|
204
|
+
class AppConfig < Taski::Task
|
205
|
+
exports :app_name, :version, :port
|
35
206
|
|
36
207
|
def build
|
37
|
-
|
208
|
+
@app_name = "MyWebApp"
|
209
|
+
@version = "2.1.0"
|
210
|
+
@port = ENV.fetch('PORT', 3000).to_i
|
38
211
|
end
|
39
212
|
end
|
40
213
|
|
41
|
-
|
42
|
-
|
43
|
-
|
214
|
+
# Application startup combining both APIs
|
215
|
+
class Application < Taski::Task
|
216
|
+
def build
|
217
|
+
puts "Starting #{AppConfig.app_name} v#{AppConfig.version}"
|
218
|
+
puts "Database: #{Environment.database_url}"
|
219
|
+
puts "Redis: #{Environment.redis_config || 'disabled'}"
|
220
|
+
puts "Port: #{AppConfig.port}"
|
221
|
+
|
222
|
+
# Start the application...
|
223
|
+
end
|
224
|
+
|
225
|
+
def clean
|
226
|
+
puts "Shutting down #{AppConfig.app_name}..."
|
227
|
+
# Cleanup logic...
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Everything runs in the correct order automatically
|
232
|
+
Application.build
|
44
233
|
```
|
45
234
|
|
46
|
-
## Installation
|
235
|
+
## 📦 Installation
|
47
236
|
|
48
|
-
|
237
|
+
> **⚠️ Warning:** Taski is currently in development. API changes may occur. Use at your own risk in production environments.
|
238
|
+
|
239
|
+
Add this line to your application's Gemfile:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
gem 'taski'
|
243
|
+
```
|
244
|
+
|
245
|
+
And then execute:
|
49
246
|
|
50
247
|
```bash
|
51
|
-
bundle
|
248
|
+
bundle install
|
52
249
|
```
|
53
250
|
|
54
|
-
|
251
|
+
Or install it yourself as:
|
55
252
|
|
56
253
|
```bash
|
57
254
|
gem install taski
|
58
255
|
```
|
59
256
|
|
60
|
-
|
257
|
+
For development and testing purposes, you can also install directly from the repository:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
# In your Gemfile
|
261
|
+
gem 'taski', git: 'https://github.com/[USERNAME]/taski.git'
|
262
|
+
```
|
263
|
+
|
264
|
+
## 🧪 Testing
|
265
|
+
|
266
|
+
Taski includes comprehensive test coverage. Run the test suite:
|
267
|
+
|
268
|
+
```bash
|
269
|
+
bundle exec rake test
|
270
|
+
```
|
271
|
+
|
272
|
+
> **ℹ️ Note:** Test output may include warnings about method redefinition from the `define` API. These warnings are expected and can be safely ignored.
|
273
|
+
|
274
|
+
|
275
|
+
## 🏛️ Architecture
|
276
|
+
|
277
|
+
Taski is built with a modular architecture:
|
278
|
+
|
279
|
+
- **Task Base**: Core framework and constants
|
280
|
+
- **Exports API**: Static dependency resolution
|
281
|
+
- **Define API**: Dynamic dependency resolution
|
282
|
+
- **Instance Management**: Thread-safe lifecycle management
|
283
|
+
- **Dependency Resolver**: Topological sorting and analysis
|
284
|
+
- **Static Analyzer**: AST-based dependency detection
|
285
|
+
|
286
|
+
## 🚧 Development Status
|
61
287
|
|
62
|
-
|
288
|
+
**Taski is currently in active development and should be considered experimental.** While the core functionality is working and well-tested, the API may undergo changes as we refine the framework based on feedback and real-world usage.
|
63
289
|
|
64
|
-
|
290
|
+
### Current Development Phase
|
291
|
+
|
292
|
+
- ✅ **Core Framework**: Dependency resolution, both APIs, thread safety
|
293
|
+
- ✅ **Testing**: Comprehensive test suite with 38+ tests
|
294
|
+
- ✅ **Type Safety**: RBS definitions and Steep integration
|
295
|
+
- 🚧 **API Stability**: Some breaking changes may occur
|
296
|
+
- 🚧 **Performance**: Optimizations for large dependency graphs
|
297
|
+
- 🚧 **Documentation**: Examples and best practices
|
298
|
+
|
299
|
+
### Known Limitations
|
300
|
+
|
301
|
+
- **API Changes**: Breaking changes may occur in minor version updates
|
302
|
+
- **Production Readiness**: Not yet recommended for production environments
|
303
|
+
- **Static Analysis**: Works best with straightforward Ruby code patterns
|
304
|
+
- **Metaprogramming**: Complex metaprogramming may require manual dependency specification
|
305
|
+
- **Performance**: Not yet optimized for very large dependency graphs (1000+ tasks)
|
306
|
+
- **Method Redefinition Warnings**: Using `define` API may generate Ruby warnings about method redefinition (this is expected behavior)
|
307
|
+
|
308
|
+
### Future Development
|
309
|
+
|
310
|
+
The future direction of Taski will be determined based on community feedback, real-world usage, and identified needs. Development priorities may include areas such as performance optimization, enhanced static analysis, and improved documentation, but specific roadmap items have not yet been finalized.
|
311
|
+
|
312
|
+
### Contributing to Development
|
313
|
+
|
314
|
+
We welcome contributions during this development phase! Areas where help is especially appreciated:
|
315
|
+
|
316
|
+
- **Real-world Testing**: Try Taski in your projects and report issues
|
317
|
+
- **Performance Testing**: Test with large dependency graphs
|
318
|
+
- **API Feedback**: Suggest improvements to the developer experience
|
319
|
+
- **Documentation**: Help improve examples and guides
|
65
320
|
|
66
321
|
## Contributing
|
67
322
|
|
68
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
323
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ahogappa/taski.
|
324
|
+
|
325
|
+
## License
|
326
|
+
|
327
|
+
The gem is available as open source under the [MIT License](LICENSE).
|
328
|
+
|
329
|
+
---
|
330
|
+
|
331
|
+
**Taski** - Build complex dependency graphs with simple, elegant Ruby code. 🚀
|
332
|
+
|
333
|
+
> **Experimental Software**: Please use responsibly and provide feedback to help us reach v1.0!
|
data/Steepfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Steepfile
|
2
|
+
|
3
|
+
D = Steep::Diagnostic
|
4
|
+
|
5
|
+
target :lib do
|
6
|
+
signature "sig"
|
7
|
+
|
8
|
+
check "lib"
|
9
|
+
|
10
|
+
library "monitor", "prism"
|
11
|
+
|
12
|
+
# Configure diagnostics with lenient settings for metaprogramming-heavy code
|
13
|
+
configure_code_diagnostics do |hash|
|
14
|
+
hash[D::Ruby::UnannotatedEmptyCollection] = :information
|
15
|
+
hash[D::Ruby::UnknownInstanceVariable] = :information
|
16
|
+
hash[D::Ruby::FallbackAny] = :information
|
17
|
+
hash[D::Ruby::NoMethod] = :warning
|
18
|
+
hash[D::Ruby::UndeclaredMethodDefinition] = :information
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Complex example from README showing both APIs
|
3
|
+
|
4
|
+
require_relative '../lib/taski'
|
5
|
+
|
6
|
+
# Mock classes for the example
|
7
|
+
class ProductionDB < Taski::Task
|
8
|
+
exports :connection_string
|
9
|
+
def build
|
10
|
+
@connection_string = "postgres://prod-server/app"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class TestDB < Taski::Task
|
15
|
+
exports :connection_string
|
16
|
+
def build
|
17
|
+
@connection_string = "postgres://test-server/app_test"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module FeatureFlag
|
22
|
+
def self.enabled?(flag)
|
23
|
+
ENV["FEATURE_#{flag.to_s.upcase}"] == 'true'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class RedisService < Taski::Task
|
28
|
+
exports :configuration
|
29
|
+
def build
|
30
|
+
@configuration = "redis://localhost:6379"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Environment configuration using Define API
|
35
|
+
class Environment < Taski::Task
|
36
|
+
define :database_url, -> {
|
37
|
+
case ENV['RAILS_ENV']
|
38
|
+
when 'production'
|
39
|
+
ProductionDB.connection_string
|
40
|
+
when 'test'
|
41
|
+
TestDB.connection_string
|
42
|
+
else
|
43
|
+
"sqlite3://development.db"
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
define :redis_config, -> {
|
48
|
+
if FeatureFlag.enabled?(:redis_cache)
|
49
|
+
RedisService.configuration
|
50
|
+
else
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
}
|
54
|
+
|
55
|
+
def build
|
56
|
+
# Environment configuration is handled by define blocks
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Static configuration using Exports API
|
61
|
+
class AppConfig < Taski::Task
|
62
|
+
exports :app_name, :version, :port
|
63
|
+
|
64
|
+
def build
|
65
|
+
@app_name = "MyWebApp"
|
66
|
+
@version = "2.1.0"
|
67
|
+
@port = ENV.fetch('PORT', 3000).to_i
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Application startup combining both APIs
|
72
|
+
class Application < Taski::Task
|
73
|
+
def build
|
74
|
+
puts "Starting #{AppConfig.app_name} v#{AppConfig.version}"
|
75
|
+
puts "Database: #{Environment.database_url}"
|
76
|
+
puts "Redis: #{Environment.redis_config || 'disabled'}"
|
77
|
+
puts "Port: #{AppConfig.port}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def clean
|
81
|
+
puts "Shutting down #{AppConfig.app_name}..."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Test different environments
|
86
|
+
puts "=== Complex Example ==="
|
87
|
+
|
88
|
+
puts "\n1. Development Environment (default):"
|
89
|
+
ENV.delete('RAILS_ENV')
|
90
|
+
ENV.delete('FEATURE_REDIS_CACHE')
|
91
|
+
Application.build
|
92
|
+
Application.reset!
|
93
|
+
|
94
|
+
puts "\n2. Test Environment:"
|
95
|
+
ENV['RAILS_ENV'] = 'test'
|
96
|
+
# Reset Environment to re-evaluate define blocks
|
97
|
+
Environment.reset!
|
98
|
+
Application.build
|
99
|
+
Application.reset!
|
100
|
+
|
101
|
+
puts "\n3. Production with Redis:"
|
102
|
+
ENV['RAILS_ENV'] = 'production'
|
103
|
+
ENV['FEATURE_REDIS_CACHE'] = 'true'
|
104
|
+
# Reset Environment to re-evaluate define blocks
|
105
|
+
Environment.reset!
|
106
|
+
Application.build
|
107
|
+
|
108
|
+
puts "\n4. Cleanup:"
|
109
|
+
Application.clean
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Quick Start example from README
|
3
|
+
|
4
|
+
require_relative '../lib/taski'
|
5
|
+
|
6
|
+
# Simple static dependency using Exports API
|
7
|
+
class DatabaseSetup < Taski::Task
|
8
|
+
exports :connection_string
|
9
|
+
|
10
|
+
def build
|
11
|
+
@connection_string = "postgresql://localhost/myapp"
|
12
|
+
puts "Database configured"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class APIServer < Taski::Task
|
17
|
+
exports :port
|
18
|
+
|
19
|
+
def build
|
20
|
+
# Automatic dependency: DatabaseSetup will be built first
|
21
|
+
puts "Starting API with #{DatabaseSetup.connection_string}"
|
22
|
+
@port = 3000
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Execute - dependencies are resolved automatically
|
27
|
+
puts "=== Quick Start Example ==="
|
28
|
+
APIServer.build
|
29
|
+
|
30
|
+
puts "\nResult: APIServer running on port #{APIServer.port}"
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
module DependencyAnalyzer
|
7
|
+
class << self
|
8
|
+
def analyze_method(klass, method_name)
|
9
|
+
return [] unless klass.instance_methods(false).include?(method_name)
|
10
|
+
|
11
|
+
method = klass.instance_method(method_name)
|
12
|
+
source_location = method.source_location
|
13
|
+
return [] unless source_location
|
14
|
+
|
15
|
+
file_path, line_number = source_location
|
16
|
+
return [] unless File.exist?(file_path)
|
17
|
+
|
18
|
+
begin
|
19
|
+
result = Prism.parse_file(file_path)
|
20
|
+
|
21
|
+
unless result.success?
|
22
|
+
warn "Taski: Parse errors in #{file_path}: #{result.errors.map(&:message).join(', ')}"
|
23
|
+
return []
|
24
|
+
end
|
25
|
+
|
26
|
+
# Handle warnings if present
|
27
|
+
if result.warnings.any?
|
28
|
+
warn "Taski: Parse warnings in #{file_path}: #{result.warnings.map(&:message).join(', ')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
dependencies = []
|
32
|
+
method_node = find_method_node(result.value, method_name, line_number)
|
33
|
+
|
34
|
+
if method_node
|
35
|
+
visitor = TaskDependencyVisitor.new
|
36
|
+
visitor.visit(method_node)
|
37
|
+
dependencies = visitor.dependencies
|
38
|
+
end
|
39
|
+
|
40
|
+
dependencies.uniq
|
41
|
+
rescue IOError, SystemCallError => e
|
42
|
+
warn "Taski: Failed to read file #{file_path}: #{e.message}"
|
43
|
+
[]
|
44
|
+
rescue => e
|
45
|
+
warn "Taski: Failed to analyze method #{klass}##{method_name}: #{e.message}"
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def find_method_node(node, method_name, target_line)
|
53
|
+
return nil unless node
|
54
|
+
|
55
|
+
case node
|
56
|
+
when Prism::DefNode
|
57
|
+
if node.name == method_name && node.location.start_line <= target_line && node.location.end_line >= target_line
|
58
|
+
return node
|
59
|
+
end
|
60
|
+
when Prism::ClassNode, Prism::ModuleNode
|
61
|
+
if node.respond_to?(:body)
|
62
|
+
return find_method_node(node.body, method_name, target_line)
|
63
|
+
end
|
64
|
+
when Prism::StatementsNode
|
65
|
+
node.body.each do |child|
|
66
|
+
result = find_method_node(child, method_name, target_line)
|
67
|
+
return result if result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Recursively search child nodes
|
72
|
+
if node.respond_to?(:child_nodes)
|
73
|
+
node.child_nodes.each do |child|
|
74
|
+
result = find_method_node(child, method_name, target_line)
|
75
|
+
return result if result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Task dependency visitor using Prism's visitor pattern
|
83
|
+
class TaskDependencyVisitor < Prism::Visitor
|
84
|
+
attr_reader :dependencies
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
@dependencies = []
|
88
|
+
@constant_cache = {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit_constant_read_node(node)
|
92
|
+
check_task_constant(node.name.to_s)
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
def visit_constant_path_node(node)
|
97
|
+
const_path = extract_constant_path(node)
|
98
|
+
check_task_constant(const_path) if const_path
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
def visit_call_node(node)
|
103
|
+
# Check for method calls on constants (e.g., TaskA.result)
|
104
|
+
case node.receiver
|
105
|
+
when Prism::ConstantReadNode
|
106
|
+
check_task_constant(node.receiver.name.to_s)
|
107
|
+
when Prism::ConstantPathNode
|
108
|
+
const_path = extract_constant_path(node.receiver)
|
109
|
+
check_task_constant(const_path) if const_path
|
110
|
+
end
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def check_task_constant(const_name)
|
117
|
+
return unless const_name
|
118
|
+
|
119
|
+
# Use caching to avoid repeated constant resolution
|
120
|
+
cached_result = @constant_cache[const_name]
|
121
|
+
return cached_result if cached_result == false # Cached negative result
|
122
|
+
return @dependencies << cached_result if cached_result # Cached positive result
|
123
|
+
|
124
|
+
begin
|
125
|
+
if Object.const_defined?(const_name)
|
126
|
+
klass = Object.const_get(const_name)
|
127
|
+
if klass.is_a?(Class) && klass < Taski::Task
|
128
|
+
@constant_cache[const_name] = klass
|
129
|
+
@dependencies << klass
|
130
|
+
else
|
131
|
+
@constant_cache[const_name] = false
|
132
|
+
end
|
133
|
+
else
|
134
|
+
@constant_cache[const_name] = false
|
135
|
+
end
|
136
|
+
rescue NameError, ArgumentError
|
137
|
+
@constant_cache[const_name] = false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def extract_constant_path(node)
|
142
|
+
case node
|
143
|
+
when Prism::ConstantReadNode
|
144
|
+
node.name.to_s
|
145
|
+
when Prism::ConstantPathNode
|
146
|
+
parent_path = extract_constant_path(node.parent) if node.parent
|
147
|
+
child_name = node.name.to_s
|
148
|
+
|
149
|
+
if parent_path && child_name
|
150
|
+
"#{parent_path}::#{child_name}"
|
151
|
+
else
|
152
|
+
child_name
|
153
|
+
end
|
154
|
+
else
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|