homura-runtime 0.3.6 → 0.3.8

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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
3
+ require "set"
4
4
 
5
5
  module HomuraRuntime
6
6
  class AsyncRegistry
@@ -22,11 +22,15 @@ module HomuraRuntime
22
22
  end
23
23
 
24
24
  def taint_return(class_name, method_name, return_class_name)
25
- (@registry.taint_returns[class_name] ||= {})[method_name] = return_class_name
25
+ (@registry.taint_returns[class_name] ||= {})[
26
+ method_name
27
+ ] = return_class_name
26
28
  end
27
29
 
28
30
  def async_accessor(lvar_name, accessor_name, class_name)
29
- @registry.async_accessors[[lvar_name.to_sym, accessor_name.to_sym]] = class_name
31
+ @registry.async_accessors[
32
+ [lvar_name.to_sym, accessor_name.to_sym]
33
+ ] = class_name
30
34
  end
31
35
 
32
36
  def async_helper(method_name, class_name)
@@ -75,28 +79,46 @@ module HomuraRuntime
75
79
  Gem.loaded_specs.each_value do |spec|
76
80
  next if spec.full_gem_path.nil?
77
81
 
78
- lib_dir = File.join(spec.full_gem_path, 'lib')
82
+ lib_dir = File.join(spec.full_gem_path, "lib")
79
83
  next unless Dir.exist?(lib_dir)
80
84
 
81
- Dir.glob(File.join(lib_dir, '**', '*.rb')).each do |path|
82
- next unless File.read(path).include?('register_async_source')
83
-
84
- require_path = path.sub(Regexp.new("^#{Regexp.escape(lib_dir)}/"), '').sub(/\.rb\z/, '')
85
- begin
86
- require require_path
87
- loaded += 1
88
- puts "[auto-await] loaded async source from #{spec.name}: #{require_path}" if debug
89
- rescue LoadError, StandardError => e
90
- warn "[auto-await] Warning: failed to load async source from #{spec.name}/#{require_path}: #{e.message}" if debug
85
+ Dir
86
+ .glob(File.join(lib_dir, "**", "*.rb"))
87
+ .each do |path|
88
+ next unless File.read(path).include?("register_async_source")
89
+
90
+ require_path =
91
+ path.sub(Regexp.new("^#{Regexp.escape(lib_dir)}/"), "").sub(
92
+ /\.rb\z/,
93
+ ""
94
+ )
95
+ begin
96
+ require require_path
97
+ loaded += 1
98
+ if debug
99
+ puts "[auto-await] loaded async source from #{spec.name}: #{require_path}"
100
+ end
101
+ rescue LoadError, StandardError => e
102
+ if debug
103
+ warn "[auto-await] Warning: failed to load async source from #{spec.name}/#{require_path}: #{e.message}"
104
+ end
105
+ end
91
106
  end
92
- end
93
107
  end
94
108
 
95
- puts "[auto-await] auto-loaded #{loaded} async source file(s)" if debug && loaded.positive?
109
+ if debug && loaded.positive?
110
+ puts "[auto-await] auto-loaded #{loaded} async source file(s)"
111
+ end
96
112
  end
97
113
  end
98
114
 
99
- attr_reader :async_classes, :async_methods, :async_factories, :taint_returns, :async_accessors, :async_helpers, :helper_factories
115
+ attr_reader :async_classes,
116
+ :async_methods,
117
+ :async_factories,
118
+ :taint_returns,
119
+ :async_accessors,
120
+ :async_helpers,
121
+ :helper_factories
100
122
 
101
123
  def initialize
102
124
  @async_classes = {}
@@ -113,7 +135,10 @@ module HomuraRuntime
113
135
  methods = @async_methods[class_name]
114
136
  return true if methods&.include?(method_name)
115
137
  except = @async_classes[class_name]
116
- return true if except && !except.include?(method_name.to_s) && !except.include?(method_name.to_sym)
138
+ if except && !except.include?(method_name.to_s) &&
139
+ !except.include?(method_name.to_sym)
140
+ return true
141
+ end
117
142
  false
118
143
  end
119
144
 
@@ -126,10 +151,8 @@ module HomuraRuntime
126
151
  end
127
152
 
128
153
  def tainted_class?(class_name)
