rigortype 0.1.4 → 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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -56
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/fact_store.rb +15 -3
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/result.rb +11 -3
  11. data/lib/rigor/analysis/run_stats.rb +193 -0
  12. data/lib/rigor/analysis/runner.rb +681 -19
  13. data/lib/rigor/analysis/worker_session.rb +339 -0
  14. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  15. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  16. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  17. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  18. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  19. data/lib/rigor/cache/store.rb +72 -9
  20. data/lib/rigor/cli/lsp_command.rb +129 -0
  21. data/lib/rigor/cli/type_of_command.rb +44 -5
  22. data/lib/rigor/cli.rb +122 -10
  23. data/lib/rigor/configuration.rb +168 -7
  24. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  25. data/lib/rigor/environment/class_registry.rb +12 -3
  26. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  27. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  28. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  29. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  30. data/lib/rigor/environment/rbs_loader.rb +238 -7
  31. data/lib/rigor/environment/reflection.rb +152 -0
  32. data/lib/rigor/environment/reporters.rb +40 -0
  33. data/lib/rigor/environment.rb +179 -10
  34. data/lib/rigor/inference/acceptance.rb +83 -4
  35. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  36. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  37. data/lib/rigor/inference/expression_typer.rb +59 -2
  38. data/lib/rigor/inference/hkt_body.rb +171 -0
  39. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  40. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  41. data/lib/rigor/inference/hkt_registry.rb +223 -0
  42. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  44. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  45. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  46. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
  47. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  48. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  49. data/lib/rigor/inference/method_dispatcher.rb +282 -6
  50. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  51. data/lib/rigor/inference/narrowing.rb +127 -8
  52. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  53. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  54. data/lib/rigor/inference/scope_indexer.rb +156 -12
  55. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  56. data/lib/rigor/inference/synthetic_method.rb +86 -0
  57. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
  59. data/lib/rigor/language_server/buffer_table.rb +63 -0
  60. data/lib/rigor/language_server/completion_provider.rb +438 -0
  61. data/lib/rigor/language_server/debouncer.rb +86 -0
  62. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  63. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  64. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  65. data/lib/rigor/language_server/hover_provider.rb +74 -0
  66. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  67. data/lib/rigor/language_server/loop.rb +71 -0
  68. data/lib/rigor/language_server/project_context.rb +145 -0
  69. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  70. data/lib/rigor/language_server/server.rb +384 -0
  71. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  72. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  73. data/lib/rigor/language_server/uri.rb +40 -0
  74. data/lib/rigor/language_server.rb +29 -0
  75. data/lib/rigor/plugin/base.rb +63 -0
  76. data/lib/rigor/plugin/blueprint.rb +60 -0
  77. data/lib/rigor/plugin/loader.rb +3 -1
  78. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  79. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  80. data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
  81. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  82. data/lib/rigor/plugin/macro.rb +31 -0
  83. data/lib/rigor/plugin/manifest.rb +127 -9
  84. data/lib/rigor/plugin/registry.rb +51 -2
  85. data/lib/rigor/plugin.rb +1 -0
  86. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  87. data/lib/rigor/rbs_extended.rb +82 -2
  88. data/lib/rigor/sig_gen/generator.rb +12 -3
  89. data/lib/rigor/trinary.rb +15 -11
  90. data/lib/rigor/type/app.rb +107 -0
  91. data/lib/rigor/type/bot.rb +6 -3
  92. data/lib/rigor/type/combinator.rb +12 -1
  93. data/lib/rigor/type/integer_range.rb +7 -7
  94. data/lib/rigor/type/refined.rb +18 -12
  95. data/lib/rigor/type/top.rb +4 -3
  96. data/lib/rigor/type.rb +1 -0
  97. data/lib/rigor/type_node/generic.rb +7 -1
  98. data/lib/rigor/type_node/identifier.rb +9 -1
  99. data/lib/rigor/type_node/string_literal.rb +4 -1
  100. data/lib/rigor/version.rb +1 -1
  101. data/sig/rigor/environment.rbs +11 -4
  102. data/sig/rigor/inference.rbs +2 -0
  103. data/sig/rigor/plugin/blueprint.rbs +7 -0
  104. data/sig/rigor/plugin/manifest.rbs +1 -1
  105. data/sig/rigor/plugin/registry.rbs +14 -1
  106. data/sig/rigor.rbs +37 -2
  107. metadata +92 -1
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ require_relative "uri"
6
+ require_relative "../analysis/runner"
7
+ require_relative "../analysis/buffer_binding"
8
+
9
+ module Rigor
10
+ module LanguageServer
11
+ # Converts buffer state into `textDocument/publishDiagnostics`
12
+ # notifications. Owns the Rigor `Analysis::Runner` orchestration
13
+ # for the per-buffer single-file scope path that editor mode v1
14
+ # already supports — every `publish_for(uri)` call materialises
15
+ # a `BufferBinding` from the BufferTable entry, runs the Runner,
16
+ # and pushes the resulting LSP `Diagnostic[]` through the writer.
17
+ #
18
+ # Slice 4 is synchronous: each call blocks until analysis
19
+ # completes (typically 100-300ms warm). Debouncing (200ms
20
+ # quiet-time before publish) and Ractor-pool dispatch are
21
+ # queued for slice 4b / slice 8 respectively.
22
+ class DiagnosticPublisher
23
+ # Maps Rigor severity symbols to LSP DiagnosticSeverity
24
+ # integers per spec § "Diagnostic":
25
+ # 1 = Error, 2 = Warning, 3 = Information, 4 = Hint.
26
+ SEVERITY_MAP = {
27
+ error: 1,
28
+ warning: 2,
29
+ info: 3,
30
+ hint: 4
31
+ }.freeze
32
+
33
+ # @param debouncer [Rigor::LanguageServer::Debouncer, nil]
34
+ # when present, `publish_for` schedules its work through
35
+ # the debouncer (cancels prior pending task for the same
36
+ # URI, fires after `debounce_seconds` quiet-time). Nil
37
+ # keeps the slice 4-7 synchronous behaviour — primarily
38
+ # useful for specs.
39
+ # @param debounce_seconds [Numeric] quiet-time before the
40
+ # debounced publish fires. 0 with a debouncer means
41
+ # "schedule on next-tick" (still async); without a
42
+ # debouncer the value is unused.
43
+ def initialize(writer:, buffer_table:, project_context:,
44
+ debouncer: nil, debounce_seconds: 0.2)
45
+ @writer = writer
46
+ @buffer_table = buffer_table
47
+ @project_context = project_context
48
+ @debouncer = debouncer
49
+ @debounce_seconds = debounce_seconds
50
+ end
51
+
52
+ # Run analysis for the buffer at `uri` (looked up in the
53
+ # BufferTable) and push a `textDocument/publishDiagnostics`
54
+ # notification. No-op when the URI isn't a `file://` form or
55
+ # the buffer isn't currently open. When a Debouncer is wired,
56
+ # the analysis is scheduled async per the configured
57
+ # `debounce_seconds`; otherwise it runs inline.
58
+ def publish_for(uri)
59
+ path = Uri.to_path(uri)
60
+ return if path.nil?
61
+
62
+ if @debouncer
63
+ @debouncer.schedule(uri, delay: @debounce_seconds) { run_and_notify(uri, path) }
64
+ else
65
+ run_and_notify(uri, path)
66
+ end
67
+ end
68
+
69
+ # Publishes an EMPTY diagnostic array for `uri`. The LSP-spec
70
+ # idiom for "clear inline markers" — called from `didClose`
71
+ # so clients drop stale highlights when the user closes a
72
+ # buffer.
73
+ def publish_empty(uri)
74
+ notify(uri, [])
75
+ end
76
+
77
+ # Cancels every in-flight debounced task. Called from
78
+ # `Server#handle_shutdown` so pending publishes don't fire
79
+ # against a closed STDOUT.
80
+ def cancel_pending
81
+ @debouncer&.cancel_all
82
+ end
83
+
84
+ private
85
+
86
+ def run_and_notify(uri, path)
87
+ entry = @buffer_table[uri]
88
+ # The buffer may have been closed during the debounce
89
+ # window — drop the publish; the empty notification from
90
+ # didClose already cleared the markers.
91
+ return if entry.nil?
92
+
93
+ diagnostics = run_analysis(path: path, bytes: entry.bytes)
94
+ notify(uri, diagnostics)
95
+ end
96
+
97
+ # Runs `Analysis::Runner` with a `BufferBinding` so the buffer
98
+ # bytes (instead of the on-disk file) drive the parse. The
99
+ # `Rigor::Analysis::ProjectScan` cached on the ProjectContext
100
+ # is passed through `prebuilt:` so plugin `#prepare`, the
101
+ # dependency-source walker, and the synthetic-method /
102
+ # project-patched scanners do not re-run per publish. The
103
+ # snapshot rebuilds only when `ProjectContext#invalidate!`
104
+ # fires (watched-file or configuration change). Returns
105
+ # the LSP-shaped Diagnostic Array, ready to serialize into
106
+ # the notification's `params.diagnostics` field.
107
+ def run_analysis(path:, bytes:)
108
+ with_tempfile(bytes) do |tmp|
109
+ binding = Analysis::BufferBinding.new(logical_path: path, physical_path: tmp.path)
110
+ runner = Analysis::Runner.new(
111
+ configuration: @project_context.configuration,
112
+ cache_store: @project_context.cache_store,
113
+ collect_stats: false,
114
+ buffer: binding,
115
+ prebuilt: @project_context.project_scan,
116
+ environment: @project_context.environment
117
+ )
118
+ result = runner.run([path])
119
+ result.diagnostics.filter_map { |diagnostic| to_lsp_diagnostic(diagnostic, path) }
120
+ end
121
+ end
122
+
123
+ def with_tempfile(bytes)
124
+ tmp = Tempfile.new(["rigor-lsp-buffer-", ".rb"])
125
+ tmp.write(bytes)
126
+ tmp.flush
127
+ yield tmp
128
+ ensure
129
+ tmp&.close
130
+ tmp&.unlink
131
+ end
132
+
133
+ # @return [Hash, nil] the LSP `Diagnostic` Hash, or nil to
134
+ # skip diagnostics outside the buffer's own path (e.g.
135
+ # `.rigor.yml`-anchored info diagnostics get filtered —
136
+ # they belong to the project, not the buffer).
137
+ def to_lsp_diagnostic(diagnostic, buffer_path)
138
+ return nil if diagnostic.path != buffer_path
139
+
140
+ # Rigor uses 1-based line + 1-based byte column; LSP uses
141
+ # 0-based line + 0-based UTF-16 code unit. UTF-16 conversion
142
+ # is queued (design doc § "Open questions"); v1 emits byte
143
+ # columns which are correct for ASCII source.
144
+ line = (diagnostic.line - 1).clamp(0, Float::INFINITY).to_i
145
+ character = (diagnostic.column - 1).clamp(0, Float::INFINITY).to_i
146
+
147
+ {
148
+ range: {
149
+ start: { line: line, character: character },
150
+ end: { line: line, character: character }
151
+ },
152
+ severity: SEVERITY_MAP.fetch(diagnostic.severity, 3),
153
+ code: diagnostic.rule,
154
+ source: "rigor",
155
+ message: diagnostic.message
156
+ }.compact
157
+ end
158
+
159
+ def notify(uri, diagnostics)
160
+ @writer.write(
161
+ method: "textDocument/publishDiagnostics",
162
+ params: { uri: uri, diagnostics: diagnostics }
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "uri"
6
+
7
+ module Rigor
8
+ module LanguageServer
9
+ # Answers `textDocument/documentSymbol` requests by walking the
10
+ # Prism AST and emitting one LSP `DocumentSymbol` per
11
+ # `ClassNode` / `ModuleNode` / `DefNode`. Nested classes /
12
+ # modules / methods nest in the `children` array so the editor's
13
+ # outline tree mirrors the source structure.
14
+ #
15
+ # SymbolKind mapping (LSP § "SymbolKind"):
16
+ # - Class (5) — `class Foo`
17
+ # - Module (2) — `module Foo`
18
+ # - Method (6) — `def m` inside a class / module
19
+ # - Function (12) — `def m` at top-level (no enclosing class)
20
+ class DocumentSymbolProvider
21
+ KIND_MODULE = 2
22
+ KIND_CLASS = 5
23
+ KIND_METHOD = 6
24
+ KIND_FUNCTION = 12
25
+
26
+ def initialize(buffer_table:, project_context:)
27
+ @buffer_table = buffer_table
28
+ @project_context = project_context
29
+ end
30
+
31
+ # @return [Array<Hash>, nil] LSP `DocumentSymbol[]` for the
32
+ # buffer at `uri`. Returns nil when the URI isn't open or
33
+ # doesn't parse cleanly enough to surface symbols — LSP
34
+ # clients fall back to no-outline in that case.
35
+ def provide(uri)
36
+ path = Uri.to_path(uri)
37
+ return nil if path.nil?
38
+
39
+ entry = @buffer_table[uri]
40
+ return nil if entry.nil?
41
+
42
+ parse_result = Prism.parse(entry.bytes, filepath: path,
43
+ version: @project_context.configuration.target_ruby)
44
+ # Tolerate partial parse errors: walk what Prism gave us
45
+ # anyway. Editors prefer a stale outline over no outline.
46
+ walk_top_level(parse_result.value)
47
+ end
48
+
49
+ private
50
+
51
+ def walk_top_level(root)
52
+ symbols = []
53
+ each_decl(root, in_namespace: false) { |s| symbols << s }
54
+ symbols
55
+ end
56
+
57
+ def each_decl(node, in_namespace:, &block)
58
+ return unless node.is_a?(Prism::Node)
59
+
60
+ case node
61
+ when Prism::ClassNode
62
+ children = []
63
+ each_decl(node.body, in_namespace: true) { |child| children << child } if node.body
64
+ block.call(class_symbol(node, children))
65
+ when Prism::ModuleNode
66
+ children = []
67
+ each_decl(node.body, in_namespace: true) { |child| children << child } if node.body
68
+ block.call(module_symbol(node, children))
69
+ when Prism::DefNode
70
+ block.call(def_symbol(node, in_namespace: in_namespace))
71
+ else
72
+ node.compact_child_nodes.each do |child|
73
+ each_decl(child, in_namespace: in_namespace, &block)
74
+ end
75
+ end
76
+ end
77
+
78
+ def class_symbol(node, children)
79
+ {
80
+ name: qualified_name_of(node.constant_path),
81
+ kind: KIND_CLASS,
82
+ range: range_from(node.location),
83
+ selectionRange: range_from(node.constant_path.location),
84
+ children: children
85
+ }
86
+ end
87
+
88
+ def module_symbol(node, children)
89
+ {
90
+ name: qualified_name_of(node.constant_path),
91
+ kind: KIND_MODULE,
92
+ range: range_from(node.location),
93
+ selectionRange: range_from(node.constant_path.location),
94
+ children: children
95
+ }
96
+ end
97
+
98
+ def def_symbol(node, in_namespace:)
99
+ name = node.name.to_s
100
+ # `def self.foo` (singleton-class) → singleton-method shape.
101
+ # We surface the same as instance methods in v1 (LSP kind
102
+ # has no distinct "ClassMethod" code); the textual `self.`
103
+ # prefix preserves the distinction visually.
104
+ display_name = node.receiver.is_a?(Prism::SelfNode) ? "self.#{name}" : name
105
+ {
106
+ name: display_name,
107
+ kind: in_namespace ? KIND_METHOD : KIND_FUNCTION,
108
+ range: range_from(node.location),
109
+ selectionRange: range_from(node.name_loc),
110
+ children: []
111
+ }
112
+ end
113
+
114
+ # Renders a `ConstantReadNode` / `ConstantPathNode` as its
115
+ # fully-qualified name string (e.g. `Foo::Bar::Baz`). Returns
116
+ # the slot-name source when the node shape is unrecognised so
117
+ # the outline still has something to display.
118
+ def qualified_name_of(node)
119
+ case node
120
+ when Prism::ConstantReadNode
121
+ node.name.to_s
122
+ when Prism::ConstantPathNode
123
+ parent = node.parent.nil? ? nil : qualified_name_of(node.parent)
124
+ parent.nil? ? node.name.to_s : "#{parent}::#{node.name}"
125
+ else
126
+ node.respond_to?(:slice) ? node.slice : "<unknown>"
127
+ end
128
+ end
129
+
130
+ # LSP `Range` is 0-based start + end with `character` in
131
+ # UTF-16 code units. Slice 6 emits byte columns (correct for
132
+ # ASCII source); UTF-16 conversion stays queued per design
133
+ # doc § "Open questions".
134
+ def range_from(location)
135
+ {
136
+ start: { line: location.start_line - 1, character: location.start_column },
137
+ end: { line: location.end_line - 1, character: location.end_column }
138
+ }
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "uri"
6
+
7
+ module Rigor
8
+ module LanguageServer
9
+ # Answers `textDocument/foldingRange` requests. Walks the Prism
10
+ # AST and emits one `FoldingRange` per foldable construct:
11
+ # `class` / `module` / `def` / `singleton class << self` /
12
+ # block (`do…end` or `{…}`). Skips single-line constructs
13
+ # (start_line == end_line) since there's nothing to fold.
14
+ #
15
+ # Ranges are LSP 0-based. `startLine` is the line containing
16
+ # the opening keyword (`class`, `def`); `endLine` is the last
17
+ # line OF the body — one line before the `end` keyword — so
18
+ # collapsed view shows the opener intact and hides the body
19
+ # only.
20
+ class FoldingRangeProvider
21
+ def initialize(buffer_table:, project_context:)
22
+ @buffer_table = buffer_table
23
+ @project_context = project_context
24
+ end
25
+
26
+ # @return [Array<Hash>, nil] LSP `FoldingRange[]` for the
27
+ # buffer, or nil when the URI isn't open / parseable.
28
+ def provide(uri)
29
+ path = Uri.to_path(uri)
30
+ return nil if path.nil?
31
+
32
+ entry = @buffer_table[uri]
33
+ return nil if entry.nil?
34
+
35
+ parse_result = Prism.parse(entry.bytes, filepath: path,
36
+ version: @project_context.configuration.target_ruby)
37
+ # Tolerate partial parse errors — fold whatever AST Prism
38
+ # produced. Editors prefer a stale outline / fold map
39
+ # over none.
40
+ ranges = []
41
+ walk(parse_result.value, ranges)
42
+ ranges
43
+ end
44
+
45
+ private
46
+
47
+ def walk(node, ranges)
48
+ return unless node.is_a?(Prism::Node)
49
+
50
+ case node
51
+ when Prism::ClassNode, Prism::ModuleNode,
52
+ Prism::SingletonClassNode, Prism::DefNode,
53
+ Prism::BlockNode
54
+ add_range(node, ranges)
55
+ end
56
+ node.compact_child_nodes.each { |child| walk(child, ranges) }
57
+ end
58
+
59
+ def add_range(node, ranges)
60
+ loc = node.location
61
+ start_line = loc.start_line - 1
62
+ # `end_line` includes the line of the `end` keyword.
63
+ # Folding should hide the body and leave both the opener
64
+ # AND the `end` keyword visible — so endLine is one line
65
+ # before `end`. When the body is a single line, that
66
+ # makes start == end (one-line fold), which most editors
67
+ # skip; we filter those out.
68
+ end_line = loc.end_line - 2
69
+ return if end_line <= start_line
70
+
71
+ ranges << { startLine: start_line, endLine: end_line }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "uri"
6
+ require_relative "hover_renderer"
7
+ require_relative "../environment"
8
+ require_relative "../scope"
9
+ require_relative "../source/node_locator"
10
+ require_relative "../inference/scope_indexer"
11
+
12
+ module Rigor
13
+ module LanguageServer
14
+ # Answers `textDocument/hover` requests by running the same
15
+ # NodeLocator + ScopeIndexer + `Scope#type_of` chain that
16
+ # `rigor type-of` already drives. The LSP wraps the result in a
17
+ # `Hover` payload with markdown contents.
18
+ #
19
+ # Per LSP spec § "Position":
20
+ # - `line` and `character` are 0-based.
21
+ # - `character` counts UTF-16 code units; v1 emits byte counts
22
+ # for ASCII source (UTF-16 conversion is queued, see design
23
+ # doc § "Open questions").
24
+ class HoverProvider
25
+ def initialize(buffer_table:, project_context:, renderer: HoverRenderer.new)
26
+ @buffer_table = buffer_table
27
+ @project_context = project_context
28
+ @renderer = renderer
29
+ end
30
+
31
+ # @return [Hash, nil] an LSP `Hover` payload or nil when no
32
+ # expression sits at the queried position. Returning nil
33
+ # maps to `result: null` per the LSP spec — clients
34
+ # suppress the hover popup in that case.
35
+ def provide(uri:, line:, character:)
36
+ path = Uri.to_path(uri)
37
+ return nil if path.nil?
38
+
39
+ entry = @buffer_table[uri]
40
+ return nil if entry.nil?
41
+
42
+ parse_result = Prism.parse(entry.bytes, filepath: path, version: @project_context.configuration.target_ruby)
43
+ return nil unless parse_result.errors.empty?
44
+
45
+ # Rigor's NodeLocator uses 1-based line / column; LSP uses
46
+ # 0-based. Translate at the boundary.
47
+ node = locate_node(source: entry.bytes, root: parse_result.value, line: line + 1, character: character + 1)
48
+ return nil if node.nil?
49
+
50
+ scope = base_scope(path)
51
+ index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
52
+ node_scope = index[node]
53
+ type = node_scope.type_of(node)
54
+
55
+ @renderer.render(node: node, type: type, node_scope_lookup: index)
56
+ end
57
+
58
+ private
59
+
60
+ def locate_node(source:, root:, line:, character:)
61
+ Source::NodeLocator.at_position(source: source, root: root, line: line, column: character)
62
+ rescue Source::NodeLocator::OutOfRangeError
63
+ nil
64
+ end
65
+
66
+ def base_scope(_path)
67
+ # Slice 7: pull the Environment from the cached
68
+ # ProjectContext so hovers don't pay the RBS-load tax on
69
+ # every cursor stop.
70
+ Scope.empty(environment: @project_context.environment)
71
+ end
72
+ end
73
+ end
74
+ end