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
@@ -1,12 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../node_key_generator"
4
+
3
5
  module TypeGuessr
4
6
  module Core
7
+ # Intermediate Representation (IR) nodes for type inference.
8
+ # Each node represents a construct in the source code and forms a
9
+ # reverse dependency graph where nodes point to their dependencies.
10
+ #
11
+ # Performance note: All node types use plain Class with positional arguments
12
+ # and attr_reader/attr_accessor. This avoids the overhead of keyword argument
13
+ # processing in CRuby's VM (setup_parameters_complex path), yielding the
14
+ # fastest possible object allocation. Loc (formerly Data.define(:offset)) is
15
+ # inlined as a plain Integer to eliminate ~2M wrapper object allocations
16
+ # during indexing.
5
17
  module IR
6
- # Location information for IR nodes
7
- # @param line [Integer] Line number (1-indexed)
8
- # @param col_range [Range] Column range
9
- Loc = Data.define(:line, :col_range)
18
+ # Shortcut to NodeKeyGenerator for generating node hash keys
19
+ NodeKeyGenerator = Core::NodeKeyGenerator
20
+
21
+ # Extract the last segment of a class/module path without array allocation.
22
+ # Uses String#rindex instead of String#split to avoid creating intermediate arrays.
23
+ # @param path [String] Class path (e.g., "Admin::User", "TypeGuessr::Core::IR::LiteralNode")
24
+ # @return [String, nil] Last segment (e.g., "User", "LiteralNode"), the path itself if no "::", or nil if empty
25
+ def self.extract_last_name(path)
26
+ return nil if path.nil? || path.empty?
27
+
28
+ last_sep = path.rindex("::")
29
+ last_sep ? path[(last_sep + 2)..] : path
30
+ end
31
+
32
+ # Method call signature for duck typing inference
33
+ # @param name [Symbol] Method name
34
+ # @param positional_count [Integer, nil] Number of positional arguments (nil if splat used)
35
+ # @param keywords [Array<Symbol>] Keyword argument names
36
+ CalledMethod = Data.define(:name, :positional_count, :keywords) do
37
+ # String representation returns method name for logging/display
38
+ def to_s
39
+ name.to_s
40
+ end
41
+ end
10
42
 
11
43
  # Pretty print helper for IR nodes (Prism-style tree output)
12
44
  module TreeInspect
@@ -17,23 +49,21 @@ module TypeGuessr
17
49
 
18
50
  def tree_inspect(indent: "", last: true, root: false)
19
51
  lines = if root
20
- ["@ #{self.class.name.split("::").last} (location: #{format_loc})"]
52
+ ["@ #{IR.extract_last_name(self.class.name)} (location: #{format_loc})"]
21
53
  else
22
54
  prefix = last ? LAST_BRANCH : BRANCH
23
55
  indent += (last ? SPACE : PIPE)
24
- ["#{indent.delete_suffix(last ? SPACE : PIPE)}#{prefix}@ #{self.class.name.split("::").last} (location: #{format_loc})"]
56
+ ["#{indent.delete_suffix(last ? SPACE : PIPE)}#{prefix}@ #{IR.extract_last_name(self.class.name)} (location: #{format_loc})"]
25
57
  end
26
58
  lines.concat(tree_inspect_fields(indent))
27
59
  lines.join("\n")
28
60
  end
29
61
 
30
- private
31
-
32
- def format_loc
33
- loc ? "(#{loc.line},#{loc.col_range.begin})-(#{loc.line},#{loc.col_range.end})" : "∅"
62
+ private def format_loc
63
+ loc ? "@#{loc}" : "∅"
34
64
  end
35
65
 
36
- def tree_field(name, value, indent, last: false)
66
+ private def tree_field(name, value, indent, last: false)
37
67
  prefix = last ? LAST_BRANCH : BRANCH
38
68
  case value
39
69
  when nil
@@ -69,59 +99,37 @@ module TypeGuessr
69
99
  end
