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.
- checksums.yaml +4 -4
- data/README.md +76 -79
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +68 -3
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +142 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +142 -13
- data/lib/rigor/configuration.rb +58 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +67 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +119 -9
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +64 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +174 -6
- data/lib/rigor/inference/narrowing.rb +103 -1
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +209 -19
- data/lib/rigor/inference/statement_evaluator.rb +172 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +10 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +4 -1
- 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
|
|
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.
|
|
375
|
-
|
|
376
|
-
|
|
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`.
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
#
|
|
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) ||
|
|
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
|
-
|
|
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.
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
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.
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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, *
|
|
506
|
-
exit_scope = reduce_scopes_with_nil_injection([primary_scope, *
|
|
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
|
-
|
|
1456
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|