129
- @async_classes.key?(class_name) ||
130
- @async_methods.key?(class_name) ||
131
- @async_factories.key?(class_name) ||
132
- @taint_returns.key?(class_name)
154
+ @async_classes.key?(class_name) || @async_methods.key?(class_name) ||
155
+ @async_factories.key?(class_name) || @taint_returns.key?(class_name)
133
156
  end
134
157
  end
135
158
  end
@@ -138,86 +161,100 @@ end
138
161
  # Each binding declares which methods return Promises so the
139
162
  # build-time analyzer can insert .__await__ automatically.
140
163
  HomuraRuntime::AsyncRegistry.register_async_source do
141
- async_method 'Cloudflare::D1Database', :execute
142
- async_method 'Cloudflare::D1Database', :get_first_row
143
- async_method 'Cloudflare::D1Database', :execute_insert
144
- async_method 'Cloudflare::D1Database', :execute_batch
145
- taint_return 'Cloudflare::D1Database', :prepare, 'Cloudflare::D1Statement'
146
- taint_return 'Cloudflare::D1Database', :[], 'Cloudflare::D1Statement'
147
-
148
- async_method 'Cloudflare::D1Statement', :all
149
- async_method 'Cloudflare::D1Statement', :first
150
- async_method 'Cloudflare::D1Statement', :run
164
+ async_method "Cloudflare::D1Database", :execute
165
+ async_method "Cloudflare::D1Database", :get_first_row
166
+ async_method "Cloudflare::D1Database", :execute_insert
167
+ async_method "Cloudflare::D1Database", :execute_batch
168
+ taint_return "Cloudflare::D1Database", :prepare, "Cloudflare::D1Statement"
169
+ taint_return "Cloudflare::D1Database", :[], "Cloudflare::D1Statement"
170
+
171
+ async_method "Cloudflare::D1Statement", :all
172
+ async_method "Cloudflare::D1Statement", :first
173
+ async_method "Cloudflare::D1Statement", :run
151
174
  # `bind` returns a new D1Statement for further chaining. Tainting the
152
175
  # return preserves the type so the auto-await pass keeps chaining
153
176
  # `.run` / `.all` / `.first` on the bound statement (otherwise
154
177
  # `db.prepare(sql).bind(...).run` drops await on `.run` and
155
178
  # `flatten_meta` ends up receiving a JS Promise instead of the
156
179
  # metadata Hash).