70
100
  end
71
101
 
72
- def tree_inspect_fields(_indent)
102
+ private def tree_inspect_fields(_indent)
73
103
  []
74
104
  end
75
105
  end
76
106
 
77
- # Base class for all IR nodes
78
- # IR represents a reverse dependency graph where each node points to nodes it depends on
79
- class Node
80
- attr_reader :loc
81
-
82
- def initialize(loc:)
83
- @loc = loc
84
- end
85
-
86
- # Returns all nodes that this node directly depends on
87
- # @return [Array<Node>]
88
- def dependencies
89
- []
90
- end
91
-
92
- # Generate a unique hash for this node (type + identifier + line)
93
- # @return [String]
94
- def node_hash
95
- raise NotImplementedError, "#{self.class} must implement node_hash"
96
- end
97
-
98
- # Generate a unique key for this node (scope_id + node_hash)
99
- # @param scope_id [String] The scope identifier (e.g., "User#save")
100
- # @return [String]
101
- def node_key(scope_id)
102
- "#{scope_id}:#{node_hash}"
103
- end
104
- end
105
-
106
107
  # Literal value node
107
108
  # @param type [TypeGuessr::Core::Types::Type] The type of the literal
108
109
  # @param literal_value [Object, nil] The actual literal value (for Symbol, Integer, String)
109
110
  # @param values [Array<Node>, nil] Internal value nodes for compound literals (Hash/Array)
110
- # @param loc [Loc] Location information
111
- #
112
- # Examples: "hello" → String, 123 → Integer, [] → Array, {} → Hash
113
- # For compound literals, values contains the internal expression nodes
114
- # For simple literals, literal_value stores the actual value (e.g., :a for symbols)
115
- LiteralNode = Data.define(:type, :literal_value, :values, :loc) do
111
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
112
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
113
+ class LiteralNode
116
114
  include TreeInspect
117
115
 
116
+ attr_reader :type, :literal_value, :values, :called_methods, :loc
117
+
118
+ def initialize(type, literal_value, values, called_methods, loc)
119
+ @type = type
120
+ @literal_value = literal_value
121
+ @values = values
122
+ @called_methods = called_methods
123
+ @loc = loc
124
+ end
125
+
118
126
  def dependencies
119
127
  values || []
120
128
  end
121
129
 
122
130
  def node_hash
123
- type_name = type.is_a?(Class) ? type.name.split("::").last : type.class.name.split("::").last
124
- "lit:#{type_name}:#{loc&.line}"
131
+ type_name = type.is_a?(Class) ? IR.extract_last_name(type.name) : IR.extract_last_name(type.class.name)
132
+ NodeKeyGenerator.literal(type_name, loc)
125
133
  end
126
134
 
127
135
  def node_key(scope_id)
@@ -129,11 +137,12 @@ module TypeGuessr
129
137
  end
130
138
 
131
139
  def tree_inspect_fields(indent)
132
- type_str = type.respond_to?(:name) ? type.name : type.class.name.split("::").last
140
+ type_str = type.respond_to?(:name) ? type.name : IR.extract_last_name(type.class.name)
133
141
  [
134
142
  tree_field(:type, type_str, indent),
135
143
  tree_field(:literal_value, literal_value, indent),
136
- tree_field(:values, values, indent, last: true),
144
+ tree_field(:values, values, indent),
145
+ tree_field(:called_methods, called_methods, indent, last: true),
137
146
  ]
138
147
  end
139
148
  end
@@ -142,18 +151,27 @@ module TypeGuessr
142
151
  # @param name [Symbol] Variable name
143
152
  # @param value [Node] The node of the assigned value
144
153
  # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
145
- # @param loc [Loc] Location information
154
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
146
155
  #
147
156
  # Note: called_methods is a shared array object that can be mutated during parsing
148
- LocalWriteNode = Data.define(:name, :value, :called_methods, :loc) do
157
+ class LocalWriteNode
149
158
  include TreeInspect
