rigortype 0.1.5 → 0.1.7

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -79
  3. data/lib/rigor/analysis/baseline.rb +347 -0
  4. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  5. data/lib/rigor/analysis/check_rules.rb +68 -3
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  7. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/runner.rb +309 -22
  11. data/lib/rigor/analysis/worker_session.rb +14 -2
  12. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  13. data/lib/rigor/builtins/static_return_refinements.rb +142 -0
  14. data/lib/rigor/cache/store.rb +33 -3
  15. data/lib/rigor/cli/baseline_command.rb +377 -0
  16. data/lib/rigor/cli/lsp_command.rb +129 -0
  17. data/lib/rigor/cli/type_of_command.rb +44 -5
  18. data/lib/rigor/cli.rb +142 -13
  19. data/lib/rigor/configuration.rb +58 -2
  20. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  21. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  22. data/lib/rigor/environment/rbs_loader.rb +67 -2
  23. data/lib/rigor/environment/reporters.rb +40 -0
  24. data/lib/rigor/environment.rb +119 -9
  25. data/lib/rigor/flow_contribution/fact.rb +20 -10
  26. data/lib/rigor/inference/acceptance.rb +48 -3
  27. data/lib/rigor/inference/expression_typer.rb +64 -2
  28. data/lib/rigor/inference/hkt_body.rb +171 -0
  29. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  30. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  31. data/lib/rigor/inference/hkt_registry.rb +223 -0
  32. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
  33. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
  34. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  35. data/lib/rigor/inference/method_dispatcher.rb +174 -6
  36. data/lib/rigor/inference/narrowing.rb +103 -1
  37. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  38. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  39. data/lib/rigor/inference/scope_indexer.rb +209 -19
  40. data/lib/rigor/inference/statement_evaluator.rb +172 -11
  41. data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
  42. data/lib/rigor/language_server/buffer_table.rb +63 -0
  43. data/lib/rigor/language_server/completion_provider.rb +438 -0
  44. data/lib/rigor/language_server/debouncer.rb +86 -0
  45. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  46. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  47. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  48. data/lib/rigor/language_server/hover_provider.rb +74 -0
  49. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  50. data/lib/rigor/language_server/loop.rb +71 -0
  51. data/lib/rigor/language_server/project_context.rb +145 -0
  52. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  53. data/lib/rigor/language_server/server.rb +384 -0
  54. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  55. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  56. data/lib/rigor/language_server/uri.rb +40 -0
  57. data/lib/rigor/language_server.rb +29 -0
  58. data/lib/rigor/plugin/base.rb +63 -0
  59. data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
  60. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  61. data/lib/rigor/plugin/manifest.rb +54 -7
  62. data/lib/rigor/plugin/registry.rb +19 -0
  63. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  64. data/lib/rigor/rbs_extended.rb +82 -2
  65. data/lib/rigor/sig_gen/generator.rb +12 -3
  66. data/lib/rigor/type/app.rb +107 -0
  67. data/lib/rigor/type.rb +1 -0
  68. data/lib/rigor/version.rb +1 -1
  69. data/sig/rigor/environment.rbs +10 -4
  70. data/sig/rigor/inference.rbs +2 -0
  71. data/sig/rigor.rbs +4 -1
  72. metadata +56 -1
@@ -177,8 +177,10 @@ module Rigor
177
177
  return if def_node.body.nil? || qualified_prefix.empty?
178
178
 
179
179
  class_name = qualified_prefix.join("::")
180
+ singleton = def_node.receiver.is_a?(Prism::SelfNode) ||
181
+ def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
180
182
  self_type =
181
- if def_node.receiver.is_a?(Prism::SelfNode)
183
+ if singleton
182
184
  Type::Combinator.singleton_of(class_name)
183
185
  else
184
186
  Type::Combinator.nominal_of(class_name)
@@ -341,11 +343,26 @@ module Rigor
341
343
  unless qualified_prefix.empty?