157
- taint_return 'Cloudflare::D1Statement', :bind, 'Cloudflare::D1Statement'
158
-
159
- async_method 'Cloudflare::KVNamespace', :get
160
- async_method 'Cloudflare::KVNamespace', :get_with_metadata
161
- async_method 'Cloudflare::KVNamespace', :put
162
- async_method 'Cloudflare::KVNamespace', :delete
163
- async_method 'Cloudflare::KVNamespace', :list
164
-
165
- async_method 'Cloudflare::R2Bucket', :get
166
- async_method 'Cloudflare::R2Bucket', :get_binary
167
- async_method 'Cloudflare::R2Bucket', :put
168
- async_method 'Cloudflare::R2Bucket', :delete
169
- async_method 'Cloudflare::R2Bucket', :list
170
- async_method 'Cloudflare::R2Bucket', :head
171
-
172
- async_method 'Cloudflare::AI', :run
173
- taint_return 'Cloudflare::AI', :run_stream, 'Cloudflare::AI::Stream'
174
- async_method 'Cloudflare::AI::Binding', :run
175
- taint_return 'Cloudflare::AI::Binding', :run_stream, 'Cloudflare::AI::Stream'
176
-
177
- async_method 'Cloudflare::Cache', :match
178
- async_method 'Cloudflare::Cache', :put
179
- async_method 'Cloudflare::Cache', :delete
180
-
181
- async_factory 'Cloudflare::Email', :new
182
- async_method 'Cloudflare::Email', :send
183
-
184
- async_method 'Cloudflare::Queue', :send
185
- async_method 'Cloudflare::Queue', :send_batch
186
-
187
- async_factory 'Cloudflare::DurableObjectNamespace', :new
188
- taint_return 'Cloudflare::DurableObjectNamespace', :get, 'Cloudflare::DurableObjectStub'
189
- taint_return 'Cloudflare::DurableObjectNamespace', :get_by_name, 'Cloudflare::DurableObjectStub'
190
- taint_return 'Cloudflare::DurableObjectState', :storage, 'Cloudflare::DurableObjectStorage'
191
- async_method 'Cloudflare::DurableObjectStub', :fetch
192
- async_method 'Cloudflare::DurableObjectStub', :request
193
- async_method 'Cloudflare::DurableObjectStub', :get
194
- async_method 'Cloudflare::DurableObjectStub', :post
195
- async_method 'Cloudflare::DurableObjectStub', :put
196
- async_method 'Cloudflare::DurableObjectStub', :delete
197
-
198
- async_method 'Cloudflare::DurableObjectStorage', :get
199
- async_method 'Cloudflare::DurableObjectStorage', :put
200
- async_method 'Cloudflare::DurableObjectStorage', :delete
201
- async_method 'Cloudflare::DurableObjectStorage', :list
202
- async_method 'Cloudflare::DurableObjectStorage', :transaction
203
-
204
- async_method 'Cloudflare::HTTP', :fetch
205
-
206
- async_method 'Faraday::Connection', :get
207
- async_method 'Faraday::Connection', :post
208
- async_method 'Faraday::Connection', :put
209
- async_method 'Faraday::Connection', :delete
210
- async_method 'Faraday::Connection', :patch
211
- async_method 'Faraday::Connection', :head
212
-
213
- helper_factory :d1, 'Cloudflare::D1Database'
214
- helper_factory :db, 'Cloudflare::D1Database'
215
- helper_factory :kv, 'Cloudflare::KVNamespace'
216
- helper_factory :bucket, 'Cloudflare::R2Bucket'
217
- helper_factory :ai, 'Cloudflare::AI::Binding'
218
- helper_factory :send_email, 'Cloudflare::Email'
219
- helper_factory :jobs_queue, 'Cloudflare::Queue'
220
- helper_factory :jobs_dlq, 'Cloudflare::Queue'
221
- helper_factory :do_counter, 'Cloudflare::DurableObjectNamespace'
222
- helper_factory :durable_object, 'Cloudflare::DurableObjectStub'
180
+ taint_return "Cloudflare::D1Statement", :bind, "Cloudflare::D1Statement"
181
+
182
+ async_method "Cloudflare::KVNamespace", :get
183
+ async_method "Cloudflare::KVNamespace", :get_with_metadata
184
+ async_method "Cloudflare::KVNamespace", :put
185
+ async_method "Cloudflare::KVNamespace", :delete
186
+ async_method "Cloudflare::KVNamespace", :list
187
+
188
+ async_method "Cloudflare::R2Bucket", :get
189
+ async_method "Cloudflare::R2Bucket", :get_binary
190
+ async_method "Cloudflare::R2Bucket", :put
191
+ async_method "Cloudflare::R2Bucket", :delete
192
+ async_method "Cloudflare::R2Bucket", :list
193
+ async_method "Cloudflare::R2Bucket", :head
194
+
195
+ async_method "Cloudflare::AI", :run
196
+ async_method "Cloudflare::AI", :speak
197
+ async_method "Cloudflare::AI", :speak_data_url
198
+ taint_return "Cloudflare::AI", :run_stream, "Cloudflare::AI::Stream"
199
+ async_method "Cloudflare::AI::Binding", :run
200
+ async_method "Cloudflare::AI::Binding", :chat
201
+ async_method "Cloudflare::AI::Binding", :chat_text
202
+ async_method "Cloudflare::AI::Binding", :transcribe
203
+ async_method "Cloudflare::AI::Binding", :transcribe_text
204
+ async_method "Cloudflare::AI::Binding", :speak
205
+ async_method "Cloudflare::AI::Binding", :speak_data_url
206
+ taint_return "Cloudflare::AI::Binding", :run_stream, "Cloudflare::AI::Stream"
207
+
208
+ async_method "Cloudflare::Cache", :match
209
+ async_method "Cloudflare::Cache", :put
210
+ async_method "Cloudflare::Cache", :delete
211
+
212
+ async_factory "Cloudflare::Email", :new
213
+ async_method "Cloudflare::Email", :send
214
+
215
+ async_method "Cloudflare::Queue", :send
216
+ async_method "Cloudflare::Queue", :send_batch
217
+
218
+ async_factory "Cloudflare::DurableObjectNamespace", :new
219
+ taint_return "Cloudflare::DurableObjectNamespace",
220
+ :get,
221
+ "Cloudflare::DurableObjectStub"
222
+ taint_return "Cloudflare::DurableObjectNamespace",
223
+ :get_by_name,
224
+ "Cloudflare::DurableObjectStub"
225
+ taint_return "Cloudflare::DurableObjectState",
226
+ :storage,
227
+ "Cloudflare::DurableObjectStorage"
228
+ async_method "Cloudflare::DurableObjectStub", :fetch
229
+ async_method "Cloudflare::DurableObjectStub", :request
230
+ async_method "Cloudflare::DurableObjectStub", :get
231
+ async_method "Cloudflare::DurableObjectStub", :post
232
+ async_method "Cloudflare::DurableObjectStub", :put
233
+ async_method "Cloudflare::DurableObjectStub", :delete
234
+
235
+ async_method "Cloudflare::DurableObjectStorage", :get
236
+ async_method "Cloudflare::DurableObjectStorage", :put
237
+ async_method "Cloudflare::DurableObjectStorage", :delete
238
+ async_method "Cloudflare::DurableObjectStorage", :list
239
+ async_method "Cloudflare::DurableObjectStorage", :transaction
240
+
241
+ async_method "Cloudflare::HTTP", :fetch
242
+
243
+ async_method "Faraday::Connection", :get
244
+ async_method "Faraday::Connection", :post
245
+ async_method "Faraday::Connection", :put
246
+ async_method "Faraday::Connection", :delete
247
+ async_method "Faraday::Connection", :patch
248
+ async_method "Faraday::Connection", :head
249
+
250
+ helper_factory :d1, "Cloudflare::D1Database"
251
+ helper_factory :db, "Cloudflare::D1Database"
252
+ helper_factory :kv, "Cloudflare::KVNamespace"
253
+ helper_factory :bucket, "Cloudflare::R2Bucket"
254
+ helper_factory :ai, "Cloudflare::AI::Binding"
255
+ helper_factory :send_email, "Cloudflare::Email"
256
+ helper_factory :jobs_queue, "Cloudflare::Queue"
257
+ helper_factory :jobs_dlq, "Cloudflare::Queue"
258
+ helper_factory :do_counter, "Cloudflare::DurableObjectNamespace"
259
+ helper_factory :durable_object, "Cloudflare::DurableObjectStub"
223
260
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'parser/current'
4
- require 'parser/source/tree_rewriter'
5
- require 'set'
3
+ require "parser/current"
4
+ require "parser/source/tree_rewriter"
5
+ require "set"
6
6
 