150
159
 
160
+ attr_reader :name, :value, :called_methods, :loc
161
+
162
+ def initialize(name, value, called_methods, loc)
163
+ @name = name
164
+ @value = value
165
+ @called_methods = called_methods
166
+ @loc = loc
167
+ end
168
+
151
169
  def dependencies
152
170
  value ? [value] : []
153
171
  end
154
172
 
155
173
  def node_hash
156
- "local_write:#{name}:#{loc&.line}"
174
+ NodeKeyGenerator.local_write(name, loc)
157
175
  end
158
176
 
159
177
  def node_key(scope_id)
@@ -173,18 +191,27 @@ module TypeGuessr
173
191
  # @param name [Symbol] Variable name
174
192
  # @param write_node [LocalWriteNode, nil] The LocalWriteNode this read references
175
193
  # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
176
- # @param loc [Loc] Location information
194
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
177
195
  #
178
196
  # Note: called_methods is shared with LocalWriteNode for method-based inference propagation
179
- LocalReadNode = Data.define(:name, :write_node, :called_methods, :loc) do
197
+ class LocalReadNode
180
198
  include TreeInspect
181
199
 
200
+ attr_reader :name, :write_node, :called_methods, :loc
201
+
202
+ def initialize(name, write_node, called_methods, loc)
203
+ @name = name
204
+ @write_node = write_node
205
+ @called_methods = called_methods
206
+ @loc = loc
207
+ end
208
+
182
209
  def dependencies
183
210
  write_node ? [write_node] : []
184
211
  end
185
212
 
186
213
  def node_hash
187
- "local_read:#{name}:#{loc&.line}"
214
+ NodeKeyGenerator.local_read(name, loc)
188
215
  end
189
216
 
190
217
  def node_key(scope_id)
@@ -205,16 +232,26 @@ module TypeGuessr
205
232
  # @param class_name [String, nil] Enclosing class name for deferred resolution
206
233
  # @param value [Node] The node of the assigned value
207
234
  # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
208
- # @param loc [Loc] Location information
209
- InstanceVariableWriteNode = Data.define(:name, :class_name, :value, :called_methods, :loc) do
235
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
236
+ class InstanceVariableWriteNode
210
237
  include TreeInspect
211
238
 
239
+ attr_reader :name, :class_name, :value, :called_methods, :loc
240
+
241
+ def initialize(name, class_name, value, called_methods, loc)
242
+ @name = name
243
+ @class_name = class_name
244
+ @value = value
245
+ @called_methods = called_methods
246
+ @loc = loc
247
+ end
248
+
212
249
  def dependencies
213
250
  value ? [value] : []
214
251
  end
215
252
 
216
253
  def node_hash
217
- "ivar_write:#{name}:#{loc&.line}"
254
+ NodeKeyGenerator.ivar_write(name, loc)
218
255
  end
219
256
 
220
257
  def node_key(scope_id)
@@ -236,19 +273,29 @@ module TypeGuessr
236
273
  # @param class_name [String, nil] Enclosing class name for deferred resolution
237
274
  # @param write_node [InstanceVariableWriteNode, nil] The write node this read references
238
275
  # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
239
- # @param loc [Loc] Location information
276
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
240
277
  #
241
278
  # Note: write_node may be nil at conversion time if assignment appears later.
242
279
  # Resolver performs deferred lookup using class_name.
243
- InstanceVariableReadNode = Data.define(:name, :class_name, :write_node, :called_methods, :loc) do
280
+ class InstanceVariableReadNode
244
281
  include TreeInspect
245
282
 
283
+ attr_reader :name, :class_name, :write_node, :called_methods, :loc
284
+
285
+ def initialize(name, class_name, write_node, called_methods, loc)
286
+ @name = name
287
+ @class_name = class_name
288
+ @write_node = write_node
289
+ @called_methods = called_methods
290
+ @loc = loc
291
+ end
292
+
246
293
  def dependencies