342
344
  body_scope = body_scope.with_self_type(Type::Combinator.singleton_of(qualified_prefix.join("::")))
343
345
  end
344
- rvalue_type = body_scope.type_of(node.value)
346
+ rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
345
347
  existing = accumulator[full]
346
348
  accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
347
349
  end
348
350
 
351
+ # Survey item (e): when the rvalue is a recognised
352
+ # `Module.new do ... end` / `Class.new do ... end` /
353
+ # `Struct.new(*sym) do ... end` / `Data.define(*sym) do
354
+ # ... end` form, type the named constant as
355
+ # `Singleton[<full>]` so the discovered-method table
356
+ # registered under `full` becomes reachable through
357
+ # singleton-side dispatch (`Const.[]=` etc.). Returns nil
358
+ # for non-meta-new rvalues so the caller falls back to the
359
+ # default `body_scope.type_of(node.value)` shape.
360
+ def meta_new_constant_type(node, full)
361
+ return nil unless meta_new_block_body(node)
362
+
363
+ Type::Combinator.singleton_of(full)
364
+ end
365
+
349
366
  # Slice 7 phase 12 — in-source method discovery pre-pass.
350
367
  # Walks every class/module body and records the methods
351
368
  # introduced via `Prism::DefNode` (instance + singleton)
@@ -371,9 +388,12 @@ module Rigor
371
388
  return
372
389
  end
373
390
  when Prism::SingletonClassNode
374
- if node.expression.is_a?(Prism::SelfNode) && node.body
375
- walk_methods(node.body, qualified_prefix, true, accumulator)
376
- return
391
+ if node.body
392
+ singleton_prefix = singleton_class_prefix(node, qualified_prefix)
393
+ if singleton_prefix
394
+ walk_methods(node.body, singleton_prefix, true, accumulator)
395
+ return
396
+ end
377
397
  end
378
398
  when Prism::ConstantWriteNode
379
399
  if meta_new_block_body(node)
@@ -395,21 +415,51 @@ module Rigor
395
415
  walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
396
416
  end
397
417
  end
418
+
419
+ # Resolves a `class << X` body's qualified prefix.
420
+ # - `class << self` keeps `qualified_prefix` (the enclosing class).
421
+ # - `class << Foo` inside `class Foo` collapses to the same prefix
422
+ # (semantically `class << self`).
423
+ # - `class << Foo` not nested in `class Foo` returns `[Foo]`
424
+ # so methods defined inside register on Foo's singleton.
425
+ # - Any other expression (variable, method call) returns nil
426
+ # so the walker falls through and skips the body.
427
+ def singleton_class_prefix(node, qualified_prefix)
428
+ case node.expression
429
+ when Prism::SelfNode
430
+ qualified_prefix
431
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
432
+ rendered = qualified_name_for(node.expression)
433
+ return nil unless rendered
434
+
435
+ if !qualified_prefix.empty? && qualified_prefix.last == rendered
436
+ qualified_prefix
437
+ else
438
+ rendered.split("::")
439
+ end
440
+ end
441
+ end
398
442
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
399
443
 
400
444
  # v0.1.2 — when a `Const = Data.define(*sym) do ... end`
401
445
  # / `Const = Struct.new(*sym) do ... end` constant write
402
446
  # carries a block, the block body holds method overrides
403
- # whose canonical class is `Const`. Returns the block body
404
- # node (a `Prism::StatementsNode`) when the rvalue
405
- # matches; nil otherwise. Used by `walk_methods` /
406
- # `walk_def_nodes` to push `Const` onto the qualified
407
- # prefix before recursing.
447
+ # whose canonical class is `Const`. Survey item (e) extended
448
+ # the recognition to `Const = Module.new do ... end` and
449
+ # `Const = Class.new(?super) do ... end` — the
450
+ # ADR-16 Tier A "block-as-method" idiom at constant-write
451
+ # position. Returns the block body node (a
452
+ # `Prism::StatementsNode`) when the rvalue matches; nil
453
+ # otherwise. Used by `walk_methods` / `walk_def_nodes` to
454
+ # push `Const` onto the qualified prefix before recursing.
408
455
  def meta_new_block_body(node)
