rigortype 0.1.5 → 0.1.6
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 +36 -50
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- 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 +120 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +74 -12
- data/lib/rigor/configuration.rb +38 -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 +45 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +106 -9
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +47 -0
- 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/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +154 -3
- 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 +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- 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 +125 -11
- 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 +8 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +3 -1
- metadata +54 -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)
|
|
@@ -371,9 +373,12 @@ module Rigor
|
|
|
371
373
|
return
|
|
372
374
|
end
|
|
373
375
|
when Prism::SingletonClassNode
|
|
374
|
-
if node.
|
|
375
|
-
|
|
376
|
-
|
|
376
|
+
if node.body
|
|
377
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
378
|
+
if singleton_prefix
|
|
379
|
+
walk_methods(node.body, singleton_prefix, true, accumulator)
|
|
380
|
+
return
|
|
381
|
+
end
|
|
377
382
|
end
|
|
378
383
|
when Prism::ConstantWriteNode
|
|
379
384
|
if meta_new_block_body(node)
|
|
@@ -395,6 +400,30 @@ module Rigor
|
|
|
395
400
|
walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
|
|
396
401
|
end
|
|
397
402
|
end
|
|
403
|
+
|
|
404
|
+
# Resolves a `class << X` body's qualified prefix.
|
|
405
|
+
# - `class << self` keeps `qualified_prefix` (the enclosing class).
|
|
406
|
+
# - `class << Foo` inside `class Foo` collapses to the same prefix
|
|
407
|
+
# (semantically `class << self`).
|
|
408
|
+
# - `class << Foo` not nested in `class Foo` returns `[Foo]`
|
|
409
|
+
# so methods defined inside register on Foo's singleton.
|
|
410
|
+
# - Any other expression (variable, method call) returns nil
|
|
411
|
+
# so the walker falls through and skips the body.
|
|
412
|
+
def singleton_class_prefix(node, qualified_prefix)
|
|
413
|
+
case node.expression
|
|
414
|
+
when Prism::SelfNode
|
|
415
|
+
qualified_prefix
|
|
416
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
417
|
+
rendered = qualified_name_for(node.expression)
|
|
418
|
+
return nil unless rendered
|
|
419
|
+
|
|
420
|
+
if !qualified_prefix.empty? && qualified_prefix.last == rendered
|
|
421
|
+
qualified_prefix
|
|
422
|
+
else
|
|
423
|
+
rendered.split("::")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
398
427
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
399
428
|
|
|
400
429
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
@@ -418,11 +447,50 @@ module Rigor
|
|
|
418
447
|
return if qualified_prefix.empty?
|
|
419
448
|
|
|
420
449
|
class_name = qualified_prefix.join("::")
|
|
421
|
-
|
|
450
|
+
singleton = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
451
|
+
kind = singleton ? :singleton : :instance
|
|
422
452
|
accumulator[class_name] ||= {}
|
|
423
453
|
accumulator[class_name][def_node.name] = kind
|
|
424
454
|
end
|
|
425
455
|
|
|
456
|
+
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
457
|
+
# `module Meta`) is semantically equivalent to `def self.bar`:
|
|
458
|
+
# at the def-site, the runtime value of the constant `Foo` is
|
|
459
|
+
# the module itself (== `self`). Recognise the form so the
|
|
460
|
+
# method registers as singleton on the enclosing class.
|
|
461
|
+
#
|
|
462
|
+
# The cross-class form `def Bar.baz` inside `module Foo` —
|
|
463
|
+
# where the receiver names a constant other than the
|
|
464
|
+
# enclosing class — is not supported at this slice; falls
|
|
465
|
+
# through to `:instance` (current behaviour) rather than
|
|
466
|
+
# silently re-routing the registration.
|
|
467
|
+
def def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
468
|
+
return true if def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class
|
|
469
|
+
|
|
470
|
+
def_receiver_targets_lexical_self?(def_node.receiver, qualified_prefix)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Only `Prism::ConstantReadNode` is observed in real Ruby —
|
|
474
|
+
# Prism mis-parses `def C::P.method` as `def C.P` (Ruby itself
|
|
475
|
+
# rejects the form as a SyntaxError). The ConstantPathNode
|
|
476
|
+
# branch stays defensive in case Prism's grammar widens.
|
|
477
|
+
def def_receiver_targets_lexical_self?(receiver, qualified_prefix)
|
|
478
|
+
return false if qualified_prefix.empty?
|
|
479
|
+
|
|
480
|
+
case receiver
|
|
481
|
+
when Prism::ConstantReadNode
|
|
482
|
+
receiver.name.to_s == qualified_prefix.last
|
|
483
|
+
when Prism::ConstantPathNode
|
|
484
|
+
rendered = render_constant_path(receiver)
|
|
485
|
+
return false unless rendered
|
|
486
|
+
|
|
487
|
+
path = rendered.split("::")
|
|
488
|
+
qualified_prefix.last(path.length) == path
|
|
489
|
+
else
|
|
490
|
+
false
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
426
494
|
# v0.0.2 #5 — instance-side def-node recording. Walks
|
|
427
495
|
# class bodies the same way as `build_discovered_methods`
|
|
428
496
|
# but records the actual `Prism::DefNode` for each
|
|
@@ -452,9 +520,12 @@ module Rigor
|
|
|
452
520
|
return
|
|
453
521
|
end
|
|
454
522
|
when Prism::SingletonClassNode
|
|
455
|
-
if node.
|
|
456
|
-
|
|
457
|
-
|
|
523
|
+
if node.body
|
|
524
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
525
|
+
if singleton_prefix
|
|
526
|
+
walk_def_nodes(node.body, singleton_prefix, true, accumulator)
|
|
527
|
+
return
|
|
528
|
+
end
|
|
458
529
|
end
|
|
459
530
|
when Prism::ConstantWriteNode
|
|
460
531
|
if meta_new_block_body(node)
|
|
@@ -481,7 +552,7 @@ module Rigor
|
|
|
481
552
|
TOP_LEVEL_DEF_KEY = "<toplevel>"
|
|
482
553
|
|
|
483
554
|
def record_def_node(def_node, qualified_prefix, in_singleton_class, accumulator)
|
|
484
|
-
return if
|
|
555
|
+
return if def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
485
556
|
|
|
486
557
|
class_name = qualified_prefix.empty? ? TOP_LEVEL_DEF_KEY : qualified_prefix.join("::")
|
|
487
558
|
accumulator[class_name] ||= {}
|
|
@@ -530,9 +601,12 @@ module Rigor
|
|
|
530
601
|
return current_visibility
|
|
531
602
|
end
|
|
532
603
|
when Prism::SingletonClassNode
|
|
533
|
-
if node.
|
|
534
|
-
|
|
535
|
-
|
|
604
|
+
if node.body
|
|
605
|
+
singleton_prefix = singleton_class_prefix(node, qualified_prefix)
|
|
606
|
+
if singleton_prefix
|
|
607
|
+
walk_method_visibilities(node.body, singleton_prefix, true, :public, accumulator)
|
|
608
|
+
return current_visibility
|
|
609
|
+
end
|
|
536
610
|
end
|
|
537
611
|
when Prism::ConstantWriteNode
|
|
538
612
|
if meta_new_block_body(node)
|
|
@@ -703,6 +777,76 @@ module Rigor
|
|
|
703
777
|
node.unescaped&.to_sym
|
|
704
778
|
end
|
|
705
779
|
|
|
780
|
+
# Walks every file in `paths` (each path is parsed once with
|
|
781
|
+
# `Prism.parse_file`) and returns the unioned project-wide
|
|
782
|
+
# `discovered_classes` Hash: `{qualified_name => Singleton[…]}`.
|
|
783
|
+
# Used by `Analysis::Runner` to seed each file's
|
|
784
|
+
# `default_scope.discovered_classes` so that lexical
|
|
785
|
+
# constant lookup in one file resolves a `class Foo`
|
|
786
|
+
# declared in a sibling file. Per-file collisions are
|
|
787
|
+
# last-write-wins (matches the existing in-file merge
|
|
788
|
+
# semantics). Parse failures fail-soft to an empty
|
|
789
|
+
# contribution. The `buffer` argument, when present,
|
|
790
|
+
# redirects reads for the bound logical path to the
|
|
791
|
+
# buffer's physical path so editor-mode pre-passes see
|
|
792
|
+
# the in-flight bytes.
|
|
793
|
+
#
|
|
794
|
+
# **Modules are intentionally excluded** from the
|
|
795
|
+
# project-wide seed: a `module M; module_function; def x; end; end`
|
|
796
|
+
# body, when surfaced as `singleton(M)` to the dispatcher,
|
|
797
|
+
# falls through to `Kernel#x` (or any Module ancestor
|
|
798
|
+
# method) when the project's per-file
|
|
799
|
+
# `discovered_methods` doesn't know `M.x` — leading to
|
|
800
|
+
# surprising types like `Kernel.select → Array[String]`.
|
|
801
|
+
# Until cross-file `discovered_methods` follows the same
|
|
802
|
+
# project-wide seed, registering modules here would
|
|
803
|
+
# introduce regressions in modules-with-module_function
|
|
804
|
+
# idioms that previously resolved to `Dynamic[Top]`.
|
|
805
|
+
# Class declarations are safe because per-file
|
|
806
|
+
# `discovered_methods` already tracks `def self.x` /
|
|
807
|
+
# `def x` instance and singleton methods consistently.
|
|
808
|
+
#
|
|
809
|
+
# @param paths [Array<String>] project file paths.
|
|
810
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
811
|
+
# @return [Hash{String => Rigor::Type::Singleton}]
|
|
812
|
+
def discovered_classes_for_paths(paths, buffer: nil)
|
|
813
|
+
accumulator = {}
|
|
814
|
+
paths.each do |path|
|
|
815
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
816
|
+
source = File.read(physical)
|
|
817
|
+
root = Prism.parse(source, filepath: path).value
|
|
818
|
+
collect_class_decls(root, [], accumulator)
|
|
819
|
+
rescue StandardError
|
|
820
|
+
# Skip files that fail to parse or read; the per-file
|
|
821
|
+
# analyzer surfaces the parse error separately.
|
|
822
|
+
next
|
|
823
|
+
end
|
|
824
|
+
accumulator.freeze
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
# Class-only variant of `record_declarations` — descends
|
|
828
|
+
# into nested module bodies (so `module Foo; class Bar`
|
|
829
|
+
# registers `Foo::Bar`) but never registers the module
|
|
830
|
+
# itself in `accumulator`.
|
|
831
|
+
def collect_class_decls(node, qualified_prefix, accumulator)
|
|
832
|
+
return unless node.is_a?(Prism::Node)
|
|
833
|
+
|
|
834
|
+
case node
|
|
835
|
+
when Prism::ClassNode
|
|
836
|
+
name = qualified_name_for(node.constant_path)
|
|
837
|
+
if name
|
|
838
|
+
full = (qualified_prefix + [name]).join("::")
|
|
839
|
+
accumulator[full] = Type::Combinator.singleton_of(full)
|
|
840
|
+
return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
841
|
+
end
|
|
842
|
+
when Prism::ModuleNode
|
|
843
|
+
name = qualified_name_for(node.constant_path)
|
|
844
|
+
return collect_class_decls(node.body, qualified_prefix + [name], accumulator) if name && node.body
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
node.compact_child_nodes.each { |child| collect_class_decls(child, qualified_prefix, accumulator) }
|
|
848
|
+
end
|
|
849
|
+
|
|
706
850
|
# Walks the program once for `Prism::ModuleNode` and
|
|
707
851
|
# `Prism::ClassNode`, recording the `Singleton[<qualified>]`
|
|
708
852
|
# type for the outermost `constant_path` node of each
|
|
@@ -760,6 +760,19 @@ module Rigor
|
|
|
760
760
|
# name as a Symbol, so the produced type is `Constant[:name]`.
|
|
761
761
|
def eval_def(node)
|
|
762
762
|
body_scope = build_method_entry_scope(node)
|
|
763
|
+
# Parameter default value expressions (e.g. `self.x` in
|
|
764
|
+
# `def copy(x: self.x)`) execute when the method is
|
|
765
|
+
# *invoked*, not when the `def` is read; their `self` is
|
|
766
|
+
# the instance receiver, not the surrounding class body.
|
|
767
|
+
# Walk the parameters subtree under `body_scope` so the
|
|
768
|
+
# scope-index records the instance `self_type` for every
|
|
769
|
+
# node inside parameter defaults. `propagate` would
|
|
770
|
+
# otherwise drop them to the outer class-body scope (where
|
|
771
|
+
# `self_type` is `singleton(C)`), making `self.foo` look
|
|
772
|
+
# like a singleton-side call. Observed surfacing 915 false
|
|
773
|
+
# positives in `prism-1.9.0`'s auto-generated `copy`
|
|
774
|
+
# methods alone.
|
|
775
|
+
sub_eval(node.parameters, body_scope, class_context: @class_context) if node.parameters
|
|
763
776
|
sub_eval(node.body, body_scope, class_context: @class_context) if node.body
|
|
764
777
|
[Type::Combinator.constant_of(node.name), scope]
|
|
765
778
|
end
|
|
@@ -783,6 +796,21 @@ module Rigor
|
|
|
783
796
|
def eval_call(node)
|
|
784
797
|
call_type = scope.type_of(node, tracer: tracer)
|
|
785
798
|
evaluate_block_if_present(node)
|
|
799
|
+
# `ruby2_keywords def foo(...)` (and similar wrappers like
|
|
800
|
+
# `private def`, `public def`, `module_function def`) parse
|
|
801
|
+
# the def as the call's positional argument; the
|
|
802
|
+
# ExpressionTyper#type_of_def handler types it as
|
|
803
|
+
# `Constant[:foo]` without walking the body. Without
|
|
804
|
+
# explicitly evaluating the argument-position def, the body's
|
|
805
|
+
# scope-index entries inherit the outer class-body
|
|
806
|
+
# `self_type = singleton(C)` from `ScopeIndexer.propagate`,
|
|
807
|
+
# so `self.helper` inside reports `undefined method 'helper'
|
|
808
|
+
# for singleton(C)`. Walking each argument-position def under
|
|
809
|
+
# the current evaluator (not a sub_eval — the def's effects
|
|
810
|
+
# do not bind into the surrounding scope) populates the
|
|
811
|
+
# scope index with the correct instance / singleton
|
|
812
|
+
# `self_type` for the def's body.
|
|
813
|
+
evaluate_def_arguments(node)
|
|
786
814
|
post_scope = record_closure_escape_if_any(node)
|
|
787
815
|
post_scope = apply_rbs_extended_assertions(node, post_scope)
|
|
788
816
|
post_scope = apply_plugin_assertions(node, post_scope)
|
|
@@ -790,6 +818,15 @@ module Rigor
|
|
|
790
818
|
[call_type, post_scope]
|
|
791
819
|
end
|
|
792
820
|
|
|
821
|
+
def evaluate_def_arguments(call_node)
|
|
822
|
+
args = call_node.arguments
|
|
823
|
+
return unless args.respond_to?(:arguments)
|
|
824
|
+
|
|
825
|
+
args.arguments.each do |arg|
|
|
826
|
+
eval_def(arg) if arg.is_a?(Prism::DefNode)
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
|
|
793
830
|
# v0.0.3 — recognises a small catalogue of RSpec
|
|
794
831
|
# matcher patterns as assert-shaped narrows on the
|
|
795
832
|
# local passed to `expect(...)`. The pattern is
|
|
@@ -1399,7 +1436,37 @@ module Rigor
|
|
|
1399
1436
|
end
|
|
1400
1437
|
|
|
1401
1438
|
def singleton_def?(def_node)
|
|
1402
|
-
def_node.receiver.is_a?(Prism::SelfNode) || current_frame_singleton?
|
|
1439
|
+
return true if def_node.receiver.is_a?(Prism::SelfNode) || current_frame_singleton?
|
|
1440
|
+
|
|
1441
|
+
def_receiver_targets_lexical_self?(def_node.receiver)
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
1445
|
+
# `module Meta`) — explicit-receiver def that semantically
|
|
1446
|
+
# equals `def self.bar` because the receiver constant
|
|
1447
|
+
# resolves to `self` at the def-site. Matched against the
|
|
1448
|
+
# current class context's tail to cover both the
|
|
1449
|
+
# `def OpenURI.x` form (single segment) and the
|
|
1450
|
+
# `def OpenURI::Meta.x` form (qualified path). Cross-class
|
|
1451
|
+
# receivers (`def Bar.baz` inside `module Foo` where the
|
|
1452
|
+
# receiver names a different constant) are not promoted to
|
|
1453
|
+
# singleton at this slice.
|
|
1454
|
+
def def_receiver_targets_lexical_self?(receiver)
|
|
1455
|
+
return false if @class_context.empty?
|
|
1456
|
+
|
|
1457
|
+
prefix = @class_context.map(&:name)
|
|
1458
|
+
case receiver
|
|
1459
|
+
when Prism::ConstantReadNode
|
|
1460
|
+
receiver.name.to_s == prefix.last
|
|
1461
|
+
when Prism::ConstantPathNode
|
|
1462
|
+
rendered = render_constant_path(receiver)
|
|
1463
|
+
return false unless rendered
|
|
1464
|
+
|
|
1465
|
+
path = rendered.split("::")
|
|
1466
|
+
prefix.last(path.length) == path
|
|
1467
|
+
else
|
|
1468
|
+
false
|
|
1469
|
+
end
|
|
1403
1470
|
end
|
|
1404
1471
|
|
|
1405
1472
|
# Slice A-engine. Inside a class body `class Foo; ...; end`,
|
|
@@ -1452,12 +1519,45 @@ module Rigor
|
|
|
1452
1519
|
end
|
|
1453
1520
|
|
|
1454
1521
|
def singleton_context_for(node)
|
|
1455
|
-
|
|
1456
|
-
|
|
1522
|
+
case node.expression
|
|
1523
|
+
when Prism::SelfNode
|
|
1524
|
+
return @class_context if @class_context.empty?
|
|
1525
|
+
|
|
1526
|
+
outer = @class_context[0..-2]
|
|
1527
|
+
last = @class_context.last
|
|
1528
|
+
outer + [ClassFrame.new(name: last.name, singleton: true)]
|
|
1529
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1530
|
+
target = singleton_constant_target(node.expression)
|
|
1531
|
+
return @class_context unless target
|
|
1532
|
+
|
|
1533
|
+
# `class << Foo` inside `class Foo` (the canonical
|
|
1534
|
+
# pattern in Ruby's own time.rb) is semantically
|
|
1535
|
+
# `class << self` — replace the enclosing frame with a
|
|
1536
|
+
# singleton frame for the same name so method
|
|
1537
|
+
# registration and `self_type` lookup land on Foo.
|
|
1538
|
+
# When the target names a different constant (rare
|
|
1539
|
+
# cross-class form), append a fresh singleton frame
|
|
1540
|
+
# tagged with the target FQN; the bodies are scoped to
|
|
1541
|
+
# that target rather than to the lexical enclosing
|
|
1542
|
+
# class.
|
|
1543
|
+
if !@class_context.empty? && @class_context.last.name == target
|
|
1544
|
+
outer = @class_context[0..-2]
|
|
1545
|
+
outer + [ClassFrame.new(name: target, singleton: true)]
|
|
1546
|
+
else
|
|
1547
|
+
[ClassFrame.new(name: target, singleton: true)]
|
|
1548
|
+
end
|
|
1549
|
+
else
|
|
1550
|
+
@class_context
|
|
1551
|
+
end
|
|
1552
|
+
end
|
|
1457
1553
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1554
|
+
def singleton_constant_target(expression)
|
|
1555
|
+
case expression
|
|
1556
|
+
when Prism::ConstantReadNode
|
|
1557
|
+
expression.name.to_s
|
|
1558
|
+
when Prism::ConstantPathNode
|
|
1559
|
+
render_constant_path(expression)
|
|
1560
|
+
end
|
|
1461
1561
|
end
|
|
1462
1562
|
|
|
1463
1563
|
# The qualified name of the immediately-enclosing class (joining
|
|
@@ -53,24 +53,36 @@ module Rigor
|
|
|
53
53
|
# inheritance resolution against RBS-known classes
|
|
54
54
|
# (ActiveRecord::Base, Dry::Struct, etc.) that aren't
|
|
55
55
|
# declared in project source.
|
|
56
|
+
# @param fact_store [Rigor::Plugin::FactStore, nil]
|
|
57
|
+
# the per-run cross-plugin fact store. ADR-18 lookups
|
|
58
|
+
# (`Plugin::Macro::HeredocTemplate::Emit#returns_from_arg`)
|
|
59
|
+
# consult this at scan time to resolve per-call-site
|
|
60
|
+
# return types from published facts; without it, those
|
|
61
|
+
# emit rows fall back to their static `returns:` (or
|
|
62
|
+
# `"untyped"` → `Dynamic[Top]`).
|
|
63
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
64
|
+
# editor-mode buffer binding. When set, reads for the
|
|
65
|
+
# logical path resolve to the buffer's physical path so
|
|
66
|
+
# the pre-pass sees the in-flight bytes instead of the
|
|
67
|
+
# on-disk copy.
|
|
56
68
|
# @return [Rigor::Inference::SyntheticMethodIndex]
|
|
57
|
-
def scan(plugin_registry:, paths:, environment: nil)
|
|
69
|
+
def scan(plugin_registry:, paths:, environment: nil, fact_store: nil, buffer: nil)
|
|
58
70
|
templates = collect_templates(plugin_registry)
|
|
59
71
|
registries = collect_trait_registries(plugin_registry)
|
|
60
72
|
return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty?
|
|
61
73
|
|
|
62
|
-
asts = parse_paths(paths)
|
|
74
|
+
asts = parse_paths(paths, buffer: buffer)
|
|
63
75
|
hierarchy = build_hierarchy(asts)
|
|
64
76
|
concern_index = build_concern_index(asts)
|
|
65
77
|
|
|
66
78
|
entries = []
|
|
67
79
|
asts.each do |path, ast|
|
|
68
80
|
walk_class_bodies(ast) do |class_name, call_node|
|
|
69
|
-
collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path)
|
|
81
|
+
collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path, fact_store)
|
|
70
82
|
collect_trait_entries(entries, registries, class_name, call_node, hierarchy, environment, path)
|
|
71
83
|
collect_concern_re_targeted_entries(
|
|
72
84
|
entries, call_node, class_name, concern_index,
|
|
73
|
-
templates, registries, hierarchy, environment, path
|
|
85
|
+
templates, registries, hierarchy, environment, path, fact_store
|
|
74
86
|
)
|
|
75
87
|
end
|
|
76
88
|
end
|
|
@@ -107,10 +119,11 @@ module Rigor
|
|
|
107
119
|
end
|
|
108
120
|
end
|
|
109
121
|
|
|
110
|
-
def parse_paths(paths)
|
|
122
|
+
def parse_paths(paths, buffer: nil)
|
|
111
123
|
paths.to_h do |path|
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
125
|
+
source = File.read(physical)
|
|
126
|
+
[path, Prism.parse(source, filepath: path).value]
|
|
114
127
|
rescue StandardError
|
|
115
128
|
[path, nil]
|
|
116
129
|
end
|
|
@@ -209,7 +222,7 @@ module Rigor
|
|
|
209
222
|
# `collect_trait_entries` fire just as if the calls had been
|
|
210
223
|
# written directly in X's body.
|
|
211
224
|
def collect_concern_re_targeted_entries(entries, call_node, class_name, concern_index, # rubocop:disable Metrics/ParameterLists
|
|
212
|
-
templates, registries, hierarchy, environment, path)
|
|
225
|
+
templates, registries, hierarchy, environment, path, fact_store = nil)
|
|
213
226
|
return unless call_node.name == :include && call_node.receiver.nil?
|
|
214
227
|
return if concern_index.empty?
|
|
215
228
|
|
|
@@ -220,7 +233,7 @@ module Rigor
|
|
|
220
233
|
next unless deferred
|
|
221
234
|
|
|
222
235
|
deferred.each do |inner_call|
|
|
223
|
-
collect_entries(entries, templates, class_name, inner_call, hierarchy, environment, path)
|
|
236
|
+
collect_entries(entries, templates, class_name, inner_call, hierarchy, environment, path, fact_store)
|
|
224
237
|
collect_trait_entries(entries, registries, class_name, inner_call, hierarchy, environment, path)
|
|
225
238
|
end
|
|
226
239
|
end
|
|
@@ -318,7 +331,7 @@ module Rigor
|
|
|
318
331
|
parent_str ? "#{parent_str}::#{name}" : name
|
|
319
332
|
end
|
|
320
333
|
|
|
321
|
-
def collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path)
|
|
334
|
+
def collect_entries(entries, templates, class_name, call_node, hierarchy, environment, path, fact_store = nil) # rubocop:disable Metrics/ParameterLists
|
|
322
335
|
templates.each do |(plugin_id, template)|
|
|
323
336
|
next unless call_node.name == template.method_name
|
|
324
337
|
next unless class_inherits_from?(class_name, template.receiver_constraint, hierarchy, environment)
|
|
@@ -326,7 +339,7 @@ module Rigor
|
|
|
326
339
|
symbol_arg = literal_symbol_arg(call_node, template.symbol_arg_position)
|
|
327
340
|
next if symbol_arg.nil?
|
|
328
341
|
|
|
329
|
-
emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node)
|
|
342
|
+
emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node, fact_store)
|
|
330
343
|
end
|
|
331
344
|
end
|
|
332
345
|
|
|
@@ -439,28 +452,31 @@ module Rigor
|
|
|
439
452
|
)
|
|
440
453
|
end
|
|
441
454
|
|
|
442
|
-
def emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node)
|
|
455
|
+
def emit_entries_for(entries, class_name, symbol_arg, template, plugin_id, path, call_node, fact_store = nil) # rubocop:disable Metrics/ParameterLists
|
|
443
456
|
template.emit.each do |row|
|
|
444
457
|
entries << build_synthetic_method(
|
|
445
458
|
class_name: class_name, name_arg: symbol_arg, row: row,
|
|
446
459
|
template: template, plugin_id: plugin_id, path: path, call_node: call_node,
|
|
447
|
-
kind: SyntheticMethod::INSTANCE
|
|
460
|
+
kind: SyntheticMethod::INSTANCE, fact_store: fact_store
|
|
448
461
|
)
|
|
449
462
|
end
|
|
450
463
|
template.class_level_emit.each do |row|
|
|
451
464
|
entries << build_synthetic_method(
|
|
452
465
|
class_name: class_name, name_arg: symbol_arg, row: row,
|
|
453
466
|
template: template, plugin_id: plugin_id, path: path, call_node: call_node,
|
|
454
|
-
kind: SyntheticMethod::SINGLETON
|
|
467
|
+
kind: SyntheticMethod::SINGLETON, fact_store: fact_store
|
|
455
468
|
)
|
|
456
469
|
end
|
|
457
470
|
end
|
|
458
471
|
|
|
459
|
-
|
|
472
|
+
# rubocop:disable Metrics/ParameterLists
|
|
473
|
+
def build_synthetic_method(class_name:, name_arg:, row:, template:, plugin_id:, path:, call_node:, kind:,
|
|
474
|
+
fact_store: nil)
|
|
475
|
+
# rubocop:enable Metrics/ParameterLists
|
|
460
476
|
SyntheticMethod.new(
|
|
461
477
|
class_name: class_name,
|
|
462
478
|
method_name: interpolate(row.name, name_arg).to_sym,
|
|
463
|
-
return_type: row
|
|
479
|
+
return_type: resolve_emit_return_type(row, call_node, fact_store),
|
|
464
480
|
kind: kind,
|
|
465
481
|
provenance: {
|
|
466
482
|
plugin_id: plugin_id,
|
|
@@ -472,6 +488,68 @@ module Rigor
|
|
|
472
488
|
)
|
|
473
489
|
end
|
|
474
490
|
|
|
491
|
+
# ADR-18 three-tier fallback for the synthetic method's
|
|
492
|
+
# `return_type` string:
|
|
493
|
+
#
|
|
494
|
+
# 1. When `row.returns_from_arg` is present AND the
|
|
495
|
+
# call-site argument at the declared position is a
|
|
496
|
+
# resolvable constant reference AND the fact_store
|
|
497
|
+
# has a matching value, use that as the return type.
|
|
498
|
+
# 2. Else if `row.returns` is a non-empty String, use it
|
|
499
|
+
# (the slice-6b static path).
|
|
500
|
+
# 3. Else use `"untyped"` so the dispatcher's
|
|
501
|
+
# `promote_via_return_type` sentinel chain yields
|
|
502
|
+
# `Dynamic[Top]`.
|
|
503
|
+
def resolve_emit_return_type(row, call_node, fact_store)
|
|
504
|
+
resolved = resolve_returns_from_arg(row.returns_from_arg, call_node, fact_store)
|
|
505
|
+
return resolved if resolved
|
|
506
|
+
return row.returns if row.returns
|
|
507
|
+
|
|
508
|
+
"untyped"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def resolve_returns_from_arg(returns_from_arg, call_node, fact_store)
|
|
512
|
+
return nil if returns_from_arg.nil?
|
|
513
|
+
|
|
514
|
+
source_rep = argument_source_representation(call_node, returns_from_arg.position)
|
|
515
|
+
return nil if source_rep.nil?
|
|
516
|
+
return nil if fact_store.nil?
|
|
517
|
+
|
|
518
|
+
fact = fact_store.read(plugin_id: returns_from_arg.plugin_id, name: returns_from_arg.fact)
|
|
519
|
+
return nil unless fact.is_a?(Hash)
|
|
520
|
+
|
|
521
|
+
fact[source_rep]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Extracts the source-text qualified-constant representation
|
|
525
|
+
# of the call's positional argument (e.g.,
|
|
526
|
+
# `"Types::String"`). Returns nil for non-constant shapes
|
|
527
|
+
# (literals, method chains, blocks, …). The floor
|
|
528
|
+
# intentionally accepts only ConstantReadNode /
|
|
529
|
+
# ConstantPathNode per ADR-18; chained-call argument
|
|
530
|
+
# resolution stays deferred.
|
|
531
|
+
def argument_source_representation(call_node, position)
|
|
532
|
+
args = call_node.arguments&.arguments
|
|
533
|
+
return nil if args.nil? || position >= args.size
|
|
534
|
+
|
|
535
|
+
node = args[position]
|
|
536
|
+
case node
|
|
537
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
538
|
+
when Prism::ConstantPathNode then qualified_constant_name(node)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def qualified_constant_name(node)
|
|
543
|
+
case node
|
|
544
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
545
|
+
when Prism::ConstantPathNode
|
|
546
|
+
parent_name = node.parent.nil? ? nil : qualified_constant_name(node.parent)
|
|
547
|
+
return nil if !node.parent.nil? && parent_name.nil?
|
|
548
|
+
|
|
549
|
+
parent_name.nil? ? node.name.to_s : "#{parent_name}::#{node.name}"
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
475
553
|
def interpolate(template_name, name_arg)
|
|
476
554
|
template_name.gsub(Rigor::Plugin::Macro::HeredocTemplate::NAME_PLACEHOLDER, name_arg.to_s)
|
|
477
555
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module LanguageServer
|
|
5
|
+
# Per-session virtual file table. The LSP server maintains the
|
|
6
|
+
# canonical view of every open buffer here; analysis (slice 4+)
|
|
7
|
+
# reads from this table instead of disk so in-flight edits are
|
|
8
|
+
# reflected immediately.
|
|
9
|
+
#
|
|
10
|
+
# Keyed by `DocumentUri` (LSP `file://...` URIs). v1 ships
|
|
11
|
+
# FULL text sync (LSP `TextDocumentSyncKind::Full = 1`) so each
|
|
12
|
+
# `didChange` carries the entire buffer text — there's no
|
|
13
|
+
# incremental edit application yet. Incremental sync is slice
|
|
14
|
+
# 10 (deferred per the design doc).
|
|
15
|
+
class BufferTable
|
|
16
|
+
# @!attribute uri [String] the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
|
|
17
|
+
# @!attribute bytes [String] the current full text of the buffer.
|
|
18
|
+
# @!attribute version [Integer] the monotonically increasing LSP version number.
|
|
19
|
+
Entry = Data.define(:uri, :bytes, :version)
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@entries = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Records a `textDocument/didOpen` event. Replaces any
|
|
26
|
+
# existing entry (LSP clients may re-open a previously closed
|
|
27
|
+
# URI; the new version is authoritative).
|
|
28
|
+
def open(uri:, bytes:, version:)
|
|
29
|
+
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Records a `textDocument/didChange` event under FULL sync.
|
|
33
|
+
# The full new buffer text replaces the entry. If the client
|
|
34
|
+
# sends a `didChange` for a URI that was never opened (spec
|
|
35
|
+
# violation), the entry is still created — defensive.
|
|
36
|
+
def change(uri:, bytes:, version:)
|
|
37
|
+
@entries[uri] = Entry.new(uri: uri, bytes: bytes, version: version)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Records a `textDocument/didClose` event. The entry is
|
|
41
|
+
# removed. Subsequent reads via `#[]` return nil.
|
|
42
|
+
def close(uri:)
|
|
43
|
+
@entries.delete(uri)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def [](uri)
|
|
47
|
+
@entries[uri]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def open?(uri)
|
|
51
|
+
@entries.key?(uri)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def size
|
|
55
|
+
@entries.size
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def uris
|
|
59
|
+
@entries.keys
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|