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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -0
  3. data/exe/type-guessr +30 -0
  4. data/lib/ruby_lsp/type_guessr/addon.rb +20 -45
  5. data/lib/ruby_lsp/type_guessr/code_index_adapter.rb +352 -0
  6. data/lib/ruby_lsp/type_guessr/constants.rb +39 -0
  7. data/lib/ruby_lsp/type_guessr/{graph_builder.rb → debug_graph_builder.rb} +27 -22
  8. data/lib/ruby_lsp/type_guessr/debug_server.rb +20 -17
  9. data/lib/ruby_lsp/type_guessr/dsl/activerecord_adapter.rb +404 -0
  10. data/lib/ruby_lsp/type_guessr/dsl/ar_schema_watcher.rb +96 -0
  11. data/lib/ruby_lsp/type_guessr/dsl/ar_type_mapper.rb +51 -0
  12. data/lib/ruby_lsp/type_guessr/dsl.rb +3 -0
  13. data/lib/ruby_lsp/type_guessr/dsl_type_registrar.rb +60 -0
  14. data/lib/ruby_lsp/type_guessr/hover.rb +129 -261
  15. data/lib/ruby_lsp/type_guessr/rails_server_addon.rb +83 -0
  16. data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +613 -277
  17. data/lib/ruby_lsp/type_guessr/type_inferrer.rb +8 -105
  18. data/lib/type-guessr.rb +3 -11
  19. data/lib/type_guessr/core/cache/gem_dependency_resolver.rb +113 -0
  20. data/lib/type_guessr/core/cache/gem_signature_cache.rb +98 -0
  21. data/lib/type_guessr/core/cache/gem_signature_extractor.rb +87 -0
  22. data/lib/type_guessr/core/cache.rb +5 -0
  23. data/lib/{ruby_lsp/type_guessr → type_guessr/core}/config.rb +19 -34
  24. data/lib/type_guessr/core/converter/call_converter.rb +161 -0
  25. data/lib/type_guessr/core/converter/container_mutation_converter.rb +241 -0
  26. data/lib/type_guessr/core/converter/context.rb +144 -0
  27. data/lib/type_guessr/core/converter/control_flow_converter.rb +425 -0
  28. data/lib/type_guessr/core/converter/definition_converter.rb +246 -0
  29. data/lib/type_guessr/core/converter/literal_converter.rb +217 -0
  30. data/lib/type_guessr/core/converter/prism_converter.rb +154 -1613
  31. data/lib/type_guessr/core/converter/rbs_converter.rb +35 -14
  32. data/lib/type_guessr/core/converter/registration.rb +100 -0
  33. data/lib/type_guessr/core/converter/variable_converter.rb +225 -0
  34. data/lib/type_guessr/core/converter.rb +4 -0
  35. data/lib/type_guessr/core/index/location_index.rb +32 -0
  36. data/lib/type_guessr/core/index.rb +3 -0
  37. data/lib/type_guessr/core/inference/resolver.rb +516 -349
  38. data/lib/type_guessr/core/inference.rb +4 -0
  39. data/lib/type_guessr/core/ir/nodes.rb +362 -103
  40. data/lib/type_guessr/core/ir.rb +3 -0
  41. data/lib/type_guessr/core/logger.rb +6 -13
  42. data/lib/type_guessr/core/node_context_helper.rb +126 -0
  43. data/lib/type_guessr/core/node_key_generator.rb +31 -0
  44. data/lib/type_guessr/core/registry/class_variable_registry.rb +63 -0
  45. data/lib/type_guessr/core/registry/instance_variable_registry.rb +84 -0
  46. data/lib/type_guessr/core/registry/method_registry.rb +65 -38
  47. data/lib/type_guessr/core/registry/signature_registry.rb +543 -0
  48. data/lib/type_guessr/core/registry.rb +6 -0
  49. data/lib/type_guessr/core/signature_builder.rb +39 -0
  50. data/lib/type_guessr/core/type_serializer.rb +96 -0
  51. data/lib/type_guessr/core/type_simplifier.rb +15 -12
  52. data/lib/type_guessr/core/types.rb +250 -32
  53. data/lib/type_guessr/core.rb +29 -0
  54. data/lib/type_guessr/mcp/file_watcher.rb +87 -0
  55. data/lib/type_guessr/mcp/server.rb +463 -0
  56. data/lib/type_guessr/mcp/standalone_runtime.rb +213 -0
  57. data/lib/type_guessr/version.rb +1 -1
  58. metadata +57 -8
  59. data/lib/type_guessr/core/rbs_provider.rb +0 -304
  60. data/lib/type_guessr/core/registry/variable_registry.rb +0 -87
  61. 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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dsl/activerecord_adapter"
@@ -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