409
456
  return nil unless node.is_a?(Prism::ConstantWriteNode)
410
457
 
411
458
  rvalue = node.value
412
- return nil unless data_define_call?(rvalue) || struct_new_call?(rvalue)
459
+ return nil unless data_define_call?(rvalue) ||
460
+ struct_new_call?(rvalue) ||
461
+ module_new_call?(rvalue) ||
462
+ class_new_call?(rvalue)
413
463
 
414
464
  rvalue.block&.body
415
465
  end
@@ -418,11 +468,50 @@ module Rigor
418
468
  return if qualified_prefix.empty?
419
469
 
420
470
  class_name = qualified_prefix.join("::")
421
- kind = def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
471
+ singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
472
+ kind = singleton ? :singleton : :instance
422
473
  accumulator[class_name] ||= {}
423
474
  accumulator[class_name][def_node.name] = kind
424
475
  end
425
476
 
477
+ # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
478
+ # `module Meta`) is semantically equivalent to `def self.bar`:
479
+ # at the def-site, the runtime value of the constant `Foo` is
480
+ # the module itself (== `self`). Recognise the form so the
481
+ # method registers as singleton on the enclosing class.
482
+ #
483
+ # The cross-class form `def Bar.baz` inside `module Foo` —
484
+ # where the receiver names a constant other than the
485
+ # enclosing class — is not supported at this slice; falls
486
+ # through to `:instance` (current behaviour) rather than
487
+ # silently re-routing the registration.
488
+ def def_singleton?(def_node, qualified_prefix, in_singleton_class)
489
+ return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
490
+
491
+ def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
492
+ end
493
+
494
+ # Only `Prism::ConstantReadNode` is observed in real Ruby —
495
+ # Prism mis-parses `def C::P.method` as `def C.P` (Ruby itself
496
+ # rejects the form as a SyntaxError). The ConstantPathNode
497
+ # branch stays defensive in case Prism's grammar widens.
498
+ def def_receiver_targets_lexical_self?(receiver, qualified_prefix)
499
+ return false if qualified_prefix.empty?
500
+
501
+ case receiver
502
+ when Prism::ConstantReadNode
503
+ receiver.name.to_s == qualified_prefix.last
504
+ when Prism::ConstantPathNode
505
+ rendered = render_constant_path(receiver)
506
+ return false unless rendered
507
+
508
+ path = rendered.split("::")
509
+ qualified_prefix.last(path.length) == path
510
+ else
511
+ false
512
+ end
513
+ end
514
+
426
515
  # v0.0.2 #5 — instance-side def-node recording. Walks
427
516
  # class bodies the same way as `build_discovered_methods`
428
517
  # but records the actual `Prism::DefNode` for each
@@ -452,9 +541,12 @@ module Rigor
452
541
  return
453
542
  end
454
543
  when Prism::SingletonClassNode
455
- if node.expression.is_a?(Prism::SelfNode) && node.body
456
- walk_def_nodes(node.body, qualified_prefix, true, accumulator)
457
- return
544
+ if node.body
545
+ singleton_prefix = singleton_class_prefix(node, qualified_prefix)
546
+ if singleton_prefix
547
+ walk_def_nodes(node.body, singleton_prefix, true, accumulator)
548
+ return
549
+ end
458
550
  end
459
551
  when Prism::ConstantWriteNode
460
552
  if meta_new_block_body(node)
@@ -481,7 +573,7 @@ module Rigor
481
573
  TOP_LEVEL_DEF_KEY = "<toplevel>"
482
574
 
483
575
  def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
484
- return if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
576
+ return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
485
577
 
486
578
  class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
487
579
  accumulator[class_name] ||= {}
