type-guessr 0.0.1 → 0.0.3
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 +41 -0
- data/exe/type-guessr +30 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
- data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
- data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
- data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
- data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
- data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
- data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
- data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
- data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
- data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
- data/lib/type-guessr.rb +3 -11
- data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
- data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
- data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
- data/lib/type_guessr/core/cache.rb +5 -0
- data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
- data/lib/type_guessr/core/converter/call_converter.rb +161 -0
- data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
- data/lib/type_guessr/core/converter/context.rb +144 -0
- data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
- data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
- data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
- data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
- data/lib/type_guessr/core/converter/registration.rb +100 -0
- data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
- data/lib/type_guessr/core/converter.rb +4 -0
- data/lib/type_guessr/core/index/location_index.rb +32 -0
- data/lib/type_guessr/core/index.rb +3 -0
- data/lib/type_guessr/core/inference/resolver.rb +516 -349
- data/lib/type_guessr/core/inference.rb +4 -0
- data/lib/type_guessr/core/ir/nodes.rb +362 -103
- data/lib/type_guessr/core/ir.rb +3 -0
- data/lib/type_guessr/core/logger.rb +6 -13
- data/lib/type_guessr/core/node_context_helper.rb +126 -0
- data/lib/type_guessr/core/node_key_generator.rb +31 -0
- data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
- data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
- data/lib/type_guessr/core/registry/method_registry.rb +65 -38
- data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
- data/lib/type_guessr/core/registry.rb +6 -0
- data/lib/type_guessr/core/signature_builder.rb +39 -0
- data/lib/type_guessr/core/type_serializer.rb +96 -0
- data/lib/type_guessr/core/type_simplifier.rb +15 -12
- data/lib/type_guessr/core/types.rb +250 -32
- data/lib/type_guessr/core.rb +29 -0
- data/lib/type_guessr/mcp/file_watcher.rb +87 -0
- data/lib/type_guessr/mcp/server.rb +463 -0
- data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
- data/lib/type_guessr/version.rb +1 -1
- metadata +57 -8
- data/lib/type_guessr/core/rbs_provider.rb +0 -304
- data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
- data/lib/type_guessr/core/signature_provider.rb +0 -101
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ar_type_mapper"
|
|
4
|
+
require_relative "ar_schema_watcher"
|
|
5
|
+
|
|
6
|
+
module RubyLsp
|
|
7
|
+
module TypeGuessr
|
|
8
|
+
module Dsl
|
|
9
|
+
# ActiveRecord DSL adapter.
|
|
10
|
+
# Discovers AR models, fetches metadata via ServerAddon,
|
|
11
|
+
# and registers column/enum/association/scope types.
|
|
12
|
+
class ActiveRecordAdapter
|
|
13
|
+
BOOL_TYPE = ::TypeGuessr::Core::Types::Union.new([
|
|
14
|
+
::TypeGuessr::Core::Types::ClassInstance.for("TrueClass"),
|
|
15
|
+
::TypeGuessr::Core::Types::ClassInstance.for("FalseClass"),
|
|
16
|
+
]).freeze
|
|
17
|
+
|
|
18
|
+
SERVER_ADDON_NAME = "TypeGuessr"
|
|
19
|
+
|
|
20
|
+
def initialize(project_root:, cache_dir: nil)
|
|
21
|
+
@project_root = project_root
|
|
22
|
+
@schema_watcher = ArSchemaWatcher.new(project_root, cache_dir: cache_dir)
|
|
23
|
+
@registered_classes = []
|
|
24
|
+
@log_callback = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_log(&block)
|
|
28
|
+
@log_callback = block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def applicable?
|
|
32
|
+
File.directory?(File.join(@project_root, "app", "models"))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Register AR::Base common class methods with SelfType.
|
|
36
|
+
# Called once. User.where → Relation[User] via SelfType substitution.
|
|
37
|
+
def register_base_methods(signature_registry:)
|
|
38
|
+
base = "ActiveRecord::Base"
|
|
39
|
+
self_type = ::TypeGuessr::Core::Types::SelfType.instance
|
|
40
|
+
relation_of_self = ::TypeGuessr::Core::Types::ClassInstance.for(
|
|
41
|
+
"ActiveRecord::Relation", { Elem: self_type }
|
|
42
|
+
)
|
|
43
|
+
nullable_self = ::TypeGuessr::Core::Types::Union.new([
|
|
44
|
+
self_type,
|
|
45
|
+
::TypeGuessr::Core::Types::ClassInstance.for("NilClass"),
|
|
46
|
+
])
|
|
47
|
+
int_type = ::TypeGuessr::Core::Types::ClassInstance.for("Integer")
|
|
48
|
+
bool_type = BOOL_TYPE
|
|
49
|
+
array_of_self = ::TypeGuessr::Core::Types::ClassInstance.for("Array")
|
|
50
|
+
|
|
51
|
+
# --- Class methods ---
|
|
52
|
+
|
|
53
|
+
# Returning Relation[self]
|
|
54
|
+
%w[
|
|
55
|
+
where all order limit offset select distinct group having reorder reverse_order
|
|
56
|
+
none unscoped reselect extending joins left_joins left_outer_joins
|
|
57
|
+
includes eager_load preload references readonly lock create_with rewhere
|
|
58
|
+
or and not invert_where merge
|
|
59
|
+
].each do |m|
|
|
60
|
+
signature_registry.register_gem_class_method(base, m, relation_of_self, force: true)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returning self?
|
|
64
|
+
%w[first last second third forty_two take find_by find_sole_by].each do |m|
|
|
65
|
+
signature_registry.register_gem_class_method(base, m, nullable_self, force: true)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returning self
|
|
69
|
+
%w[
|
|
70
|
+
first! last! take! sole find_by! find
|
|
71
|
+
create create! new
|
|
72
|
+
find_or_create_by find_or_create_by! find_or_initialize_by
|
|
73
|
+
create_or_find_by create_or_find_by!
|
|
74
|
+
].each do |m|
|
|
75
|
+
signature_registry.register_gem_class_method(base, m, self_type, force: true)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returning Integer
|
|
79
|
+
%w[count update_all delete_all].each do |m|
|
|
80
|
+
signature_registry.register_gem_class_method(base, m, int_type, force: true)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returning Array
|
|
84
|
+
%w[ids pluck destroy_all].each do |m|
|
|
85
|
+
signature_registry.register_gem_class_method(base, m, array_of_self, force: true)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returning bool
|
|
89
|
+
%w[exists? any? many? none? empty?].each do |m|
|
|
90
|
+
signature_registry.register_gem_class_method(base, m, bool_type, force: true)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# --- Instance methods ---
|
|
94
|
+
|
|
95
|
+
# Returning bool
|
|
96
|
+
%w[
|
|
97
|
+
save save! update update! toggle! touch
|
|
98
|
+
new_record? persisted? destroyed? changed? previously_new_record? frozen?
|
|
99
|
+
has_changes_to_save? saved_changes? valid? invalid?
|
|
100
|
+
].each do |m|
|
|
101
|
+
signature_registry.register_gem_method(base, m, bool_type, force: true)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returning self
|
|
105
|
+
%w[destroy destroy! reload toggle increment decrement increment! decrement!].each do |m|
|
|
106
|
+
signature_registry.register_gem_method(base, m, self_type, force: true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# --- Relation instance methods ---
|
|
110
|
+
rel = "ActiveRecord::Relation"
|
|
111
|
+
elem = ::TypeGuessr::Core::Types::TypeVariable.new(:Elem)
|
|
112
|
+
nullable_elem = ::TypeGuessr::Core::Types::Union.new([
|
|
113
|
+
elem,
|
|
114
|
+
::TypeGuessr::Core::Types::ClassInstance.for("NilClass"),
|
|
115
|
+
])
|
|
116
|
+
relation_of_elem = ::TypeGuessr::Core::Types::ClassInstance.for(
|
|
117
|
+
"ActiveRecord::Relation", { Elem: elem }
|
|
118
|
+
)
|
|
119
|
+
array_of_elem = ::TypeGuessr::Core::Types::ArrayType.new(elem)
|
|
120
|
+
|
|
121
|
+
# Returning Relation[Elem]
|
|
122
|
+
%w[
|
|
123
|
+
where or and not invert_where merge rewhere readonly lock create_with load reload
|
|
124
|
+
].each do |m|
|
|
125
|
+
signature_registry.register_gem_method(rel, m, relation_of_elem, force: true)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returning Elem?
|
|
129
|
+
%w[first last second take].each do |m|
|
|
130
|
+
signature_registry.register_gem_method(rel, m, nullable_elem, force: true)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returning Integer
|
|
134
|
+
%w[size length count].each do |m|
|
|
135
|
+
signature_registry.register_gem_method(rel, m, int_type, force: true)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returning bool
|
|
139
|
+
%w[empty? any? many? none?].each do |m|
|
|
140
|
+
signature_registry.register_gem_method(rel, m, bool_type, force: true)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returning Array[Elem]
|
|
144
|
+
%w[to_a to_ary].each do |m|
|
|
145
|
+
signature_registry.register_gem_method(rel, m, array_of_elem, force: true)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# --- CollectionProxy instance methods ---
|
|
149
|
+
cp = "ActiveRecord::Associations::CollectionProxy"
|
|
150
|
+
elem_type = ::TypeGuessr::Core::Types::TypeVariable.new(:Elem)
|
|
151
|
+
|
|
152
|
+
%w[build create create! new].each do |m|
|
|
153
|
+
signature_registry.register_gem_method(cp, m, elem_type, force: true)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
log("Registered AR::Base common methods (SelfType)")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Cache check → cache hit: restore, cache miss + runner_client: fetch → register → save.
|
|
160
|
+
def register_models(runner_client:, signature_registry:, code_index:)
|
|
161
|
+
cached = @schema_watcher.load_cache
|
|
162
|
+
if cached
|
|
163
|
+
apply_model_data(cached, signature_registry, code_index)
|
|
164
|
+
log("Loaded #{@registered_classes.size} models from DSL cache (fast path)")
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
return unless runner_client
|
|
169
|
+
|
|
170
|
+
register_server_addon(runner_client)
|
|
171
|
+
models = discover_models
|
|
172
|
+
return if models.empty?
|
|
173
|
+
|
|
174
|
+
data = fetch_all_metadata(runner_client, models)
|
|
175
|
+
apply_model_data(data, signature_registry, code_index)
|
|
176
|
+
@schema_watcher.save_cache(data) unless data.empty?
|
|
177
|
+
log("Registered #{@registered_classes.size} models via ServerAddon")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def changed?
|
|
181
|
+
@schema_watcher.changed?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Build-then-swap: fetch new data first, then purge old + apply new.
|
|
185
|
+
def refresh(runner_client:, signature_registry:, code_index:)
|
|
186
|
+
return unless runner_client
|
|
187
|
+
|
|
188
|
+
register_server_addon(runner_client)
|
|
189
|
+
models = discover_models
|
|
190
|
+
new_data = fetch_all_metadata(runner_client, models)
|
|
191
|
+
|
|
192
|
+
# Swap: purge old, apply new
|
|
193
|
+
@registered_classes.each { |cn| code_index.unregister_method_classes(cn) }
|
|
194
|
+
@registered_classes.clear
|
|
195
|
+
apply_model_data(new_data, signature_registry, code_index)
|
|
196
|
+
@schema_watcher.save_cache(new_data) unless new_data.empty?
|
|
197
|
+
log("Refreshed #{@registered_classes.size} models via ServerAddon")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private def log(message)
|
|
201
|
+
@log_callback&.call(message)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private def register_server_addon(runner_client)
|
|
205
|
+
addon_path = File.expand_path("../rails_server_addon", __dir__)
|
|
206
|
+
runner_client.register_server_addon(addon_path)
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
log("Failed to register ServerAddon: #{e.message}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private def discover_models
|
|
212
|
+
models_dir = File.join(@project_root, "app", "models")
|
|
213
|
+
return [] unless File.directory?(models_dir)
|
|
214
|
+
|
|
215
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).filter_map do |path|
|
|
216
|
+
relative = path.delete_prefix("#{models_dir}/").delete_suffix(".rb")
|
|
217
|
+
next if relative.start_with?("concerns/")
|
|
218
|
+
next if relative == "application_record"
|
|
219
|
+
|
|
220
|
+
relative.split("/").map { |part| camelize(part) }.join("::")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private def fetch_all_metadata(runner_client, models)
|
|
225
|
+
data = {}
|
|
226
|
+
|
|
227
|
+
models.each do |class_name|
|
|
228
|
+
result = runner_client.delegate_request(
|
|
229
|
+
server_addon_name: SERVER_ADDON_NAME,
|
|
230
|
+
request_name: "model_metadata",
|
|
231
|
+
name: class_name
|
|
232
|
+
)
|
|
233
|
+
next unless result.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
model_data = build_model_data(class_name, result)
|
|
236
|
+
data[class_name] = model_data unless model_data.empty?
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
log("Failed to fetch metadata for #{class_name}: #{e.message}")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
data
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private def build_model_data(class_name, result)
|
|
245
|
+
methods = {}
|
|
246
|
+
build_column_methods(methods, result[:columns] || result["columns"] || [])
|
|
247
|
+
build_enum_methods(methods, class_name, result[:enums] || result["enums"] || {})
|
|
248
|
+
build_association_methods(methods, result[:associations] || result["associations"] || [])
|
|
249
|
+
build_scope_methods(methods, class_name, result[:scopes] || result["scopes"] || [])
|
|
250
|
+
methods
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private def build_column_methods(methods, columns)
|
|
254
|
+
columns.each do |col|
|
|
255
|
+
col_name, col_type, nullable = col
|
|
256
|
+
col_name = col_name.to_s
|
|
257
|
+
nullable = true if nullable.nil?
|
|
258
|
+
|
|
259
|
+
# Reader: user.name → String?
|
|
260
|
+
methods[col_name] = { "kind" => "column", "type" => col_type.to_s, "nullable" => nullable }
|
|
261
|
+
|
|
262
|
+
# Predicate: user.name? → bool
|
|
263
|
+
methods["#{col_name}?"] = { "kind" => "column_predicate" }
|
|
264
|
+
|
|
265
|
+
# Dirty tracking
|
|
266
|
+
methods["#{col_name}_changed?"] = { "kind" => "column_predicate" }
|
|
267
|
+
methods["#{col_name}_previously_changed?"] = { "kind" => "column_predicate" }
|
|
268
|
+
methods["saved_change_to_#{col_name}?"] = { "kind" => "column_predicate" }
|
|
269
|
+
methods["will_save_change_to_#{col_name}?"] = { "kind" => "column_predicate" }
|
|
270
|
+
methods["#{col_name}_was"] = { "kind" => "column", "type" => col_type.to_s, "nullable" => true }
|
|
271
|
+
methods["#{col_name}_in_database"] = { "kind" => "column", "type" => col_type.to_s, "nullable" => true }
|
|
272
|
+
methods["#{col_name}_before_last_save"] = { "kind" => "column", "type" => col_type.to_s, "nullable" => true }
|
|
273
|
+
methods["#{col_name}_will_change!"] = { "kind" => "column_predicate" }
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
private def build_enum_methods(methods, class_name, enums)
|
|
278
|
+
enums.each do |attr_name, values|
|
|
279
|
+
methods[attr_name.to_s] = { "kind" => "enum_reader", "type" => "string", "nullable" => true }
|
|
280
|
+
|
|
281
|
+
values.each_key do |value_name|
|
|
282
|
+
methods["#{value_name}?"] = { "kind" => "enum_predicate" }
|
|
283
|
+
methods["#{value_name}!"] = { "kind" => "enum_bang" }
|
|
284
|
+
methods["scope:#{value_name}"] = { "kind" => "enum_scope", "class_name" => class_name }
|
|
285
|
+
methods["scope:not_#{value_name}"] = { "kind" => "enum_scope", "class_name" => class_name }
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
private def build_association_methods(methods, associations)
|
|
291
|
+
associations.each do |assoc|
|
|
292
|
+
name = (assoc[:name] || assoc["name"]).to_s
|
|
293
|
+
macro = (assoc[:macro] || assoc["macro"]).to_s
|
|
294
|
+
target = assoc[:class_name] || assoc["class_name"]
|
|
295
|
+
|
|
296
|
+
# Reader: user.posts / user.profile
|
|
297
|
+
methods[name] = { "kind" => "association", "macro" => macro, "target" => target }
|
|
298
|
+
|
|
299
|
+
# has_many: post_ids → Array[Integer]
|
|
300
|
+
if %w[has_many has_and_belongs_to_many].include?(macro)
|
|
301
|
+
singular = name.end_with?("s") ? name[0..-2] : name
|
|
302
|
+
methods["#{singular}_ids"] = { "kind" => "association_ids" }
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Derived methods for singular associations (has_one, belongs_to)
|
|
306
|
+
next unless %w[has_one belongs_to].include?(macro)
|
|
307
|
+
|
|
308
|
+
methods["build_#{name}"] = { "kind" => "association_builder", "target" => target }
|
|
309
|
+
methods["create_#{name}"] = { "kind" => "association_builder", "target" => target }
|
|
310
|
+
methods["create_#{name}!"] = { "kind" => "association_builder", "target" => target }
|
|
311
|
+
methods["reload_#{name}"] = { "kind" => "association", "macro" => macro, "target" => target }
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private def build_scope_methods(methods, class_name, scopes)
|
|
316
|
+
scopes.each do |scope_name|
|
|
317
|
+
methods["scope:#{scope_name}"] = { "kind" => "scope", "class_name" => class_name }
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
private def apply_model_data(data, signature_registry, code_index)
|
|
322
|
+
data.each do |class_name, methods|
|
|
323
|
+
methods.each do |method_name, info|
|
|
324
|
+
case info["kind"]
|
|
325
|
+
when "column"
|
|
326
|
+
return_type = ArTypeMapper.map(info["type"], nullable: info.fetch("nullable", true))
|
|
327
|
+
register_instance_method(class_name, method_name, return_type, signature_registry, code_index)
|
|
328
|
+
when "enum_reader"
|
|
329
|
+
return_type = ArTypeMapper.map("string", nullable: true)
|
|
330
|
+
register_instance_method(class_name, method_name, return_type, signature_registry, code_index)
|
|
331
|
+
when "column_predicate"
|
|
332
|
+
register_instance_method(class_name, method_name, BOOL_TYPE, signature_registry, code_index)
|
|
333
|
+
when "enum_predicate"
|
|
334
|
+
register_instance_method(class_name, method_name, BOOL_TYPE, signature_registry, code_index)
|
|
335
|
+
when "enum_bang"
|
|
336
|
+
register_instance_method(
|
|
337
|
+
class_name, method_name,
|
|
338
|
+
::TypeGuessr::Core::Types::Unknown.instance,
|
|
339
|
+
signature_registry, code_index
|
|
340
|
+
)
|
|
341
|
+
when "enum_scope"
|
|
342
|
+
scope_name = method_name.delete_prefix("scope:")
|
|
343
|
+
register_class_scope(info["class_name"], scope_name, signature_registry)
|
|
344
|
+
when "association"
|
|
345
|
+
register_association(class_name, method_name, info["macro"], info["target"],
|
|
346
|
+
signature_registry, code_index)
|
|
347
|
+
when "association_builder"
|
|
348
|
+
target_type = ::TypeGuessr::Core::Types::ClassInstance.for(info["target"])
|
|
349
|
+
register_instance_method(class_name, method_name, target_type, signature_registry, code_index)
|
|
350
|
+
when "association_ids"
|
|
351
|
+
array_type = ::TypeGuessr::Core::Types::ArrayType.new(
|
|
352
|
+
::TypeGuessr::Core::Types::ClassInstance.for("Integer")
|
|
353
|
+
)
|
|
354
|
+
register_instance_method(class_name, method_name, array_type, signature_registry, code_index)
|
|
355
|
+
when "scope"
|
|
356
|
+
scope_name = method_name.delete_prefix("scope:")
|
|
357
|
+
register_class_scope(info["class_name"], scope_name, signature_registry)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
@registered_classes << class_name
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
private def register_instance_method(class_name, method_name, return_type, signature_registry, code_index)
|
|
366
|
+
signature_registry.register_gem_method(class_name, method_name, return_type, force: true)
|
|
367
|
+
code_index.register_method_class(class_name, method_name)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
private def register_class_scope(class_name, scope_name, signature_registry)
|
|
371
|
+
return_type = ::TypeGuessr::Core::Types::ClassInstance.for(
|
|
372
|
+
"ActiveRecord::Relation",
|
|
373
|
+
{ Elem: ::TypeGuessr::Core::Types::ClassInstance.for(class_name) }
|
|
374
|
+
)
|
|
375
|
+
signature_registry.register_gem_class_method(class_name, scope_name, return_type, force: true)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
private def register_association(class_name, assoc_name, macro, target, signature_registry, code_index)
|
|
379
|
+
return_type = case macro
|
|
380
|
+
when "has_many", "has_and_belongs_to_many"
|
|
381
|
+
::TypeGuessr::Core::Types::ClassInstance.for(
|
|
382
|
+
"ActiveRecord::Associations::CollectionProxy",
|
|
383
|
+
{ Elem: ::TypeGuessr::Core::Types::ClassInstance.for(target) }
|
|
384
|
+
)
|
|
385
|
+
when "has_one", "belongs_to"
|
|
386
|
+
::TypeGuessr::Core::Types::Union.new([
|
|
387
|
+
::TypeGuessr::Core::Types::ClassInstance.for(target),
|
|
388
|
+
::TypeGuessr::Core::Types::ClassInstance.for("NilClass"),
|
|
389
|
+
])
|
|
390
|
+
else
|
|
391
|
+
::TypeGuessr::Core::Types::Unknown.instance
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
register_instance_method(class_name, assoc_name, return_type, signature_registry, code_index)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
private def camelize(str)
|
|
398
|
+
str.split("_").map(&:capitalize).join
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha2"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module RubyLsp
|
|
8
|
+
module TypeGuessr
|
|
9
|
+
module Dsl
|
|
10
|
+
# Watches AR schema files (db/schema.rb, db/structure.sql) for changes
|
|
11
|
+
# and manages a disk cache for DSL-generated type data.
|
|
12
|
+
#
|
|
13
|
+
# AR-specific: Mongoid has no schema file concept and needs a different strategy.
|
|
14
|
+
class ArSchemaWatcher
|
|
15
|
+
CACHE_VERSION = 1
|
|
16
|
+
|
|
17
|
+
def initialize(project_root, cache_dir: nil)
|
|
18
|
+
@project_root = project_root
|
|
19
|
+
@cache_dir = cache_dir || default_cache_dir
|
|
20
|
+
@last_hash = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def schema_files
|
|
24
|
+
patterns = [
|
|
25
|
+
File.join(@project_root, "db", "**", "schema.rb"),
|
|
26
|
+
File.join(@project_root, "db", "**", "structure.sql"),
|
|
27
|
+
]
|
|
28
|
+
patterns.flat_map { |p| Dir.glob(p) }.sort
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def current_hash
|
|
32
|
+
files = schema_files
|
|
33
|
+
return "empty" if files.empty?
|
|
34
|
+
|
|
35
|
+
digest = Digest::SHA256.new
|
|
36
|
+
files.each do |f|
|
|
37
|
+
digest.update(f)
|
|
38
|
+
digest.update(File.read(f))
|
|
39
|
+
end
|
|
40
|
+
digest.hexdigest
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def changed?
|
|
44
|
+
hash = current_hash
|
|
45
|
+
changed = @last_hash.nil? || @last_hash != hash
|
|
46
|
+
@last_hash = hash
|
|
47
|
+
changed
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_cache
|
|
51
|
+
path = cache_path
|
|
52
|
+
return nil unless File.exist?(path)
|
|
53
|
+
|
|
54
|
+
data = JSON.parse(File.read(path))
|
|
55
|
+
return nil unless data["version"] == CACHE_VERSION
|
|
56
|
+
|
|
57
|
+
hash = current_hash
|
|
58
|
+
return nil unless data["schema_hash"] == hash
|
|
59
|
+
|
|
60
|
+
@last_hash = hash
|
|
61
|
+
data["models"]
|
|
62
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def save_cache(models_data)
|
|
67
|
+
FileUtils.mkdir_p(File.dirname(cache_path))
|
|
68
|
+
|
|
69
|
+
hash = current_hash
|
|
70
|
+
data = {
|
|
71
|
+
"version" => CACHE_VERSION,
|
|
72
|
+
"schema_hash" => hash,
|
|
73
|
+
"models" => models_data
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
File.write(cache_path, JSON.generate(data))
|
|
77
|
+
@last_hash = hash
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def clear_cache
|
|
81
|
+
FileUtils.rm_f(cache_path)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private def cache_path
|
|
85
|
+
project_dir = @project_root.gsub(%r{[/\\]}, "-").gsub(/\A-/, "")
|
|
86
|
+
File.join(@cache_dir, project_dir, "cache.json")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private def default_cache_dir
|
|
90
|
+
xdg_cache = ENV.fetch("XDG_CACHE_HOME", File.join(Dir.home, ".cache"))
|
|
91
|
+
File.join(xdg_cache, "type-guessr", "dsl-cache")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../type_guessr/core/types"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module TypeGuessr
|
|
7
|
+
module Dsl
|
|
8
|
+
# Maps ActiveRecord column type strings to TypeGuessr Types.
|
|
9
|
+
# Pure mapping with no external dependencies.
|
|
10
|
+
# AR-specific: Mongoid provides Ruby types directly in field declarations.
|
|
11
|
+
module ArTypeMapper
|
|
12
|
+
def self.map(ar_type, nullable: true)
|
|
13
|
+
base = case ar_type.to_s
|
|
14
|
+
when "string", "text"
|
|
15
|
+
::TypeGuessr::Core::Types::ClassInstance.for("String")
|
|
16
|
+
when "integer", "bigint"
|
|
17
|
+
::TypeGuessr::Core::Types::ClassInstance.for("Integer")
|
|
18
|
+
when "boolean"
|
|
19
|
+
::TypeGuessr::Core::Types::Union.new([
|
|
20
|
+
::TypeGuessr::Core::Types::ClassInstance.for("TrueClass"),
|
|
21
|
+
::TypeGuessr::Core::Types::ClassInstance.for("FalseClass"),
|
|
22
|
+
])
|
|
23
|
+
when "float"
|
|
24
|
+
::TypeGuessr::Core::Types::ClassInstance.for("Float")
|
|
25
|
+
when "decimal"
|
|
26
|
+
::TypeGuessr::Core::Types::ClassInstance.for("BigDecimal")
|
|
27
|
+
when "date"
|
|
28
|
+
::TypeGuessr::Core::Types::ClassInstance.for("Date")
|
|
29
|
+
when "datetime", "timestamp"
|
|
30
|
+
::TypeGuessr::Core::Types::ClassInstance.for("ActiveSupport::TimeWithZone")
|
|
31
|
+
when "json", "jsonb"
|
|
32
|
+
::TypeGuessr::Core::Types::ClassInstance.for("Hash")
|
|
33
|
+
else
|
|
34
|
+
::TypeGuessr::Core::Types::Unknown.instance
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if nullable && !base.is_a?(::TypeGuessr::Core::Types::Unknown)
|
|
38
|
+
nil_type = ::TypeGuessr::Core::Types::ClassInstance.for("NilClass")
|
|
39
|
+
if base.is_a?(::TypeGuessr::Core::Types::Union)
|
|
40
|
+
::TypeGuessr::Core::Types::Union.new(base.types + [nil_type])
|
|
41
|
+
else
|
|
42
|
+
::TypeGuessr::Core::Types::Union.new([base, nil_type])
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
base
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dsl"
|
|
4
|
+
|
|
5
|
+
module RubyLsp
|
|
6
|
+
module TypeGuessr
|
|
7
|
+
# Pure orchestrator for DSL type registration.
|
|
8
|
+
# No framework logic, no cache logic — delegates everything to Adapters.
|
|
9
|
+
class DslTypeRegistrar
|
|
10
|
+
def initialize(signature_registry:, code_index:, project_root:, adapters: nil)
|
|
11
|
+
@signature_registry = signature_registry
|
|
12
|
+
@code_index = code_index
|
|
13
|
+
@project_root = project_root
|
|
14
|
+
@adapters = adapters || default_adapters
|
|
15
|
+
@base_registered = false
|
|
16
|
+
@log_callback = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_log(&block)
|
|
20
|
+
@log_callback = block
|
|
21
|
+
active_adapters.each { |a| a.on_log(&block) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def register_all(runner_client: nil)
|
|
25
|
+
unless @base_registered
|
|
26
|
+
active_adapters.each { |a| a.register_base_methods(signature_registry: @signature_registry) }
|
|
27
|
+
@base_registered = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
active_adapters.each do |adapter|
|
|
31
|
+
adapter.register_models(
|
|
32
|
+
runner_client: runner_client,
|
|
33
|
+
signature_registry: @signature_registry,
|
|
34
|
+
code_index: @code_index
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check_and_refresh(runner_client: nil)
|
|
40
|
+
active_adapters.each do |adapter|
|
|
41
|
+
next unless adapter.changed?
|
|
42
|
+
|
|
43
|
+
adapter.refresh(
|
|
44
|
+
runner_client: runner_client,
|
|
45
|
+
signature_registry: @signature_registry,
|
|
46
|
+
code_index: @code_index
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private def active_adapters
|
|
52
|
+
@adapters.select(&:applicable?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private def default_adapters
|
|
56
|
+
[Dsl::ActiveRecordAdapter.new(project_root: @project_root)]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|