247
294
  write_node ? [write_node] : []
248
295
  end
249
296
 
250
297
  def node_hash
251
- "ivar_read:#{name}:#{loc&.line}"
298
+ NodeKeyGenerator.ivar_read(name, loc)
252
299
  end
253
300
 
254
301
  def node_key(scope_id)
@@ -270,16 +317,26 @@ module TypeGuessr
270
317
  # @param class_name [String, nil] Enclosing class name for deferred resolution
271
318
  # @param value [Node] The node of the assigned value
272
319
  # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
273
- # @param loc [Loc] Location information
274
- ClassVariableWriteNode = Data.define(:name, :class_name, :value, :called_methods, :loc) do
320
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
321
+ class ClassVariableWriteNode
275
322
  include TreeInspect
276
323
 
324
+ attr_reader :name, :class_name, :value, :called_methods, :loc
325
+
326
+ def initialize(name, class_name, value, called_methods, loc)
327
+ @name = name
328
+ @class_name = class_name
329
+ @value = value
330
+ @called_methods = called_methods
331
+ @loc = loc
332
+ end
333
+
277
334
  def dependencies
278
335
  value ? [value] : []
279
336
  end
280
337
 
281
338
  def node_hash
282
- "cvar_write:#{name}:#{loc&.line}"
339
+ NodeKeyGenerator.cvar_write(name, loc)
283
340
  end
284
341
 
285
342
  def node_key(scope_id)
@@ -301,16 +358,26 @@ module TypeGuessr
301
358
  # @param class_name [String, nil] Enclosing class name for deferred resolution
302
359
  # @param write_node [ClassVariableWriteNode, nil] The write node this read references
303
360
  # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
304
- # @param loc [Loc] Location information
305
- ClassVariableReadNode = Data.define(:name, :class_name, :write_node, :called_methods, :loc) do
361
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
362
+ class ClassVariableReadNode
306
363
  include TreeInspect
307
364
 
365
+ attr_reader :name, :class_name, :write_node, :called_methods, :loc
366
+
367
+ def initialize(name, class_name, write_node, called_methods, loc)
368
+ @name = name
369
+ @class_name = class_name
370
+ @write_node = write_node
371
+ @called_methods = called_methods
372
+ @loc = loc
373
+ end
374
+
308
375
  def dependencies
309
376
  write_node ? [write_node] : []
310
377
  end
311
378
 
312
379
  def node_hash
313
- "cvar_read:#{name}:#{loc&.line}"
380
+ NodeKeyGenerator.cvar_read(name, loc)
314
381
  end
315
382
 
316
383
  def node_key(scope_id)
@@ -333,18 +400,28 @@ module TypeGuessr
333
400
  # :keyword_optional, :keyword_rest, :block, :forwarding)
334
401
  # @param default_value [Node, nil] Default value node (nil if no default)
335
402
  # @param called_methods [Array<Symbol>] Methods called on this parameter (for method-based inference)
336
- # @param loc [Loc] Location information
403
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
337
404
  #
338
405
  # Note: called_methods is a shared array object that can be mutated during parsing
339
- ParamNode = Data.define(:name, :kind, :default_value, :called_methods, :loc) do
406
+ class ParamNode
340
407
  include TreeInspect
341
408
 
409
+ attr_reader :name, :kind, :default_value, :called_methods, :loc
410
+
411
+ def initialize(name, kind, default_value, called_methods, loc)
412
+ @name = name
413
+ @kind = kind
414
+ @default_value = default_value
415
+ @called_methods = called_methods
416
+ @loc = loc
417
+ end
418
+
342
419
  def dependencies
343
420
  default_value ? [default_value] : []
344
421
  end
345
422
 
346
423
  def node_hash
347
- "param:#{name}:#{loc&.line}"
424
+ NodeKeyGenerator.param(name, loc)
348
425
  end