@@ -530,9 +622,12 @@ module Rigor
530
622
  return current_visibility
531
623
  end
532
624
  when Prism::SingletonClassNode
533
- if node.expression.is_a?(Prism::SelfNode) && node.body
534
- walk_method_visibilities(node.body, qualified_prefix, true, :public, accumulator)
535
- return current_visibility
625
+ if node.body
626
+ singleton_prefix = singleton_class_prefix(node, qualified_prefix)
627
+ if singleton_prefix
628
+ walk_method_visibilities(node.body, singleton_prefix, true, :public, accumulator)
629
+ return current_visibility
630
+ end
536
631
  end
537
632
  when Prism::ConstantWriteNode
538
633
  if meta_new_block_body(node)
@@ -703,6 +798,76 @@ module Rigor
703
798
  node.unescaped&.to_sym
704
799
  end
705
800
 
801
+ # Walks every file in `paths` (each path is parsed once with
802
+ # `Prism.parse_file`) and returns the unioned project-wide
803
+ # `discovered_classes` Hash: `{qualified_name => Singleton[…]}`.
804
+ # Used by `Analysis::Runner` to seed each file's
805
+ # `default_scope.discovered_classes` so that lexical
806
+ # constant lookup in one file resolves a `class Foo`
807
+ # declared in a sibling file. Per-file collisions are
808
+ # last-write-wins (matches the existing in-file merge
809
+ # semantics). Parse failures fail-soft to an empty
810
+ # contribution. The `buffer` argument, when present,
811
+ # redirects reads for the bound logical path to the
812
+ # buffer's physical path so editor-mode pre-passes see
813
+ # the in-flight bytes.
814
+ #
815
+ # **Modules are intentionally excluded** from the
816
+ # project-wide seed: a `module M; module_function; def x; end; end`
817
+ # body, when surfaced as `singleton(M)` to the dispatcher,
818
+ # falls through to `Kernel#x` (or any Module ancestor
819
+ # method) when the project's per-file
820
+ # `discovered_methods` doesn't know `M.x` — leading to
821
+ # surprising types like `Kernel.select → Array[String]`.
822
+ # Until cross-file `discovered_methods` follows the same
823
+ # project-wide seed, registering modules here would
824
+ # introduce regressions in modules-with-module_function
825
+ # idioms that previously resolved to `Dynamic[Top]`.
826
+ # Class declarations are safe because per-file
827
+ # `discovered_methods` already tracks `def self.x` /
828
+ # `def x` instance and singleton methods consistently.
829
+ #
830
+ # @param paths [Array<String>] project file paths.
831
+ # @param buffer [Rigor::Analysis::BufferBinding, nil]
832
+ # @return [Hash{String => Rigor::Type::Singleton}]
833
+ def discovered_classes_for_paths(paths, buffer: nil)
834
+ accumulator = {}
835
+ paths.each do |path|
836
+ physical = buffer ? buffer.resolve(path) : path
837
+ source = File.read(physical)
838
+ root = Prism.parse(source, filepath: path).value
839
+ collect_class_decls(root, [], accumulator)
840
+ rescue StandardError
841
+ # Skip files that fail to parse or read; the per-file
842
+ # analyzer surfaces the parse error separately.
843
+ next
844
+ end
845
+ accumulator.freeze
846
+ end
847
+
848
+ # Class-only variant of `record_declarations` — descends
849
+ # into nested module bodies (so `module Foo; class Bar`
850
+ # registers `Foo::Bar`) but never registers the module
851
+ # itself in `accumulator`.
852
+ def collect_class_decls(node, qualified_prefix, accumulator)
853
+ return unless node.is_a?(Prism::Node)
854
+
855
+ case node
856
+ when Prism::ClassNode
857
+ name = qualified_name_for(node.constant_path)
858
+ if name
859
+ full = (qualified_prefix + [name]).join("::")
860
+ accumulator[full] = Type::Combinator.singleton_of(full)
861
+ return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if node.body
862
+ end
863
+ when Prism::ModuleNode
864
+ name = qualified_name_for(node.constant_path)
865
+ return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if name && node.body
866
+ end
867
+
868
+ node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
869
+ end
870
+
706
871
  # Walks the program once for `Prism::ModuleNode` and
