type-guessr 0.0.1

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.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../types"
4
+
5
+ module TypeGuessr
6
+ module Core
7
+ module Inference
8
+ # Represents the result of type inference with reasoning
9
+ # Contains the inferred type and why it was inferred
10
+ class Result
11
+ attr_reader :type, :reason, :source
12
+
13
+ # @param type [Types::Type] The inferred type
14
+ # @param reason [String] Why this type was inferred
15
+ # @param source [Symbol] Source of the type (:gem, :project, :stdlib, :literal, :unknown)
16
+ def initialize(type, reason, source = :unknown)
17
+ @type = type
18
+ @reason = reason
19
+ @source = source
20
+ end
21
+
22
+ def ==(other)
23
+ other.is_a?(Result) &&
24
+ type == other.type &&
25
+ reason == other.reason &&
26
+ source == other.source
27
+ end
28
+
29
+ alias eql? ==
30
+
31
+ def hash
32
+ [type, reason, source].hash
33
+ end
34
+
35
+ def to_s
36
+ "#{type} (#{reason})"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,599 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TypeGuessr
4
+ module Core
5
+ 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)
10
+
11
+ # Pretty print helper for IR nodes (Prism-style tree output)
12
+ module TreeInspect
13
+ BRANCH = "├── "
14
+ LAST_BRANCH = "└── "
15
+ PIPE = "│ "
16
+ SPACE = " "
17
+
18
+ def tree_inspect(indent: "", last: true, root: false)
19
+ lines = if root
20
+ ["@ #{self.class.name.split("::").last} (location: #{format_loc})"]
21
+ else
22
+ prefix = last ? LAST_BRANCH : BRANCH
23
+ indent += (last ? SPACE : PIPE)
24
+ ["#{indent.delete_suffix(last ? SPACE : PIPE)}#{prefix}@ #{self.class.name.split("::").last} (location: #{format_loc})"]
25
+ end
26
+ lines.concat(tree_inspect_fields(indent))
27
+ lines.join("\n")
28
+ end
29
+
30
+ private
31
+
32
+ def format_loc
33
+ loc ? "(#{loc.line},#{loc.col_range.begin})-(#{loc.line},#{loc.col_range.end})" : "∅"
34
+ end
35
+
36
+ def tree_field(name, value, indent, last: false)
37
+ prefix = last ? LAST_BRANCH : BRANCH
38
+ case value
39
+ when nil
40
+ "#{indent}#{prefix}#{name}: ∅"
41
+ when Array
42
+ if value.empty?
43
+ "#{indent}#{prefix}#{name}: (length: 0)"
44
+ else
45
+ lines = ["#{indent}#{prefix}#{name}: (length: #{value.size})"]
46
+ value.each_with_index do |item, idx|
47
+ is_last = idx == value.size - 1
48
+ item_indent = indent + (last ? SPACE : PIPE)
49
+ if item.is_a?(TreeInspect)
50
+ lines << item.tree_inspect(indent: item_indent, last: is_last)
51
+ else
52
+ item_prefix = is_last ? LAST_BRANCH : BRANCH
53
+ lines << "#{item_indent}#{item_prefix}#{item.inspect}"
54
+ end
55
+ end
56
+ lines.join("\n")
57
+ end
58
+ when Symbol, String, Integer, TrueClass, FalseClass
59
+ "#{indent}#{prefix}#{name}: #{value.inspect}"
60
+ else
61
+ if value.is_a?(TreeInspect)
62
+ lines = ["#{indent}#{prefix}#{name}:"]
63
+ child_indent = indent + (last ? SPACE : PIPE)
64
+ lines << value.tree_inspect(indent: child_indent, last: true)
65
+ lines.join("\n")
66
+ else
67
+ "#{indent}#{prefix}#{name}: #{value.inspect}"
68
+ end
69
+ end
70
+ end
71
+
72
+ def tree_inspect_fields(_indent)
73
+ []
74
+ end
75
+ end
76
+
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
+ # Literal value node
107
+ # @param type [TypeGuessr::Core::Types::Type] The type of the literal
108
+ # @param literal_value [Object, nil] The actual literal value (for Symbol, Integer, String)
109
+ # @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
116
+ include TreeInspect
117
+
118
+ def dependencies
119
+ values || []
120
+ end
121
+
122
+ 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}"
125
+ end
126
+
127
+ def node_key(scope_id)
128
+ "#{scope_id}:#{node_hash}"
129
+ end
130
+
131
+ def tree_inspect_fields(indent)
132
+ type_str = type.respond_to?(:name) ? type.name : type.class.name.split("::").last
133
+ [
134
+ tree_field(:type, type_str, indent),
135
+ tree_field(:literal_value, literal_value, indent),
136
+ tree_field(:values, values, indent, last: true),
137
+ ]
138
+ end
139
+ end
140
+
141
+ # Local variable write node (assignment)
142
+ # @param name [Symbol] Variable name
143
+ # @param value [Node] The node of the assigned value
144
+ # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
145
+ # @param loc [Loc] Location information
146
+ #
147
+ # Note: called_methods is a shared array object that can be mutated during parsing
148
+ LocalWriteNode = Data.define(:name, :value, :called_methods, :loc) do
149
+ include TreeInspect
150
+
151
+ def dependencies
152
+ value ? [value] : []
153
+ end
154
+
155
+ def node_hash
156
+ "local_write:#{name}:#{loc&.line}"
157
+ end
158
+
159
+ def node_key(scope_id)
160
+ "#{scope_id}:#{node_hash}"
161
+ end
162
+
163
+ def tree_inspect_fields(indent)
164
+ [
165
+ tree_field(:name, name, indent),
166
+ tree_field(:value, value, indent),
167
+ tree_field(:called_methods, called_methods, indent, last: true),
168
+ ]
169
+ end
170
+ end
171
+
172
+ # Local variable read node (reference)
173
+ # @param name [Symbol] Variable name
174
+ # @param write_node [LocalWriteNode, nil] The LocalWriteNode this read references
175
+ # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
176
+ # @param loc [Loc] Location information
177
+ #
178
+ # Note: called_methods is shared with LocalWriteNode for method-based inference propagation
179
+ LocalReadNode = Data.define(:name, :write_node, :called_methods, :loc) do
180
+ include TreeInspect
181
+
182
+ def dependencies
183
+ write_node ? [write_node] : []
184
+ end
185
+
186
+ def node_hash
187
+ "local_read:#{name}:#{loc&.line}"
188
+ end
189
+
190
+ def node_key(scope_id)
191
+ "#{scope_id}:#{node_hash}"
192
+ end
193
+
194
+ def tree_inspect_fields(indent)
195
+ [
196
+ tree_field(:name, name, indent),
197
+ tree_field(:write_node, write_node, indent),
198
+ tree_field(:called_methods, called_methods, indent, last: true),
199
+ ]
200
+ end
201
+ end
202
+
203
+ # Instance variable write node (@name = value)
204
+ # @param name [Symbol] Variable name (e.g., :@recipe)
205
+ # @param class_name [String, nil] Enclosing class name for deferred resolution
206
+ # @param value [Node] The node of the assigned value
207
+ # @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
210
+ include TreeInspect
211
+
212
+ def dependencies
213
+ value ? [value] : []
214
+ end
215
+
216
+ def node_hash
217
+ "ivar_write:#{name}:#{loc&.line}"
218
+ end
219
+
220
+ def node_key(scope_id)
221
+ "#{scope_id}:#{node_hash}"
222
+ end
223
+
224
+ def tree_inspect_fields(indent)
225
+ [
226
+ tree_field(:name, name, indent),
227
+ tree_field(:class_name, class_name, indent),
228
+ tree_field(:value, value, indent),
229
+ tree_field(:called_methods, called_methods, indent, last: true),
230
+ ]
231
+ end
232
+ end
233
+
234
+ # Instance variable read node (@name)
235
+ # @param name [Symbol] Variable name (e.g., :@recipe)
236
+ # @param class_name [String, nil] Enclosing class name for deferred resolution
237
+ # @param write_node [InstanceVariableWriteNode, nil] The write node this read references
238
+ # @param called_methods [Array<Symbol>] Methods called on this variable (for method-based inference)
239
+ # @param loc [Loc] Location information
240
+ #
241
+ # Note: write_node may be nil at conversion time if assignment appears later.
242
+ # Resolver performs deferred lookup using class_name.
243
+ InstanceVariableReadNode = Data.define(:name, :class_name, :write_node, :called_methods, :loc) do
244
+ include TreeInspect
245
+
246
+ def dependencies
247
+ write_node ? [write_node] : []
248
+ end
249
+
250
+ def node_hash
251
+ "ivar_read:#{name}:#{loc&.line}"
252
+ end
253
+
254
+ def node_key(scope_id)
255
+ "#{scope_id}:#{node_hash}"
256
+ end
257
+
258
+ def tree_inspect_fields(indent)
259
+ [
260
+ tree_field(:name, name, indent),
261
+ tree_field(:class_name, class_name, indent),
262
+ tree_field(:write_node, write_node, indent),
263
+ tree_field(:called_methods, called_methods, indent, last: true),
264
+ ]
265
+ end
266
+ end
267
+
268
+ # Class variable write node (@@name = value)
269
+ # @param name [Symbol] Variable name (e.g., :@@count)
270
+ # @param class_name [String, nil] Enclosing class name for deferred resolution
271
+ # @param value [Node] The node of the assigned value
272
+ # @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
275
+ include TreeInspect
276
+
277
+ def dependencies
278
+ value ? [value] : []
279
+ end
280
+
281
+ def node_hash
282
+ "cvar_write:#{name}:#{loc&.line}"
283
+ end
284
+
285
+ def node_key(scope_id)
286
+ "#{scope_id}:#{node_hash}"
287
+ end
288
+
289
+ def tree_inspect_fields(indent)
290
+ [
291
+ tree_field(:name, name, indent),
292
+ tree_field(:class_name, class_name, indent),
293
+ tree_field(:value, value, indent),
294
+ tree_field(:called_methods, called_methods, indent, last: true),
295
+ ]
296
+ end
297
+ end
298
+
299
+ # Class variable read node (@@name)
300
+ # @param name [Symbol] Variable name (e.g., :@@count)
301
+ # @param class_name [String, nil] Enclosing class name for deferred resolution
302
+ # @param write_node [ClassVariableWriteNode, nil] The write node this read references
303
+ # @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
306
+ include TreeInspect
307
+
308
+ def dependencies
309
+ write_node ? [write_node] : []
310
+ end
311
+
312
+ def node_hash
313
+ "cvar_read:#{name}:#{loc&.line}"
314
+ end
315
+
316
+ def node_key(scope_id)
317
+ "#{scope_id}:#{node_hash}"
318
+ end
319
+
320
+ def tree_inspect_fields(indent)
321
+ [
322
+ tree_field(:name, name, indent),
323
+ tree_field(:class_name, class_name, indent),
324
+ tree_field(:write_node, write_node, indent),
325
+ tree_field(:called_methods, called_methods, indent, last: true),
326
+ ]
327
+ end
328
+ end
329
+
330
+ # Method parameter node
331
+ # @param name [Symbol] Parameter name
332
+ # @param kind [Symbol] Parameter kind (:required, :optional, :rest, :keyword_required,
333
+ # :keyword_optional, :keyword_rest, :block, :forwarding)
334
+ # @param default_value [Node, nil] Default value node (nil if no default)
335
+ # @param called_methods [Array<Symbol>] Methods called on this parameter (for method-based inference)
336
+ # @param loc [Loc] Location information
337
+ #
338
+ # 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
340
+ include TreeInspect
341
+
342
+ def dependencies
343
+ default_value ? [default_value] : []
344
+ end
345
+
346
+ def node_hash
347
+ "param:#{name}:#{loc&.line}"
348
+ end
349
+
350
+ def node_key(scope_id)
351
+ "#{scope_id}:#{node_hash}"
352
+ end
353
+
354
+ def tree_inspect_fields(indent)
355
+ [
356
+ tree_field(:name, name, indent),
357
+ tree_field(:kind, kind, indent),
358
+ tree_field(:default_value, default_value, indent),
359
+ tree_field(:called_methods, called_methods, indent, last: true),
360
+ ]
361
+ end
362
+ end
363
+
364
+ # Constant reference node
365
+ # @param name [String] Constant name (e.g., "DEFAULT_NAME", "User::ADMIN")
366
+ # @param dependency [Node] The node where this constant is defined
367
+ # @param loc [Loc] Location information
368
+ ConstantNode = Data.define(:name, :dependency, :loc) do
369
+ include TreeInspect
370
+
371
+ def dependencies
372
+ dependency ? [dependency] : []
373
+ end
374
+
375
+ def node_hash
376
+ "const:#{name}:#{loc&.line}"
377
+ end
378
+
379
+ def node_key(scope_id)
380
+ "#{scope_id}:#{node_hash}"
381
+ end
382
+
383
+ def tree_inspect_fields(indent)
384
+ [
385
+ tree_field(:name, name, indent),
386
+ tree_field(:dependency, dependency, indent, last: true),
387
+ ]
388
+ end
389
+ end
390
+
391
+ # Method call node
392
+ # @param method [Symbol] Method name
393
+ # @param receiver [Node, nil] Receiver node (nil for implicit self)
394
+ # @param args [Array<Node>] Argument nodes
395
+ # @param block_params [Array<BlockParamSlot>] Block parameter slots
396
+ # @param block_body [Node, nil] Block body return node (for inferring block return type)
397
+ # @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
400
+ include TreeInspect
401
+
402
+ def dependencies
403
+ deps = []
404
+ deps << receiver if receiver
405
+ deps.concat(args)
406
+ deps << block_body if block_body
407
+ deps
408
+ end
409
+
410
+ def node_hash
411
+ "call:#{method}:#{loc&.line}"
412
+ end
413
+
414
+ def node_key(scope_id)
415
+ "#{scope_id}:#{node_hash}"
416
+ end
417
+
418
+ def tree_inspect_fields(indent)
419
+ [
420
+ tree_field(:method, method, indent),
421
+ tree_field(:receiver, receiver, indent),
422
+ tree_field(:args, args, indent),
423
+ tree_field(:block_params, block_params, indent),
424
+ tree_field(:block_body, block_body, indent),
425
+ tree_field(:has_block, has_block, indent, last: true),
426
+ ]
427
+ end
428
+ end
429
+
430
+ # Block parameter slot
431
+ # Represents a parameter slot in a block (e.g., |user| in users.each { |user| ... })
432
+ # @param index [Integer] Parameter index (0-based)
433
+ # @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
436
+ include TreeInspect
437
+
438
+ def dependencies
439
+ [call_node]
440
+ end
441
+
442
+ def node_hash
443
+ "bparam:#{index}:#{loc&.line}"
444
+ end
445
+
446
+ def node_key(scope_id)
447
+ "#{scope_id}:#{node_hash}"
448
+ end
449
+
450
+ def tree_inspect_fields(indent)
451
+ [
452
+ tree_field(:index, index, indent),
453
+ tree_field(:call_node, "(CallNode ref)", indent, last: true),
454
+ ]
455
+ end
456
+ end
457
+
458
+ # Branch merge node
459
+ # Represents the convergence point of multiple branches (if/else, case/when, etc.)
460
+ # The type is the union of all branch types
461
+ # @param branches [Array<Node>] Final nodes from each branch
462
+ # @param loc [Loc] Location information
463
+ MergeNode = Data.define(:branches, :loc) do
464
+ include TreeInspect
465
+
466
+ def dependencies
467
+ branches
468
+ end
469
+
470
+ def node_hash
471
+ "merge:#{loc&.line}"
472
+ end
473
+
474
+ def node_key(scope_id)
475
+ "#{scope_id}:#{node_hash}"
476
+ end
477
+
478
+ def tree_inspect_fields(indent)
479
+ [tree_field(:branches, branches, indent, last: true)]
480
+ end
481
+ end
482
+
483
+ # Method definition node
484
+ # @param name [Symbol] Method name
485
+ # @param class_name [String, nil] Enclosing class name
486
+ # @param params [Array<ParamNode>] Parameter nodes
487
+ # @param return_node [Node] Node representing the return value
488
+ # @param body_nodes [Array<Node>] All nodes in the method body
489
+ # @param loc [Loc] Location information
490
+ # @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
492
+ include TreeInspect
493
+
494
+ def dependencies
495
+ deps = params.dup
496
+ deps << return_node if return_node
497
+ deps.concat(body_nodes || [])
498
+ deps
499
+ end
500
+
501
+ def node_hash
502
+ "def:#{name}:#{loc&.line}"
503
+ end
504
+
505
+ def node_key(scope_id)
506
+ "#{scope_id}:#{node_hash}"
507
+ end
508
+
509
+ def tree_inspect_fields(indent)
510
+ [
511
+ tree_field(:name, name, indent),
512
+ tree_field(:class_name, class_name, indent),
513
+ tree_field(:params, params, indent),
514
+ tree_field(:return_node, return_node, indent),
515
+ tree_field(:body_nodes, body_nodes, indent),
516
+ tree_field(:singleton, singleton, indent, last: true),
517
+ ]
518
+ end
519
+ end
520
+
521
+ # Class/Module node - container for methods and other definitions
522
+ # @param name [String] Class or module name
523
+ # @param methods [Array<DefNode>] Method definitions in this class/module
524
+ # @param loc [Loc] Location information
525
+ ClassModuleNode = Data.define(:name, :methods, :loc) do
526
+ include TreeInspect
527
+
528
+ def dependencies
529
+ methods
530
+ end
531
+
532
+ def node_hash
533
+ "class:#{name}:#{loc&.line}"
534
+ end
535
+
536
+ def node_key(scope_id)
537
+ "#{scope_id}:#{node_hash}"
538
+ end
539
+
540
+ def tree_inspect_fields(indent)
541
+ [
542
+ tree_field(:name, name, indent),
543
+ tree_field(:methods, methods, indent, last: true),
544
+ ]
545
+ end
546
+ end
547
+
548
+ # Self reference node
549
+ # @param class_name [String] Name of the enclosing class/module
550
+ # @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
553
+ include TreeInspect
554
+
555
+ def dependencies
556
+ []
557
+ end
558
+
559
+ def node_hash
560
+ "self:#{class_name}:#{loc&.line}"
561
+ end
562
+
563
+ def node_key(scope_id)
564
+ "#{scope_id}:#{node_hash}"
565
+ end
566
+
567
+ def tree_inspect_fields(indent)
568
+ [
569
+ tree_field(:class_name, class_name, indent),
570
+ tree_field(:singleton, singleton, indent, last: true),
571
+ ]
572
+ end
573
+ end
574
+
575
+ # Explicit return statement node
576
+ # @param value [Node, nil] Return value node (nil for bare `return`)
577
+ # @param loc [Loc] Location information
578
+ ReturnNode = Data.define(:value, :loc) do
579
+ include TreeInspect
580
+
581
+ def dependencies
582
+ value ? [value] : []
583
+ end
584
+
585
+ def node_hash
586
+ "return:#{loc&.line}"
587
+ end
588
+
589
+ def node_key(scope_id)
590
+ "#{scope_id}:#{node_hash}"
591
+ end
592
+
593
+ def tree_inspect_fields(indent)
594
+ [tree_field(:value, value, indent, last: true)]
595
+ end
596
+ end
597
+ end
598
+ end
599
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../ruby_lsp/type_guessr/config"
4
+
5
+ module TypeGuessr
6
+ module Core
7
+ # Unified logging interface for TypeGuessr
8
+ # Uses Config.debug? to control output
9
+ module Logger
10
+ module_function
11
+
12
+ # Log debug message with optional context
13
+ # @param msg [String] the debug message
14
+ # @param context [Hash] optional context information
15
+ def debug(msg, context = {})
16
+ return unless debug_enabled?
17
+
18
+ output = "[TypeGuessr:DEBUG] #{msg}"
19
+ output += " #{context.inspect}" unless context.empty?
20
+ warn output
21
+ end
22
+
23
+ # Log error message with optional exception
24
+ # @param msg [String] the error message
25
+ # @param exception [Exception, nil] optional exception for backtrace
26
+ def error(msg, exception = nil)
27
+ return unless debug_enabled?
28
+
29
+ warn "[TypeGuessr:ERROR] #{msg}"
30
+ return unless exception
31
+
32
+ warn " #{exception.class}: #{exception.message}"
33
+ warn exception.backtrace.first(5).map { |l| " #{l}" }.join("\n")
34
+ end
35
+
36
+ # Check if debug mode is enabled
37
+ # @return [Boolean] true if Config.debug? returns true
38
+ def debug_enabled?
39
+ RubyLsp::TypeGuessr::Config.debug?
40
+ end
41
+ end
42
+ end
43
+ end