7
7
  module HomuraRuntime
8
8
  module AutoAwait
@@ -16,7 +16,7 @@ module HomuraRuntime
16
16
  @async_local_methods = Set.new
17
17
  end
18
18
 
19
- def process(source, filename = '(auto-await)')
19
+ def process(source, filename = "(auto-await)")
20
20
  buffer = Parser::Source::Buffer.new(filename)
21
21
  buffer.source = source
22
22
  parser = Parser::CurrentRuby.new
@@ -52,7 +52,9 @@ module HomuraRuntime
52
52
  # Bottom-up traversal: process children first so helper-factory
53
53
  # sends (e.g. bare +db+) populate @env before their parent
54
54
  # send (e.g. +db.execute(...)+) is checked by should_await?.
55
- node.children.each { |child| process_node(child) if child.is_a?(Parser::AST::Node) }
55
+ node.children.each do |child|
56
+ process_node(child) if child.is_a?(Parser::AST::Node)
57
+ end
56
58
  case node.type
57
59
  when :lvasgn
58
60
  process_lvasgn(node)
@@ -85,13 +87,16 @@ module HomuraRuntime
85
87
  before_awaits = @await_nodes.length
86
88
  @env = @registry.helper_factories.dup
87
89
 
88
- node.children[1..-1].each { |child| process_node(child) if child.is_a?(Parser::AST::Node) }
90
+ node.children[1..-1].each do |child|
91
+ process_node(child) if child.is_a?(Parser::AST::Node)
92
+ end
89
93
 