707
872
  # `Prism::ClassNode`, recording the `Singleton[<qualified>]`
708
873
  # type for the outermost `constant_path` node of each
@@ -802,6 +967,31 @@ module Rigor
802
967
  positional.all?(Prism::SymbolNode)
803
968
  end
804
969
 
970
+ # Recognises `Module.new` and `Module.new(&block)` /
971
+ # `Module.new do ... end` at constant-write rvalue
972
+ # position. The block body is the anonymous module's
973
+ # `module_eval` body; defs inside it bind methods on the
974
+ # named constant (`Const = Module.new do ...; def foo; ...; end; end`).
975
+ # Arguments are NOT inspected because `Module.new` accepts
976
+ # no positionals — Ruby raises ArgumentError if any are
977
+ # passed — so a malformed call falls through the walker
978
+ # without affecting analysis.
979
+ def module_new_call?(node)
980
+ meta_call_with_name?(node, :Module, :new)
981
+ end
982
+
983
+ # Recognises `Class.new`, `Class.new(super_class)`, and the
984
+ # block form `Class.new { ... }`. Like `module_new_call?`,
985
+ # the block body is walked as the anonymous class's body.
986
+ # The optional `super_class` positional is accepted but does
987
+ # NOT route through `ancestor` discovery in this slice — the
988
+ # synthesised class still answers method lookups via its
989
+ # own body's defs, mirroring how `Struct.new` / `Data.define`
990
+ # are handled.
991
+ def class_new_call?(node)
992
+ meta_call_with_name?(node, :Class, :new)
993
+ end
994
+
805
995
  def meta_call_with_name?(node, receiver_name, method_name)
806
996
  return false unless node.is_a?(Prism::CallNode)
807
997
  return false unless node.name == method_name
@@ -497,13 +497,25 @@ module Rigor
497
497
  def eval_begin(node)
498
498
  primary_type, primary_scope = eval_begin_primary(node)
499
499
  rescue_chain = collect_rescue_chain_results(node.rescue_clause, scope)
500
-
501
- if rescue_chain.empty?
500
+ # Rescue arms whose body unconditionally exits (`return`,
501
+ # `next`, `break`, `raise`, `throw`, `exit`, `abort`,
502
+ # `fail`) contribute neither a type fragment NOR a scope
503
+ # to the post-begin flow — control left the `begin` via
504
+ # that arm. Mirrors the `eval_if` / `eval_unless` /
505
+ # `eval_and_or` early-return narrowing. Without this
506
+ # filter, a `rescue ... return` on a local bound only in
507
+ # the primary body nil-injects that local across the
508
+ # join, defeating the rescue arm's whole point of guaranteeing
509
+ # the primary local is in scope for downstream statements.
510
+ live_rescues = rescue_chain.reject { |_pair, arm_node| branch_unconditionally_exits?(arm_node.statements) }
511
+ .map(&:first)
512
+
513
+ if live_rescues.empty?
502
514
  exit_type = primary_type
503
515
  exit_scope = primary_scope
504
516
  else
505
- exit_type = Type::Combinator.union(primary_type, *rescue_chain.map(&:first))
506
- exit_scope = reduce_scopes_with_nil_injection([primary_scope, *rescue_chain.map(&:last)])
517
+ exit_type = Type::Combinator.union(primary_type, *live_rescues.map(&:first))
518
+ exit_scope = reduce_scopes_with_nil_injection([primary_scope, *live_rescues.map(&:last)])
507
519
  end
508
520
 
509
521
  if node.ensure_clause
@@ -540,7 +552,7 @@ module Rigor
540
552
  current = rescue_node
541
553
  while current