349
426
 
350
427
  def node_key(scope_id)
@@ -364,16 +441,26 @@ module TypeGuessr
364
441
  # Constant reference node
365
442
  # @param name [String] Constant name (e.g., "DEFAULT_NAME", "User::ADMIN")
366
443
  # @param dependency [Node] The node where this constant is defined
367
- # @param loc [Loc] Location information
368
- ConstantNode = Data.define(:name, :dependency, :loc) do
444
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
445
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
446
+ class ConstantNode
369
447
  include TreeInspect
370
448
 
449
+ attr_reader :name, :dependency, :called_methods, :loc
450
+
451
+ def initialize(name, dependency, called_methods, loc)
452
+ @name = name
453
+ @dependency = dependency
454
+ @called_methods = called_methods
455
+ @loc = loc
456
+ end
457
+
371
458
  def dependencies
372
459
  dependency ? [dependency] : []
373
460
  end
374
461
 
375
462
  def node_hash
376
- "const:#{name}:#{loc&.line}"
463
+ NodeKeyGenerator.constant(name, loc)
377
464
  end
378
465
 
379
466
  def node_key(scope_id)
@@ -383,7 +470,8 @@ module TypeGuessr
383
470
  def tree_inspect_fields(indent)
384
471
  [
385
472
  tree_field(:name, name, indent),
386
- tree_field(:dependency, dependency, indent, last: true),
473
+ tree_field(:dependency, dependency, indent),
474
+ tree_field(:called_methods, called_methods, indent, last: true),
387
475
  ]
388
476
  end
389
477
  end
@@ -395,10 +483,25 @@ module TypeGuessr
395
483
  # @param block_params [Array<BlockParamSlot>] Block parameter slots
396
484
  # @param block_body [Node, nil] Block body return node (for inferring block return type)
397
485
  # @param has_block [Boolean] Whether a block was provided (even if empty)
398
- # @param loc [Loc] Location information
399
- CallNode = Data.define(:method, :receiver, :args, :block_params, :block_body, :has_block, :loc) do
486
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
487
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
488
+ class CallNode
400
489
  include TreeInspect
401
490
 
491
+ attr_reader :method, :receiver, :args, :called_methods, :loc
492
+ attr_accessor :block_params, :block_body, :has_block
493
+
494
+ def initialize(method, receiver, args, block_params, block_body, has_block, called_methods, loc)
495
+ @method = method
496
+ @receiver = receiver
497
+ @args = args
498
+ @block_params = block_params
499
+ @block_body = block_body
500
+ @has_block = has_block
501
+ @called_methods = called_methods
502
+ @loc = loc
503
+ end
504
+
402
505
  def dependencies
403
506
  deps = []
404
507
  deps << receiver if receiver
@@ -408,7 +511,7 @@ module TypeGuessr
408
511
  end
409
512
 
410
513
  def node_hash
411
- "call:#{method}:#{loc&.line}"
514
+ NodeKeyGenerator.call(method, loc)
412
515
  end
413
516
 
414
517
  def node_key(scope_id)
@@ -422,7 +525,8 @@ module TypeGuessr
422
525
  tree_field(:args, args, indent),
423
526
  tree_field(:block_params, block_params, indent),
424
527
  tree_field(:block_body, block_body, indent),
425
- tree_field(:has_block, has_block, indent, last: true),
528
+ tree_field(:has_block, has_block, indent),
529
+ tree_field(:called_methods, called_methods, indent, last: true),
426
530
  ]
427
531
  end
428
532
  end
@@ -431,16 +535,26 @@ module TypeGuessr
431
535
  # Represents a parameter slot in a block (e.g., |user| in users.each { |user| ... })
432
536
  # @param index [Integer] Parameter index (0-based)
433
537
  # @param call_node [CallNode] The call node this slot belongs to