90
94
  body = node.children[2]
91
95
  return_cls = infer_class(body)
92
96
  @method_returns[method_name] = return_cls if return_cls
93
97
  body_source = node.loc.expression&.source.to_s
94
- if @await_nodes.length > before_awaits || body_source.include?('.__await__')
98
+ if @await_nodes.length > before_awaits ||
99
+ body_source.include?(".__await__")
95
100
  @async_local_methods << method_name
96
101
  end
97
102
 
@@ -115,7 +120,8 @@ module HomuraRuntime
115
120
 
116
121
  def process_send(node)
117
122
  receiver, method_name = *node
118
- if receiver.nil? && (factory_cls = @registry.helper_factories[method_name])
123
+ if receiver.nil? &&
124
+ (factory_cls = @registry.helper_factories[method_name])
119
125
  @env[method_name] = factory_cls
120
126
  end
121
127
  if should_await?(node)
@@ -144,7 +150,9 @@ module HomuraRuntime
144
150
  when :send
145
151
  receiver, method_name = *node
146
152
  if receiver.nil?
147
- return @method_returns[method_name] if @method_returns.key?(method_name)
153
+ if @method_returns.key?(method_name)
154
+ return @method_returns[method_name]
155
+ end
148
156
  return @env[method_name] if @env.key?(method_name)
149
157
  end
150
158
  infer_send_class(node)
@@ -175,7 +183,8 @@ module HomuraRuntime
175
183
  key_node = node.children[2]
176
184
  if key_node&.type == :str
177
185
  key = key_node.children[0]
178
- mapped = @registry.async_accessors[[env_name(receiver), key.to_sym]]
186
+ mapped =
187
+ @registry.async_accessors[[env_name(receiver), key.to_sym]]
179
188
  return mapped if mapped
180
189
  end
181
190
  end
@@ -189,7 +198,9 @@ module HomuraRuntime
189
198
  return ret if ret
190
199
  end
191
200
  else
192
- return @method_returns[method_name] if @method_returns.key?(method_name)
201
+ if @method_returns.key?(method_name)
202
+ return @method_returns[method_name]
203
+ end
193
204
  end
194
205
  nil
195
206
  end
@@ -198,14 +209,17 @@ module HomuraRuntime
198
209
  return {} unless durable_object_define_call?(call_node)
199
210
  return {} unless args_node&.type == :args
200
211
 
201
- arg_names = args_node.children.filter_map do |arg|
202
- next unless arg&.type == :arg
203
- arg.children[0]
204
- end
212
+ arg_names =
213
+ args_node.children.filter_map do |arg|
214
+ next unless arg&.type == :arg
215
+ arg.children[0]
216
+ end
205
217
  return {} if arg_names.empty?
206
218
 
207
- bindings = { arg_names[0] => 'Cloudflare::DurableObjectState' }
208
- bindings[arg_names[1]] = 'Cloudflare::DurableObjectRequest' if arg_names.length > 1
219
+ bindings = { arg_names[0] => "Cloudflare::DurableObjectState" }
220
+ bindings[
221
+ arg_names[1]
222
+ ] = "Cloudflare::DurableObjectRequest" if arg_names.length > 1
209
223
  bindings
210
224
  end
211
225
 
@@ -213,7 +227,8 @@ module HomuraRuntime
213
227
  return false unless call_node&.type == :send
214
228
 
215
229
  receiver, method_name = *call_node
216
- method_name == :define && const_path(receiver) == 'Cloudflare::DurableObject'
230
+ method_name == :define &&
231
+ const_path(receiver) == "Cloudflare::DurableObject"
217
232
  end
218
233
 
219
234
  def infer_index_class(node)
@@ -273,7 +288,7 @@ module HomuraRuntime
273
288
  parts.unshift(n.children[1])
274
289
  n = n.children[0]
275
290
  end
276
- parts.join('::')
291
+ parts.join("::")
277
292
  end
278
293
 
279
294
  def debug(msg)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'parser/source/tree_rewriter'
3
+ require "parser/source/tree_rewriter"
4
4
 
5
5
  module HomuraRuntime
6
6
  module AutoAwait