542
554
  rescue_scope = bind_rescue_reference(current, entry_scope)
543
- results << eval_branch_or_nil(current.statements, rescue_scope)
555
+ results << [eval_branch_or_nil(current.statements, rescue_scope), current]
544
556
  current = current.subsequent
545
557
  end
546
558
  results
@@ -696,10 +708,35 @@ module Rigor
696
708
  # edge-aware: `a && b` can only produce the falsey fragment of
697
709
  # `a` when the RHS is skipped, while `a || b` can only produce
698
710
  # the truthy fragment of `a` when the RHS is skipped.
711
+ #
712
+ # When the RHS unconditionally exits (`raise` / `return` /
713
+ # `throw` / `exit` / `abort` / `fail` / `next` / `break`), the
714
+ # post-OR / post-AND scope is the LHS-skipped edge alone:
715
+ # `a or raise` only survives when `a` was truthy, so subsequent
716
+ # statements observe `a` narrowed to its truthy fragment; the
717
+ # symmetric `a and raise` survives only when `a` was falsey.
718
+ # Same shape as the `eval_if` / `eval_unless` early-return
719
+ # narrowing.
699
720
  def eval_and_or(node)
700
721
  left_type, left_scope = sub_eval(node.left, scope)
701
722
  truthy_left, falsey_left = Narrowing.predicate_scopes(node.left, left_scope)
702
723
  rhs_entry = node.is_a?(Prism::AndNode) ? truthy_left : falsey_left
724
+ if branch_unconditionally_exits?(node.right)
725
+ # Walk the RHS for side-effects (on_enter callbacks,
726
+ # diagnostic dispatch on the raise / return expression
727
+ # itself) but discard its scope: control never reaches
728
+ # any statement after `a or raise` via that edge.
729
+ sub_eval(node.right, rhs_entry)
730
+ surviving_type =
731
+ if node.is_a?(Prism::AndNode)
732
+ Narrowing.narrow_falsey(left_type)
733
+ else
734
+ Narrowing.narrow_truthy(left_type)
735
+ end
736
+ surviving_scope = node.is_a?(Prism::AndNode) ? falsey_left : truthy_left
737
+ return [surviving_type, surviving_scope]
738
+ end
739
+
703
740
  right_type, right_scope = sub_eval(node.right, rhs_entry)
704
741
  skipped_type =
705
742
  if node.is_a?(Prism::AndNode)
@@ -760,6 +797,19 @@ module Rigor
760
797
  # name as a Symbol, so the produced type is `Constant[:name]`.
761
798
  def eval_def(node)
762
799
  body_scope = build_method_entry_scope(node)
800
+ # Parameter default value expressions (e.g. `self.x` in
801
+ # `def copy(x: self.x)`) execute when the method is
802
+ # *invoked*, not when the `def` is read; their `self` is
803
+ # the instance receiver, not the surrounding class body.
804
+ # Walk the parameters subtree under `body_scope` so the
805
+ # scope-index records the instance `self_type` for every
806
+ # node inside parameter defaults. `propagate` would
807
+ # otherwise drop them to the outer class-body scope (where
808
+ # `self_type` is `singleton(C)`), making `self.foo` look
809
+ # like a singleton-side call. Observed surfacing 915 false
810
+ # positives in `prism-1.9.0`'s auto-generated `copy`
811
+ # methods alone.
812
+ sub_eval(node.parameters, body_scope, class_context: @class_context) if node.parameters
763
813
  sub_eval(node.body, body_scope, class_context: @class_context) if node.body
764
814
  [Type::Combinator.constant_of(node.name), scope]
765
815
  end
@@ -783,6 +833,21 @@ module Rigor
783
833
  def eval_call(node)
784
834
  call_type = scope.type_of(node, tracer: tracer)
785
835
  evaluate_block_if_present(node)