434
- # @param loc [Loc] Location information for the parameter itself
435
- BlockParamSlot = Data.define(:index, :call_node, :loc) do
538
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
539
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
540
+ class BlockParamSlot
436
541
  include TreeInspect
437
542
 
543
+ attr_reader :index, :call_node, :called_methods, :loc
544
+
545
+ def initialize(index, call_node, called_methods, loc)
546
+ @index = index
547
+ @call_node = call_node
548
+ @called_methods = called_methods
549
+ @loc = loc
550
+ end
551
+
438
552
  def dependencies
439
553
  [call_node]
440
554
  end
441
555
 
442
556
  def node_hash
443
- "bparam:#{index}:#{loc&.line}"
557
+ NodeKeyGenerator.bparam(index, loc)
444
558
  end
445
559
 
446
560
  def node_key(scope_id)
@@ -450,7 +564,8 @@ module TypeGuessr
450
564
  def tree_inspect_fields(indent)
451
565
  [
452
566
  tree_field(:index, index, indent),
453
- tree_field(:call_node, "(CallNode ref)", indent, last: true),
567
+ tree_field(:call_node, "(CallNode ref)", indent),
568
+ tree_field(:called_methods, called_methods, indent, last: true),
454
569
  ]
455
570
  end
456
571
  end
@@ -459,16 +574,25 @@ module TypeGuessr
459
574
  # Represents the convergence point of multiple branches (if/else, case/when, etc.)
460
575
  # The type is the union of all branch types
461
576
  # @param branches [Array<Node>] Final nodes from each branch
462
- # @param loc [Loc] Location information
463
- MergeNode = Data.define(:branches, :loc) do
577
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
578
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
579
+ class MergeNode
464
580
  include TreeInspect
465
581
 
582
+ attr_reader :branches, :called_methods, :loc
583
+
584
+ def initialize(branches, called_methods, loc)
585
+ @branches = branches
586
+ @called_methods = called_methods
587
+ @loc = loc
588
+ end
589
+
466
590
  def dependencies
467
591
  branches
468
592
  end
469
593
 
470
594
  def node_hash
471
- "merge:#{loc&.line}"
595
+ NodeKeyGenerator.merge(loc)
472
596
  end
473
597
 
474
598
  def node_key(scope_id)
@@ -476,7 +600,50 @@ module TypeGuessr
476
600
  end
477
601
 
478
602
  def tree_inspect_fields(indent)
479
- [tree_field(:branches, branches, indent, last: true)]
603
+ [
604
+ tree_field(:branches, branches, indent),
605
+ tree_field(:called_methods, called_methods, indent, last: true),
606
+ ]
607
+ end
608
+ end
609
+
610
+ # Short-circuit or node
611
+ # Represents || and ||= operations where LHS is evaluated first,
612
+ # and RHS is only evaluated if LHS is falsy (nil/false)
613
+ # @param lhs [Node] Left-hand side (evaluated first)
614
+ # @param rhs [Node] Right-hand side (evaluated only if LHS is falsy)
615
+ # @param called_methods [Array<CalledMethod>] Methods called on this node
616
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
617
+ class OrNode
618
+ include TreeInspect
619
+
620
+ attr_reader :lhs, :rhs, :called_methods, :loc
621
+
622
+ def initialize(lhs, rhs, called_methods, loc)
623
+ @lhs = lhs
624
+ @rhs = rhs
625
+ @called_methods = called_methods
626
+ @loc = loc
627
+ end
628
+
629
+ def dependencies
630
+ [lhs, rhs]
631
+ end
632
+
633
+ def node_hash
634
+ NodeKeyGenerator.or_node(loc)
635
+ end
636
+
637
+ def node_key(scope_id)
638
+ "#{scope_id}:#{node_hash}"
639
+ end
640
+
641
+ def tree_inspect_fields(indent)
642
+ [
643
+ tree_field(:lhs, lhs, indent),
644
+ tree_field(:rhs, rhs, indent),
645
+ tree_field(:called_methods, called_methods, indent, last: true),
646
+ ]
480
647
  end
