autotype 0.1.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 +7 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/README.md +93 -0
- data/Rakefile +21 -0
- data/autotype.example.yml +19 -0
- data/autotype.gemspec +38 -0
- data/exe/autotype +6 -0
- data/ext/autotype/Makefile +273 -0
- data/ext/autotype/autotype.bundle.dSYM/Contents/Info.plist +20 -0
- data/ext/autotype/autotype.bundle.dSYM/Contents/Resources/Relocations/aarch64/autotype.bundle.yml +5 -0
- data/ext/autotype/autotype.c +277 -0
- data/ext/autotype/extconf.rb +15 -0
- data/lib/autotype/discovery_profile.rb +527 -0
- data/lib/autotype/engine.rb +4613 -0
- data/lib/autotype/native.rb +34 -0
- data/lib/autotype/native_bridge.rb +121 -0
- data/lib/autotype/profile.rb +46 -0
- data/lib/autotype/type_string.rb +48 -0
- data/lib/autotype/version.rb +5 -0
- data/lib/autotype.rb +15 -0
- data/native/autotype/Makefile +23 -0
- data/native/autotype/include/stc.h +107 -0
- data/native/autotype/src/main.c +50 -0
- data/native/autotype/src/solver.c +296 -0
- data/native/autotype/src/type.c +223 -0
- data/spec/autotype_config_spec.rb +91 -0
- data/spec/autotype_native_spec.rb +71 -0
- data/spec/autotype_spec.rb +1028 -0
- data/spec/examples.txt +72 -0
- data/spec/fixtures/autotype.yml +9 -0
- data/spec/fixtures/entities/tool_call.rb +7 -0
- data/spec/fixtures/entities/tool_result.rb +7 -0
- data/spec/fixtures/pipeline/actors/demo_actor.rb +8 -0
- data/spec/spec_helper.rb +25 -0
- metadata +149 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Autotype
|
|
6
|
+
# Infers framework wiring, entity layout, and type hints from Ruby source and
|
|
7
|
+
# the filesystem. Optional autotype.yml supplies skip globs and overrides when
|
|
8
|
+
# heuristics are wrong — nothing else is required.
|
|
9
|
+
class DiscoveryProfile
|
|
10
|
+
PORT_CALLS = %i[input output].freeze
|
|
11
|
+
OPTION_CALLS = %i[option].freeze
|
|
12
|
+
FIELD_CALLS = %i[field].freeze
|
|
13
|
+
HANDLER_METHOD_CANDIDATES = %i[process handle call perform run execute].freeze
|
|
14
|
+
DISPATCH_PARAM_CANDIDATES = %i[from source port input_name].freeze
|
|
15
|
+
EMIT_MESSAGES = %i[emit publish send emit_entity].freeze
|
|
16
|
+
EMIT_PORT_KEYWORDS = %i[to port output].freeze
|
|
17
|
+
|
|
18
|
+
def self.load_overrides(path, root: File.dirname(path))
|
|
19
|
+
raw = YAML.safe_load_file(path, aliases: true) || {}
|
|
20
|
+
new(root: root, overrides: raw, config_path: path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.find(start_dir = Dir.pwd)
|
|
24
|
+
dir = File.expand_path(start_dir)
|
|
25
|
+
loop do
|
|
26
|
+
candidate = File.join(dir, "autotype.yml")
|
|
27
|
+
return candidate if File.file?(candidate)
|
|
28
|
+
|
|
29
|
+
parent = File.dirname(dir)
|
|
30
|
+
break if parent == dir
|
|
31
|
+
|
|
32
|
+
dir = parent
|
|
33
|
+
end
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(root: Dir.pwd, overrides: {}, config_path: nil)
|
|
38
|
+
@root = File.expand_path(root)
|
|
39
|
+
@overrides = stringify_keys(overrides)
|
|
40
|
+
@config_path = config_path
|
|
41
|
+
@search_roots = Set.new([@root])
|
|
42
|
+
@port_dsl_owners = Set.new
|
|
43
|
+
@referenced_constants = Set.new
|
|
44
|
+
reset_inference!
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
attr_reader :root
|
|
48
|
+
|
|
49
|
+
def path
|
|
50
|
+
@config_path
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prepare_search!(paths)
|
|
54
|
+
paths.each do |file|
|
|
55
|
+
next unless File.file?(file)
|
|
56
|
+
|
|
57
|
+
@search_roots << find_project_root(File.dirname(File.expand_path(file)))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def finalize!(collectors)
|
|
62
|
+
methods = collectors.flat_map(&:methods)
|
|
63
|
+
ports = merge_ports(collectors)
|
|
64
|
+
|
|
65
|
+
@port_wiring ||= infer_port_wiring(methods, ports)
|
|
66
|
+
@output_emit ||= infer_output_emit(methods, ports)
|
|
67
|
+
@config_hash ||= infer_config_hash(methods, collectors)
|
|
68
|
+
@side_effect_methods ||= infer_side_effect_methods(methods, ports)
|
|
69
|
+
@structured_type_prefixes = infer_structured_prefixes(collectors, ports) if @structured_type_prefixes.empty?
|
|
70
|
+
@framework_self_fallbacks = infer_self_fallbacks(methods, ports) if @framework_self_fallbacks.empty?
|
|
71
|
+
collectors.each { collect_referenced_constants(_1) }
|
|
72
|
+
@finalized = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def skipped_files
|
|
76
|
+
Array(@overrides.fetch("skip", [])).to_set
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def member_type_hints
|
|
80
|
+
@member_type_hints ||= parse_type_map(@overrides.dig("types", "members"))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def option_type_hints
|
|
84
|
+
@option_type_hints ||= parse_type_map(@overrides.dig("types", "options"))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def side_effect_methods
|
|
88
|
+
@side_effect_methods || Set.new
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def structured_type_prefixes
|
|
92
|
+
@structured_type_prefixes || []
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def port_wiring
|
|
96
|
+
@port_wiring
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def output_emit
|
|
100
|
+
@output_emit
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def config_hash
|
|
104
|
+
@config_hash
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def framework_self_fallbacks
|
|
108
|
+
@framework_self_fallbacks || {}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pipeline_class?(collector, owner)
|
|
112
|
+
return false if collector.singleton_context? || collector.namespace_empty?
|
|
113
|
+
|
|
114
|
+
return true if @port_dsl_owners.include?(owner)
|
|
115
|
+
|
|
116
|
+
superclass = collector.superclass_for(owner)
|
|
117
|
+
superclass == "Actor" || superclass&.end_with?("::Actor")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def entity_define_receiver?(receiver_slice)
|
|
121
|
+
return false unless receiver_slice
|
|
122
|
+
|
|
123
|
+
receiver_slice == "Entity" ||
|
|
124
|
+
receiver_slice == "Data" ||
|
|
125
|
+
receiver_slice.end_with?("::Entity")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def visit_call_node(collector, node)
|
|
129
|
+
return unless node.receiver.nil?
|
|
130
|
+
return if collector.singleton_context?
|
|
131
|
+
|
|
132
|
+
owner = collector.current_owner
|
|
133
|
+
if port_call?(node.name)
|
|
134
|
+
record_port(collector, node)
|
|
135
|
+
@port_dsl_owners << owner
|
|
136
|
+
elsif option_call?(node.name)
|
|
137
|
+
record_option(collector, node)
|
|
138
|
+
@port_dsl_owners << owner
|
|
139
|
+
elsif field_call?(node.name) && collector.structured_field_context?
|
|
140
|
+
record_field(collector, node)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def metadata_from(collector)
|
|
145
|
+
InferenceMetadata.from_collector(
|
|
146
|
+
collector,
|
|
147
|
+
member_type_hints: member_type_hints,
|
|
148
|
+
option_type_hints: option_type_hints,
|
|
149
|
+
side_effect_methods: side_effect_methods,
|
|
150
|
+
structured_type_prefixes: structured_type_prefixes,
|
|
151
|
+
port_wiring: port_wiring,
|
|
152
|
+
output_emit: output_emit,
|
|
153
|
+
config_hash: config_hash,
|
|
154
|
+
framework_self_fallbacks: framework_self_fallbacks,
|
|
155
|
+
type_locators: [method(:locate_type_file)]
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def locate_type_file(type_name)
|
|
160
|
+
return @type_path_cache[type_name] if @type_path_cache.key?(type_name)
|
|
161
|
+
|
|
162
|
+
explicit = explicit_locator(type_name)
|
|
163
|
+
return cache_type_path(type_name, explicit) if explicit
|
|
164
|
+
|
|
165
|
+
basename = type_name.split("::").last
|
|
166
|
+
snake = underscore(basename)
|
|
167
|
+
candidates = @search_roots.flat_map do |search_root|
|
|
168
|
+
Dir.glob(File.join(search_root, "**", "#{snake}.rb"))
|
|
169
|
+
end.uniq
|
|
170
|
+
|
|
171
|
+
path = candidates.find { defines_type?(_1, type_name, basename) }
|
|
172
|
+
cache_type_path(type_name, path)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def reset_inference!
|
|
178
|
+
@finalized = false
|
|
179
|
+
@port_wiring = explicit_port_wiring
|
|
180
|
+
@output_emit = explicit_output_emit
|
|
181
|
+
@config_hash = explicit_config_hash
|
|
182
|
+
@side_effect_methods = explicit_side_effect_methods
|
|
183
|
+
@structured_type_prefixes = Array(@overrides.dig("structured", "prefixes"))
|
|
184
|
+
@framework_self_fallbacks = explicit_self_fallbacks
|
|
185
|
+
@type_path_cache = {}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def cache_type_path(type_name, path)
|
|
189
|
+
@type_path_cache[type_name] = path
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def explicit_locator(type_name)
|
|
193
|
+
Array(@overrides.fetch("locators", [])).each do |locator|
|
|
194
|
+
prefix = locator.fetch("prefix")
|
|
195
|
+
next unless type_name.start_with?(prefix)
|
|
196
|
+
|
|
197
|
+
basename = type_name.split("::").last
|
|
198
|
+
snake = underscore(basename)
|
|
199
|
+
Array(locator.fetch("paths")).each do |template|
|
|
200
|
+
path = File.expand_path(template.gsub("{snake}", snake), @root)
|
|
201
|
+
return path if File.file?(path)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def explicit_port_wiring
|
|
208
|
+
raw = @overrides.dig("wiring", "ports")
|
|
209
|
+
return unless raw
|
|
210
|
+
|
|
211
|
+
InferenceMetadata::PortWiring.new(
|
|
212
|
+
handler_method: raw.fetch("handler").to_sym,
|
|
213
|
+
dispatch_param: raw.fetch("dispatch_param").to_sym,
|
|
214
|
+
dispatch_param_type: TypeString.parse(raw.fetch("dispatch_type", "Symbol")),
|
|
215
|
+
entity_param: raw.fetch("entity_param", "first_required").to_sym
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def explicit_output_emit
|
|
220
|
+
raw = @overrides.dig("wiring", "emit")
|
|
221
|
+
return unless raw
|
|
222
|
+
|
|
223
|
+
InferenceMetadata::OutputEmitWiring.new(
|
|
224
|
+
message: raw.fetch("message").to_sym,
|
|
225
|
+
port_keyword: raw.fetch("port_keyword").to_sym,
|
|
226
|
+
argument_index: raw.fetch("argument_index")
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def explicit_config_hash
|
|
231
|
+
raw = @overrides.dig("wiring", "config_hash")
|
|
232
|
+
return unless raw
|
|
233
|
+
|
|
234
|
+
InferenceMetadata::ConfigHashWiring.new(
|
|
235
|
+
init_param: raw.fetch("init_param").to_sym,
|
|
236
|
+
reader_method: raw.fetch("reader").to_sym,
|
|
237
|
+
ivar: raw.fetch("ivar")
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def explicit_side_effect_methods
|
|
242
|
+
methods = Array(@overrides.dig("pipeline", "side_effect_methods"))
|
|
243
|
+
return if methods.empty?
|
|
244
|
+
|
|
245
|
+
methods.map(&:to_sym).to_set
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def explicit_self_fallbacks
|
|
249
|
+
@overrides.fetch("self_types", {}).each_with_object({}) do |(message, type_string), fallbacks|
|
|
250
|
+
type = TypeString.parse(type_string)
|
|
251
|
+
fallbacks[message.to_sym] = ->(_method, _capability) { type }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def merge_ports(collectors)
|
|
256
|
+
ports = Hash.new { |hash, key| hash[key] = { inputs: {}, outputs: {} } }
|
|
257
|
+
collectors.each do |collector|
|
|
258
|
+
collector.declared_ports.each do |owner, declared|
|
|
259
|
+
ports[owner][:inputs].merge!(declared[:inputs])
|
|
260
|
+
ports[owner][:outputs].merge!(declared[:outputs])
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
ports
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def infer_port_wiring(methods, ports)
|
|
267
|
+
owners = ports.select { |_owner, declared| declared[:inputs].any? }.keys
|
|
268
|
+
observations = owners.filter_map do |owner|
|
|
269
|
+
observe_port_handler(methods, owner)
|
|
270
|
+
end
|
|
271
|
+
return observations.first if observations.length == 1
|
|
272
|
+
|
|
273
|
+
grouped = observations.group_by { |observation| [observation.handler_method, observation.dispatch_param] }
|
|
274
|
+
grouped.values.max_by(&:length)&.first
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def observe_port_handler(methods, owner)
|
|
278
|
+
candidates = methods.select do |method|
|
|
279
|
+
method.owner == owner &&
|
|
280
|
+
method.kind == :instance &&
|
|
281
|
+
method.parameters.any? { |parameter| dispatch_param?(parameter) }
|
|
282
|
+
end
|
|
283
|
+
return if candidates.empty?
|
|
284
|
+
|
|
285
|
+
handler = HANDLER_METHOD_CANDIDATES.filter_map { |name| candidates.find { |method| method.method_name == name } }.first
|
|
286
|
+
handler ||= candidates.min_by { |method| method.line.to_i }
|
|
287
|
+
|
|
288
|
+
dispatch = handler.parameters.find { |parameter| dispatch_param?(parameter) }
|
|
289
|
+
InferenceMetadata::PortWiring.new(
|
|
290
|
+
handler_method: handler.method_name,
|
|
291
|
+
dispatch_param: dispatch.name,
|
|
292
|
+
dispatch_param_type: Named.new("Symbol"),
|
|
293
|
+
entity_param: :first_required
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def dispatch_param?(parameter)
|
|
298
|
+
DISPATCH_PARAM_CANDIDATES.include?(parameter.name) &&
|
|
299
|
+
%i[keyword optional_keyword].include?(parameter.kind)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def infer_output_emit(methods, ports)
|
|
303
|
+
owners_with_outputs = ports.select { |_owner, declared| declared[:outputs].any? }.keys.to_set
|
|
304
|
+
observations = []
|
|
305
|
+
methods.each do |method|
|
|
306
|
+
next unless owners_with_outputs.include?(method.owner)
|
|
307
|
+
|
|
308
|
+
method.capabilities.each do |capability|
|
|
309
|
+
next unless capability.receiver == method.self_type
|
|
310
|
+
next unless EMIT_MESSAGES.include?(capability.message)
|
|
311
|
+
|
|
312
|
+
EMIT_PORT_KEYWORDS.each do |keyword|
|
|
313
|
+
next unless capability.keywords.key?(keyword)
|
|
314
|
+
|
|
315
|
+
observations << {
|
|
316
|
+
message: capability.message,
|
|
317
|
+
port_keyword: keyword,
|
|
318
|
+
argument_index: 0
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
return if observations.empty?
|
|
324
|
+
|
|
325
|
+
grouped = observations.group_by { |observation| observation.values_at(:message, :port_keyword, :argument_index) }
|
|
326
|
+
winner = grouped.max_by { |_key, group| group.length }&.last&.first
|
|
327
|
+
InferenceMetadata::OutputEmitWiring.new(
|
|
328
|
+
message: winner.fetch(:message),
|
|
329
|
+
port_keyword: winner.fetch(:port_keyword),
|
|
330
|
+
argument_index: winner.fetch(:argument_index)
|
|
331
|
+
)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def infer_config_hash(methods, collectors)
|
|
335
|
+
option_owners = collectors.flat_map { |collector| collector.declared_config_options.keys }.to_set
|
|
336
|
+
return if option_owners.empty?
|
|
337
|
+
|
|
338
|
+
observations = option_owners.filter_map do |owner|
|
|
339
|
+
reader = methods.find do |method|
|
|
340
|
+
method.owner == owner &&
|
|
341
|
+
method.kind == :instance &&
|
|
342
|
+
method.method_name == :options &&
|
|
343
|
+
method.parameters.empty?
|
|
344
|
+
end
|
|
345
|
+
next unless reader
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
init_param: :options,
|
|
349
|
+
reader_method: :options,
|
|
350
|
+
ivar: "@options"
|
|
351
|
+
}
|
|
352
|
+
end
|
|
353
|
+
return if observations.empty?
|
|
354
|
+
|
|
355
|
+
grouped = observations.group_by(&:itself)
|
|
356
|
+
winner = grouped.max_by { |_key, group| group.length }&.first
|
|
357
|
+
InferenceMetadata::ConfigHashWiring.new(
|
|
358
|
+
init_param: winner.fetch(:init_param),
|
|
359
|
+
reader_method: winner.fetch(:reader_method),
|
|
360
|
+
ivar: winner.fetch(:ivar)
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def infer_side_effect_methods(methods, ports)
|
|
365
|
+
explicit = explicit_side_effect_methods
|
|
366
|
+
return explicit if explicit
|
|
367
|
+
|
|
368
|
+
pipeline_owners = ports.keys.to_set | @port_dsl_owners
|
|
369
|
+
methods.filter_map do |method|
|
|
370
|
+
next unless pipeline_owners.include?(method.owner)
|
|
371
|
+
next unless method.method_name.to_s.match?(/\Aon_[a-z_]+\z/)
|
|
372
|
+
|
|
373
|
+
method.method_name
|
|
374
|
+
end.to_set
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def infer_structured_prefixes(collectors, ports)
|
|
378
|
+
explicit = Array(@overrides.dig("structured", "prefixes"))
|
|
379
|
+
return explicit if explicit.any?
|
|
380
|
+
|
|
381
|
+
constants = Set.new
|
|
382
|
+
collectors.each do |collector|
|
|
383
|
+
collector.referenced_types.each { constants << _1 }
|
|
384
|
+
collector.structured_owners.each { constants << _1 }
|
|
385
|
+
collector.declared_ports.each_value do |declared|
|
|
386
|
+
declared[:inputs].each_value { |type| constants << type.name if type.is_a?(Named) }
|
|
387
|
+
declared[:outputs].each_value { |type| constants << type.name if type.is_a?(Named) }
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
prefixes = constants
|
|
392
|
+
.select { |name| name.include?("::") }
|
|
393
|
+
.map { |name| name.sub(/::[^:]+\z/, "::") }
|
|
394
|
+
.group_by(&:itself)
|
|
395
|
+
.select { |_prefix, group| group.length >= 1 }
|
|
396
|
+
.keys
|
|
397
|
+
prefixes.sort_by { |prefix| -prefix.length }
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def infer_self_fallbacks(methods, ports)
|
|
401
|
+
fallbacks = explicit_self_fallbacks
|
|
402
|
+
pipeline_owners = ports.keys.to_set | @port_dsl_owners
|
|
403
|
+
hash_type = Generic.new("Hash", [Named.new("Symbol"), Named.new("Object")])
|
|
404
|
+
|
|
405
|
+
%i[inputs outputs].each do |message|
|
|
406
|
+
next if fallbacks.key?(message)
|
|
407
|
+
|
|
408
|
+
used = methods.any? do |method|
|
|
409
|
+
pipeline_owners.include?(method.owner) &&
|
|
410
|
+
method.capabilities.any? { |capability| capability.message == message && capability.receiver == method.self_type }
|
|
411
|
+
end
|
|
412
|
+
fallbacks[message] = ->(_method, _capability) { hash_type } if used
|
|
413
|
+
end
|
|
414
|
+
fallbacks
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def collect_referenced_constants(collector)
|
|
418
|
+
collector.referenced_types.each { @referenced_constants << _1 }
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def port_call?(name) = PORT_CALLS.include?(name)
|
|
422
|
+
def option_call?(name) = OPTION_CALLS.include?(name)
|
|
423
|
+
def field_call?(name) = FIELD_CALLS.include?(name)
|
|
424
|
+
|
|
425
|
+
def record_port(collector, node)
|
|
426
|
+
port_name = collector.symbol_arguments(node).first
|
|
427
|
+
type_name = collector.keyword_constant(node.arguments, :type)
|
|
428
|
+
return unless port_name && type_name
|
|
429
|
+
|
|
430
|
+
owner = collector.current_owner
|
|
431
|
+
bucket = node.name == :input ? :inputs : :outputs
|
|
432
|
+
collector.declared_ports[owner][bucket][port_name] = Named.new(type_name)
|
|
433
|
+
collector.referenced_types << type_name if structured_constant?(type_name)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def record_option(collector, node)
|
|
437
|
+
option_name = collector.symbol_arguments(node).first
|
|
438
|
+
return unless option_name
|
|
439
|
+
|
|
440
|
+
collector.declared_config_options[collector.current_owner][option_name] =
|
|
441
|
+
option_type_hints[option_name] || infer_option_type(option_name)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def record_field(collector, node)
|
|
445
|
+
field_name = collector.symbol_arguments(node).first
|
|
446
|
+
return unless field_name
|
|
447
|
+
|
|
448
|
+
hint = member_type_hints[field_name] || infer_member_type(field_name)
|
|
449
|
+
collector.declared_member_types[collector.current_owner][field_name] = hint if hint
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def infer_member_type(name)
|
|
453
|
+
type_name = CORE_MEMBER_NAME_TYPES[name]
|
|
454
|
+
Named.new(type_name) if type_name
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def infer_option_type(name)
|
|
458
|
+
return member_type_hints[name] if member_type_hints.key?(name)
|
|
459
|
+
|
|
460
|
+
inferred = infer_member_type(name) || infer_name_pattern_type(name.to_s)
|
|
461
|
+
inferred || Named.new("Object")
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def infer_name_pattern_type(name)
|
|
465
|
+
case name
|
|
466
|
+
when /\A(?:is_|has_|allow_|enable_|disable_)/ then Named.new("bool")
|
|
467
|
+
when /_(?:url|uri|path|token|model|mode|collection|actor|source|id|key|name|label)\z/ then Named.new("String")
|
|
468
|
+
when /_(?:timeout|size|count|batch|limit|max|min|page|step|iteration)\z/ then Named.new("Integer")
|
|
469
|
+
when /_(?:min|max|score|distance|ratio|threshold)\z/ then Named.new("Float")
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def structured_constant?(type_name)
|
|
474
|
+
type_name.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)+\z/)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def defines_type?(path, type_name, basename)
|
|
478
|
+
return false unless File.file?(path)
|
|
479
|
+
|
|
480
|
+
source = File.read(path)
|
|
481
|
+
return true if source.match?(/\b#{Regexp.escape(type_name)}(?:\s*=|\s*\n)/)
|
|
482
|
+
return true if source.match?(/\bmodule\s+\w+(?:\s*;\s*\w+)*\s*\n(?:.*\n)*?\s*(?:#{Regexp.escape(basename)}\s*=|class\s+#{Regexp.escape(basename)}\b)/m)
|
|
483
|
+
return true if source.match?(/\bclass\s+#{Regexp.escape(basename)}\b/)
|
|
484
|
+
return true if source.match?(/\b#{Regexp.escape(basename)}\s*=\s*\w+(?:::)?(?:Entity|Data)\.define\b/)
|
|
485
|
+
|
|
486
|
+
false
|
|
487
|
+
rescue StandardError
|
|
488
|
+
false
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def find_project_root(start_dir)
|
|
492
|
+
dir = File.expand_path(start_dir)
|
|
493
|
+
loop do
|
|
494
|
+
markers = %w[Gemfile .git autotype.yml].map { |name| File.join(dir, name) }
|
|
495
|
+
return dir if markers.any? { File.exist?(_1) }
|
|
496
|
+
|
|
497
|
+
parent = File.dirname(dir)
|
|
498
|
+
break if parent == dir
|
|
499
|
+
|
|
500
|
+
dir = parent
|
|
501
|
+
end
|
|
502
|
+
File.expand_path(start_dir)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def parse_type_map(map)
|
|
506
|
+
(map || {}).transform_keys(&:to_sym).transform_values { |type_string| TypeString.parse(type_string) }
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def underscore(name)
|
|
510
|
+
name
|
|
511
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
512
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
513
|
+
.downcase
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def stringify_keys(value)
|
|
517
|
+
case value
|
|
518
|
+
when Hash
|
|
519
|
+
value.each_with_object({}) { |(key, entry), hash| hash[key.to_s] = stringify_keys(entry) }
|
|
520
|
+
when Array
|
|
521
|
+
value.map { stringify_keys(_1) }
|
|
522
|
+
else
|
|
523
|
+
value
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
end
|