836
+ # `ruby2_keywords def foo(...)` (and similar wrappers like
837
+ # `private def`, `public def`, `module_function def`) parse
838
+ # the def as the call's positional argument; the
839
+ # ExpressionTyper#type_of_def handler types it as
840
+ # `Constant[:foo]` without walking the body. Without
841
+ # explicitly evaluating the argument-position def, the body's
842
+ # scope-index entries inherit the outer class-body
843
+ # `self_type = singleton(C)` from `ScopeIndexer.propagate`,
844
+ # so `self.helper` inside reports `undefined method 'helper'
845
+ # for singleton(C)`. Walking each argument-position def under
846
+ # the current evaluator (not a sub_eval — the def's effects
847
+ # do not bind into the surrounding scope) populates the
848
+ # scope index with the correct instance / singleton
849
+ # `self_type` for the def's body.
850
+ evaluate_def_arguments(node)
786
851
  post_scope = record_closure_escape_if_any(node)
787
852
  post_scope = apply_rbs_extended_assertions(node, post_scope)
788
853
  post_scope = apply_plugin_assertions(node, post_scope)
@@ -790,6 +855,15 @@ module Rigor
790
855
  [call_type, post_scope]
791
856
  end
792
857
 
858
+ def evaluate_def_arguments(call_node)
859
+ args = call_node.arguments
860
+ return unless args.respond_to?(:arguments)
861
+
862
+ args.arguments.each do |arg|
863
+ eval_def(arg) if arg.is_a?(Prism::DefNode)
864
+ end
865
+ end
866
+
793
867
  # v0.0.3 — recognises a small catalogue of RSpec
794
868
  # matcher patterns as assert-shaped narrows on the
795
869
  # local passed to `expect(...)`. The pattern is
@@ -1069,7 +1143,16 @@ module Rigor
1069
1143
  # narrowing logic via `Narrowing.narrow_for_fact` so the
1070
1144
  # predicate / assert / plugin paths all converge on the
1071
1145
  # same hierarchy-aware narrowing rules.
1146
+ #
1147
+ # v0.1.8 Pillar 2 Slice 1 added the `:local` target_kind
1148
+ # branch so plugins recognising bespoke call shapes
1149
+ # (`expect(x).to be_a(T)`) can directly narrow a named
1150
+ # local in the surrounding scope, bypassing the
1151
+ # parameter-name lookup that requires an authoritative RBS
1152
+ # sig on the called method (which RSpec matchers lack).
1072
1153
  def apply_post_return_fact(fact, call_node, current_scope, method_def)
1154
+ return apply_local_post_return_fact(fact, current_scope) if fact.target_kind == :local
1155
+
1073
1156
  target_node = fact_target_node(fact, call_node, method_def)
1074
1157
  return apply_self_post_return_fact(fact, target_node, current_scope) if fact.target_kind == :self
1075
1158
  return current_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
@@ -1082,6 +1165,21 @@ module Rigor
1082
1165
  current_scope.with_local(local_name, narrowed)
1083
1166
  end
1084
1167
 
1168
+ # v0.1.8 Pillar 2 Slice 1 — narrows the named local directly
1169
+ # without consulting the call node's argument list. The fact's
1170
+ # `target_name` is the local-variable name as written in
1171
+ # source. Silently no-ops when the local is unbound in the
1172
+ # current scope (the plugin's named local may have already
1173
+ # gone out of scope when the contribution fires).
1174
+ def apply_local_post_return_fact(fact, current_scope)
1175
+ local_name = fact.target_name
1176
+ current_type = current_scope.local(local_name)
1177
+ return current_scope if current_type.nil?
1178
+
1179
+ narrowed = Narrowing.narrow_for_fact(current_type, fact, current_scope.environment)
1180
+ current_scope.with_local(local_name, narrowed)
1181
+ end
1182
+
1085
1183
  # v0.1.1 Track 1 slice 3 — `assert self is T` post-return
1086
1184
  # narrowing for the four supported receiver shapes (mirrors
1087
1185
  # `Narrowing#apply_self_fact`).