481
648
  end
482
649
 
@@ -486,11 +653,28 @@ module TypeGuessr
486
653
  # @param params [Array<ParamNode>] Parameter nodes
487
654
  # @param return_node [Node] Node representing the return value
488
655
  # @param body_nodes [Array<Node>] All nodes in the method body
489
- # @param loc [Loc] Location information
656
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
657
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
490
658
  # @param singleton [Boolean] true if this is a singleton method (def self.method_name)
491
- DefNode = Data.define(:name, :class_name, :params, :return_node, :body_nodes, :loc, :singleton) do
659
+ class DefNode
492
660
  include TreeInspect
493
661
 
662
+ attr_reader :name, :class_name, :params, :return_node, :body_nodes, :called_methods, :loc, :singleton,
663
+ :module_function
664
+
665
+ def initialize(name, class_name, params, return_node, body_nodes, called_methods, loc, singleton,
666
+ module_function: false)
667
+ @name = name
668
+ @class_name = class_name
669
+ @params = params
670
+ @return_node = return_node
671
+ @body_nodes = body_nodes
672
+ @called_methods = called_methods
673
+ @loc = loc
674
+ @singleton = singleton
675
+ @module_function = module_function
676
+ end
677
+
494
678
  def dependencies
495
679
  deps = params.dup
496
680
  deps << return_node if return_node
@@ -499,7 +683,7 @@ module TypeGuessr
499
683
  end
500
684
 
501
685
  def node_hash
502
- "def:#{name}:#{loc&.line}"
686
+ NodeKeyGenerator.def_node(name, loc)
503
687
  end
504
688
 
505
689
  def node_key(scope_id)
@@ -513,6 +697,7 @@ module TypeGuessr
513
697
  tree_field(:params, params, indent),
514
698
  tree_field(:return_node, return_node, indent),
515
699
  tree_field(:body_nodes, body_nodes, indent),
700
+ tree_field(:called_methods, called_methods, indent),
516
701
  tree_field(:singleton, singleton, indent, last: true),
517
702
  ]
518
703
  end
@@ -521,16 +706,26 @@ module TypeGuessr
521
706
  # Class/Module node - container for methods and other definitions
522
707
  # @param name [String] Class or module name
523
708
  # @param methods [Array<DefNode>] Method definitions in this class/module
524
- # @param loc [Loc] Location information
525
- ClassModuleNode = Data.define(:name, :methods, :loc) do
709
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
710
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
711
+ class ClassModuleNode
526
712
  include TreeInspect
527
713
 
714
+ attr_reader :name, :methods, :called_methods, :loc
715
+
716
+ def initialize(name, methods, called_methods, loc)
717
+ @name = name
718
+ @methods = methods
719
+ @called_methods = called_methods
720
+ @loc = loc
721
+ end
722
+
528
723
  def dependencies
529
724
  methods
530
725
  end
531
726
 
532
727
  def node_hash
533
- "class:#{name}:#{loc&.line}"
728
+ NodeKeyGenerator.class_module(name, loc)
534
729
  end
535
730
 
536
731
  def node_key(scope_id)
@@ -540,7 +735,8 @@ module TypeGuessr
540
735
  def tree_inspect_fields(indent)
541
736
  [
542
737
  tree_field(:name, name, indent),
543
- tree_field(:methods, methods, indent, last: true),
738
+ tree_field(:methods, methods, indent),
739
+ tree_field(:called_methods, called_methods, indent, last: true),
544
740
  ]
545
741
  end
546
742
  end
@@ -548,16 +744,26 @@ module TypeGuessr
548
744
  # Self reference node
549
745
  # @param class_name [String] Name of the enclosing class/module
550
746
  # @param singleton [Boolean] Whether this self is in a singleton method context
551
- # @param loc [Loc] Location information
552
- SelfNode = Data.define(:class_name, :singleton, :loc) do
747
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
748
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
749
+ class SelfNode
553
750
  include TreeInspect