@@ -1399,7 +1497,37 @@ module Rigor
1399
1497
  end
1400
1498
 
1401
1499
  def singleton_def?(def_node)
1402
- def_node.receiver.is_a?(Prism::SelfNode) || current_frame_singleton?
1500
+ return true if def_node.receiver.is_a?(Prism::SelfNode) || current_frame_singleton?
1501
+
1502
+ def_receiver_targets_lexical_self?(def_node.receiver)
1503
+ end
1504
+
1505
+ # `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
1506
+ # `module Meta`) — explicit-receiver def that semantically
1507
+ # equals `def self.bar` because the receiver constant
1508
+ # resolves to `self` at the def-site. Matched against the
1509
+ # current class context's tail to cover both the
1510
+ # `def OpenURI.x` form (single segment) and the
1511
+ # `def OpenURI::Meta.x` form (qualified path). Cross-class
1512
+ # receivers (`def Bar.baz` inside `module Foo` where the
1513
+ # receiver names a different constant) are not promoted to
1514
+ # singleton at this slice.
1515
+ def def_receiver_targets_lexical_self?(receiver)
1516
+ return false if @class_context.empty?
1517
+
1518
+ prefix = @class_context.map(&:name)
1519
+ case receiver
1520
+ when Prism::ConstantReadNode
1521
+ receiver.name.to_s == prefix.last
1522
+ when Prism::ConstantPathNode
1523
+ rendered = render_constant_path(receiver)
1524
+ return false unless rendered
1525
+
1526
+ path = rendered.split("::")
1527
+ prefix.last(path.length) == path
1528
+ else
1529
+ false
1530
+ end
1403
1531
  end
1404
1532
 
1405
1533
  # Slice A-engine. Inside a class body `class Foo; ...; end`,
@@ -1452,12 +1580,45 @@ module Rigor
1452
1580
  end
1453
1581
 
1454
1582
  def singleton_context_for(node)
1455
- return @class_context unless node.expression.is_a?(Prism::SelfNode)
1456
- return @class_context if @class_context.empty?
1583
+ case node.expression
1584
+ when Prism::SelfNode
1585
+ return @class_context if @class_context.empty?
1586
+
1587
+ outer = @class_context[0..-2]
1588
+ last = @class_context.last
1589
+ outer + [ClassFrame.new(name: last.name, singleton: true)]
1590
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
1591
+ target = singleton_constant_target(node.expression)
1592
+ return @class_context unless target
1593
+
1594
+ # `class << Foo` inside `class Foo` (the canonical
1595
+ # pattern in Ruby's own time.rb) is semantically
1596
+ # `class << self` — replace the enclosing frame with a
1597
+ # singleton frame for the same name so method
1598
+ # registration and `self_type` lookup land on Foo.
1599
+ # When the target names a different constant (rare
1600
+ # cross-class form), append a fresh singleton frame
1601
+ # tagged with the target FQN; the bodies are scoped to
1602
+ # that target rather than to the lexical enclosing
1603
+ # class.
1604
+ if !@class_context.empty? && @class_context.last.name == target
1605
+ outer = @class_context[0..-2]
1606
+ outer + [ClassFrame.new(name: target, singleton: true)]
1607
+ else
1608
+ [ClassFrame.new(name: target, singleton: true)]
1609
+ end
1610
+ else
1611
+ @class_context
1612
+ end
1613
+ end
1457
1614
 
1458
- outer = @class_context[0..-2]
1459
- last = @class_context.last
1460
- outer + [ClassFrame.new(name: last.name, singleton: true)]
1615
+ def singleton_constant_target(expression)
1616
+ case expression
1617
+ when Prism::ConstantReadNode
1618
+ expression.name.to_s
1619
+ when Prism::ConstantPathNode
1620
+ render_constant_path(expression)
1621
+ end
1461
1622
  end
1462
1623
 
1463
1624
  # The qualified name of the immediately-enclosing class (joining