554
751
 
752
+ attr_reader :class_name, :singleton, :called_methods, :loc
753
+
754
+ def initialize(class_name, singleton, called_methods, loc)
755
+ @class_name = class_name
756
+ @singleton = singleton
757
+ @called_methods = called_methods
758
+ @loc = loc
759
+ end
760
+
555
761
  def dependencies
556
762
  []
557
763
  end
558
764
 
559
765
  def node_hash
560
- "self:#{class_name}:#{loc&.line}"
766
+ NodeKeyGenerator.self_node(class_name, loc)
561
767
  end
562
768
 
563
769
  def node_key(scope_id)
@@ -567,23 +773,73 @@ module TypeGuessr
567
773
  def tree_inspect_fields(indent)
568
774
  [
569
775
  tree_field(:class_name, class_name, indent),
570
- tree_field(:singleton, singleton, indent, last: true),
776
+ tree_field(:singleton, singleton, indent),
777
+ tree_field(:called_methods, called_methods, indent, last: true),
778
+ ]
779
+ end
780
+ end
781
+
782
+ # Type narrowing wrapper node
783
+ # Wraps a value node and signals that falsy types should be removed
784
+ # Used after guard clauses: `return unless x` → x is truthy after guard
785
+ # @param value [Node] The original node to narrow
786
+ # @param kind [Symbol] Narrowing kind (:truthy removes nil/false)
787
+ # @param called_methods [Array<CalledMethod>] Methods called on this node
788
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
789
+ class NarrowNode
790
+ include TreeInspect
791
+
792
+ attr_reader :value, :kind, :called_methods, :loc
793
+
794
+ def initialize(value, kind, called_methods, loc)
795
+ @value = value
796
+ @kind = kind
797
+ @called_methods = called_methods
798
+ @loc = loc
799
+ end
800
+
801
+ def dependencies
802
+ value ? [value] : []
803
+ end
804
+
805
+ def node_hash
806
+ NodeKeyGenerator.narrow(kind, loc)
807
+ end
808
+
809
+ def node_key(scope_id)
810
+ "#{scope_id}:#{node_hash}"
811
+ end
812
+
813
+ def tree_inspect_fields(indent)
814
+ [
815
+ tree_field(:value, value, indent),
816
+ tree_field(:kind, kind, indent),
817
+ tree_field(:called_methods, called_methods, indent, last: true),
571
818
  ]
572
819
  end
573
820
  end
574
821
 
575
822
  # Explicit return statement node
576
823
  # @param value [Node, nil] Return value node (nil for bare `return`)
577
- # @param loc [Loc] Location information
578
- ReturnNode = Data.define(:value, :loc) do
824
+ # @param called_methods [Array<CalledMethod>] Methods called on this node (for method-based inference)
825
+ # @param loc [Integer, nil] Byte offset from start of file (0-indexed)
826
+ class ReturnNode
579
827
  include TreeInspect
580
828
 
829
+ attr_reader :value, :called_methods, :loc
830
+
831
+ def initialize(value, called_methods, loc)
832
+ @value = value
833
+ @called_methods = called_methods
834
+ @loc = loc
835
+ end
836
+
581
837
  def dependencies
582
838
  value ? [value] : []
583
839
  end
584
840
 
585
841
  def node_hash
586
- "return:#{loc&.line}"
842
+ NodeKeyGenerator.return_node(loc)
587
843
  end
588
844
 
589
845
  def node_key(scope_id)
@@ -591,7 +847,10 @@ module TypeGuessr
591
847
  end
592
848
 
593
849
  def tree_inspect_fields(indent)
594
- [tree_field(:value, value, indent, last: true)]
850
+ [
851
+ tree_field(:value, value, indent),
852
+ tree_field(:called_methods, called_methods, indent, last: true),
853
+ ]
595
854
  end
596
855
  end
597
856
  end