ruby-lsp 0.18.3 → 0.19.0

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/core_ext/uri.rb +9 -4
  4. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
  5. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +66 -8
  6. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +56 -32
  7. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +3 -2
  8. data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +32 -8
  9. data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +262 -0
  10. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  11. data/lib/ruby_indexer/test/classes_and_modules_test.rb +11 -0
  12. data/lib/ruby_indexer/test/constant_test.rb +8 -0
  13. data/lib/ruby_indexer/test/enhancements_test.rb +2 -0
  14. data/lib/ruby_indexer/test/instance_variables_test.rb +12 -0
  15. data/lib/ruby_indexer/test/method_test.rb +10 -0
  16. data/lib/ruby_indexer/test/rbs_indexer_test.rb +8 -0
  17. data/lib/ruby_indexer/test/reference_finder_test.rb +86 -0
  18. data/lib/ruby_lsp/addon.rb +79 -17
  19. data/lib/ruby_lsp/base_server.rb +6 -0
  20. data/lib/ruby_lsp/erb_document.rb +3 -2
  21. data/lib/ruby_lsp/global_state.rb +8 -0
  22. data/lib/ruby_lsp/internal.rb +3 -1
  23. data/lib/ruby_lsp/listeners/completion.rb +1 -1
  24. data/lib/ruby_lsp/listeners/hover.rb +57 -0
  25. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +24 -21
  26. data/lib/ruby_lsp/requests/completion_resolve.rb +29 -0
  27. data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
  28. data/lib/ruby_lsp/requests/rename.rb +189 -0
  29. data/lib/ruby_lsp/requests/support/common.rb +2 -2
  30. data/lib/ruby_lsp/requests/support/source_uri.rb +8 -1
  31. data/lib/ruby_lsp/scope.rb +47 -0
  32. data/lib/ruby_lsp/server.rb +64 -32
  33. data/lib/ruby_lsp/static_docs.rb +15 -0
  34. data/lib/ruby_lsp/store.rb +12 -0
  35. data/lib/ruby_lsp/test_helper.rb +1 -1
  36. data/lib/ruby_lsp/type_inferrer.rb +6 -1
  37. data/lib/ruby_lsp/utils.rb +3 -6
  38. data/static_docs/yield.md +81 -0
  39. metadata +19 -8
  40. data/lib/ruby_lsp/parameter_scope.rb +0 -33
@@ -26,7 +26,8 @@ require "ruby_lsp/base_server"
26
26
  require "ruby_indexer/ruby_indexer"
27
27
  require "core_ext/uri"
28
28
  require "ruby_lsp/utils"
29
- require "ruby_lsp/parameter_scope"
29
+ require "ruby_lsp/static_docs"
30
+ require "ruby_lsp/scope"
30
31
  require "ruby_lsp/global_state"
31
32
  require "ruby_lsp/server"
32
33
  require "ruby_lsp/type_inferrer"
@@ -80,3 +81,4 @@ require "ruby_lsp/requests/show_syntax_tree"
80
81
  require "ruby_lsp/requests/signature_help"
81
82
  require "ruby_lsp/requests/type_hierarchy_supertypes"
82
83
  require "ruby_lsp/requests/workspace_symbol"
84
+ require "ruby_lsp/requests/rename"
@@ -438,7 +438,7 @@ module RubyLsp
438
438
  text_edit: Interface::TextEdit.new(range: range, new_text: keyword),
439
439
  kind: Constant::CompletionItemKind::KEYWORD,
440
440
  data: {
441
- skip_resolve: true,
441
+ keyword: true,
442
442
  },
443
443
  )
444
444
  end
@@ -21,8 +21,10 @@ module RubyLsp
21
21
  Prism::InstanceVariableWriteNode,
22
22
  Prism::SymbolNode,
23
23
  Prism::StringNode,
24
+ Prism::InterpolatedStringNode,
24
25
  Prism::SuperNode,
25
26
  Prism::ForwardingSuperNode,
27
+ Prism::YieldNode,
26
28
  ],
27
29
  T::Array[T.class_of(Prism::Node)],
28
30
  )
@@ -68,9 +70,22 @@ module RubyLsp
68
70
  :on_instance_variable_target_node_enter,
69
71
  :on_super_node_enter,
70
72
  :on_forwarding_super_node_enter,
73
+ :on_string_node_enter,
74
+ :on_interpolated_string_node_enter,
75
+ :on_yield_node_enter,
71
76
  )
72
77
  end
73
78
 
79
+ sig { params(node: Prism::StringNode).void }
80
+ def on_string_node_enter(node)
81
+ generate_heredoc_hover(node)
82
+ end
83
+
84
+ sig { params(node: Prism::InterpolatedStringNode).void }
85
+ def on_interpolated_string_node_enter(node)
86
+ generate_heredoc_hover(node)
87
+ end
88
+
74
89
  sig { params(node: Prism::ConstantReadNode).void }
75
90
  def on_constant_read_node_enter(node)
76
91
  return if @sorbet_level != RubyDocument::SorbetLevel::Ignore
@@ -153,8 +168,50 @@ module RubyLsp
153
168
  handle_super_node_hover
154
169
  end
155
170
 
171
+ sig { params(node: Prism::YieldNode).void }
172
+ def on_yield_node_enter(node)
173
+ handle_keyword_documentation(node.keyword)
174
+ end
175
+
156
176
  private
157
177
 
178
+ sig { params(node: T.any(Prism::InterpolatedStringNode, Prism::StringNode)).void }
179
+ def generate_heredoc_hover(node)
180
+ return unless node.heredoc?
181
+
182
+ opening_content = node.opening_loc&.slice
183
+ return unless opening_content
184
+
185
+ match = /(<<(?<type>(-|~)?))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(opening_content)
186
+ return unless match
187
+
188
+ heredoc_delimiter = match.named_captures["delimiter"]
189
+
190
+ if heredoc_delimiter
191
+ message = if match["type"] == "~"
192
+ "This is a squiggly heredoc definition using the `#{heredoc_delimiter}` delimiter. " \
193
+ "Indentation will be ignored in the resulting string."
194
+ else
195
+ "This is a heredoc definition using the `#{heredoc_delimiter}` delimiter. " \
196
+ "Indentation will be considered part of the string."
197
+ end
198
+
199
+ @response_builder.push(message, category: :documentation)
200
+ end
201
+ end
202
+
203
+ sig { params(keyword: String).void }
204
+ def handle_keyword_documentation(keyword)
205
+ content = KEYWORD_DOCS[keyword]
206
+ return unless content
207
+
208
+ doc_path = File.join(STATIC_DOCS_PATH, "#{keyword}.md")
209
+
210
+ @response_builder.push("```ruby\n#{keyword}\n```", category: :title)
211
+ @response_builder.push("[Read more](#{doc_path})", category: :links)
212
+ @response_builder.push(content, category: :documentation)
213
+ end
214
+
158
215
  sig { void }
159
216
  def handle_super_node_hover
160
217
  # Sorbet can handle super hover on typed true or higher
@@ -27,7 +27,7 @@ module RubyLsp
27
27
  def initialize(dispatcher, response_builder)
28
28
  @response_builder = response_builder
29
29
  @special_methods = T.let(nil, T.nilable(T::Array[String]))
30
- @current_scope = T.let(ParameterScope.new, ParameterScope)
30
+ @current_scope = T.let(Scope.new, Scope)
31
31
  @inside_regex_capture = T.let(false, T::Boolean)
32
32
  @inside_implicit_node = T.let(false, T::Boolean)
33
33
 
@@ -102,7 +102,7 @@ module RubyLsp
102
102
 
103
103
  sig { params(node: Prism::DefNode).void }
104
104
  def on_def_node_enter(node)
105
- @current_scope = ParameterScope.new(@current_scope)
105
+ @current_scope = Scope.new(@current_scope)
106
106
  end
107
107
 
108
108
  sig { params(node: Prism::DefNode).void }
@@ -112,7 +112,7 @@ module RubyLsp
112
112
 
113
113
  sig { params(node: Prism::BlockNode).void }
114
114
  def on_block_node_enter(node)
115
- @current_scope = ParameterScope.new(@current_scope)
115
+ @current_scope = Scope.new(@current_scope)
116
116
  end
117
117
 
118
118
  sig { params(node: Prism::BlockNode).void }
@@ -128,39 +128,39 @@ module RubyLsp
128
128
  sig { params(node: Prism::BlockParameterNode).void }
129
129
  def on_block_parameter_node_enter(node)
130
130
  name = node.name
131
- @current_scope << name.to_sym if name
131
+ @current_scope.add(name.to_sym, :parameter) if name
132
132
  end
133
133
 
134
134
  sig { params(node: Prism::RequiredKeywordParameterNode).void }
135
135
  def on_required_keyword_parameter_node_enter(node)
136
- @current_scope << node.name
136
+ @current_scope.add(node.name, :parameter)
137
137
  end
138
138
 
139
139
  sig { params(node: Prism::OptionalKeywordParameterNode).void }
140
140
  def on_optional_keyword_parameter_node_enter(node)
141
- @current_scope << node.name
141
+ @current_scope.add(node.name, :parameter)
142
142
  end
143
143
 
144
144
  sig { params(node: Prism::KeywordRestParameterNode).void }
145
145
  def on_keyword_rest_parameter_node_enter(node)
146
146
  name = node.name
147
- @current_scope << name.to_sym if name
147
+ @current_scope.add(name.to_sym, :parameter) if name
148
148
  end
149
149
 
150
150
  sig { params(node: Prism::OptionalParameterNode).void }
151
151
  def on_optional_parameter_node_enter(node)
152
- @current_scope << node.name
152
+ @current_scope.add(node.name, :parameter)
153
153
  end
154
154
 
155
155
  sig { params(node: Prism::RequiredParameterNode).void }
156
156
  def on_required_parameter_node_enter(node)
157
- @current_scope << node.name
157
+ @current_scope.add(node.name, :parameter)
158
158
  end
159
159
 
160
160
  sig { params(node: Prism::RestParameterNode).void }
161
161
  def on_rest_parameter_node_enter(node)
162
162
  name = node.name
163
- @current_scope << name.to_sym if name
163
+ @current_scope.add(name.to_sym, :parameter) if name
164
164
  end
165
165
 
166
166
  sig { params(node: Prism::SelfNode).void }
@@ -170,8 +170,8 @@ module RubyLsp
170
170
 
171
171
  sig { params(node: Prism::LocalVariableWriteNode).void }
172
172
  def on_local_variable_write_node_enter(node)
173
- type = @current_scope.type_for(node.name)
174
- @response_builder.add_token(node.name_loc, type) if type == :parameter
173
+ local = @current_scope.lookup(node.name)
174
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
175
175
  end
176
176
 
177
177
  sig { params(node: Prism::LocalVariableReadNode).void }
@@ -184,25 +184,26 @@ module RubyLsp
184
184
  return
185
185
  end
186
186
 
187
- @response_builder.add_token(node.location, @current_scope.type_for(node.name))
187
+ local = @current_scope.lookup(node.name)
188
+ @response_builder.add_token(node.location, local&.type || :variable)
188
189
  end
189
190
 
190
191
  sig { params(node: Prism::LocalVariableAndWriteNode).void }
191
192
  def on_local_variable_and_write_node_enter(node)
192
- type = @current_scope.type_for(node.name)
193
- @response_builder.add_token(node.name_loc, type) if type == :parameter
193
+ local = @current_scope.lookup(node.name)
194
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
194
195
  end
195
196
 
196
197
  sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
197
198
  def on_local_variable_operator_write_node_enter(node)
198
- type = @current_scope.type_for(node.name)
199
- @response_builder.add_token(node.name_loc, type) if type == :parameter
199
+ local = @current_scope.lookup(node.name)
200
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
200
201
  end
201
202
 
202
203
  sig { params(node: Prism::LocalVariableOrWriteNode).void }
203
204
  def on_local_variable_or_write_node_enter(node)
204
- type = @current_scope.type_for(node.name)
205
- @response_builder.add_token(node.name_loc, type) if type == :parameter
205
+ local = @current_scope.lookup(node.name)
206
+ @response_builder.add_token(node.name_loc, :parameter) if local&.type == :parameter
206
207
  end
207
208
 
208
209
  sig { params(node: Prism::LocalVariableTargetNode).void }
@@ -213,7 +214,8 @@ module RubyLsp
213
214
  # prevent pushing local variable target tokens. See https://github.com/ruby/prism/issues/1912
214
215
  return if @inside_regex_capture
215
216
 
216
- @response_builder.add_token(node.location, @current_scope.type_for(node.name))
217
+ local = @current_scope.lookup(node.name)
218
+ @response_builder.add_token(node.location, local&.type || :variable)
217
219
  end
218
220
 
219
221
  sig { params(node: Prism::ClassNode).void }
@@ -311,7 +313,8 @@ module RubyLsp
311
313
  capture_name_offset = T.must(content.index("(?<#{name}>")) + 3
312
314
  local_var_loc = loc.copy(start_offset: loc.start_offset + capture_name_offset, length: name.length)
313
315
 
314
- @response_builder.add_token(local_var_loc, @current_scope.type_for(name))
316
+ local = @current_scope.lookup(name)
317
+ @response_builder.add_token(local_var_loc, local&.type || :variable)
315
318
  end
316
319
  end
317
320
  end
@@ -40,6 +40,8 @@ module RubyLsp
40
40
  # For example, forgetting to return the `insertText` included in the original item will make the editor use the
41
41
  # `label` for the text edit instead
42
42
  label = @item[:label].dup
43
+ return keyword_resolve(@item) if @item.dig(:data, :keyword)
44
+
43
45
  entries = @index[label] || []
44
46
 
45
47
  owner_name = @item.dig(:data, :owner_name)
@@ -72,6 +74,33 @@ module RubyLsp
72
74
 
73
75
  @item
74
76
  end
77
+
78
+ private
79
+
80
+ sig { params(item: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
81
+ def keyword_resolve(item)
82
+ keyword = item[:label]
83
+ content = KEYWORD_DOCS[keyword]
84
+
85
+ if content
86
+ doc_path = File.join(STATIC_DOCS_PATH, "#{keyword}.md")
87
+
88
+ @item[:documentation] = Interface::MarkupContent.new(
89
+ kind: "markdown",
90
+ value: <<~MARKDOWN.chomp,
91
+ ```ruby
92
+ #{keyword}
93
+ ```
94
+
95
+ [Read more](#{doc_path})
96
+
97
+ #{content}
98
+ MARKDOWN
99
+ )
100
+ end
101
+
102
+ item
103
+ end
75
104
  end
76
105
  end
77
106
  end
@@ -63,7 +63,7 @@ module RubyLsp
63
63
  if (comment_match = @previous_line.match(/^#(\s*)/))
64
64
  handle_comment_line(T.must(comment_match[1]))
65
65
  elsif @document.syntax_error?
66
- match = /(?<=<<(-|~))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(@previous_line)
66
+ match = /(<<((-|~)?))(?<quote>['"`]?)(?<delimiter>\w+)\k<quote>/.match(@previous_line)
67
67
  heredoc_delimiter = match && match.named_captures["delimiter"]
68
68
 
69
69
  if heredoc_delimiter
@@ -0,0 +1,189 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The
7
+ # [rename](https://microsoft.github.io/language-server-protocol/specification#textDocument_rename)
8
+ # request renames all instances of a symbol in a document.
9
+ class Rename < Request
10
+ extend T::Sig
11
+ include Support::Common
12
+
13
+ class InvalidNameError < StandardError; end
14
+
15
+ sig do
16
+ params(
17
+ global_state: GlobalState,
18
+ store: Store,
19
+ document: T.any(RubyDocument, ERBDocument),
20
+ params: T::Hash[Symbol, T.untyped],
21
+ ).void
22
+ end
23
+ def initialize(global_state, store, document, params)
24
+ super()
25
+ @global_state = global_state
26
+ @store = store
27
+ @document = document
28
+ @position = T.let(params[:position], T::Hash[Symbol, Integer])
29
+ @new_name = T.let(params[:newName], String)
30
+ end
31
+
32
+ sig { override.returns(T.nilable(Interface::WorkspaceEdit)) }
33
+ def perform
34
+ char_position = @document.create_scanner.find_char_position(@position)
35
+
36
+ node_context = RubyDocument.locate(
37
+ @document.parse_result.value,
38
+ char_position,
39
+ node_types: [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode],
40
+ )
41
+ target = node_context.node
42
+ parent = node_context.parent
43
+ return if !target || target.is_a?(Prism::ProgramNode)
44
+
45
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
46
+ target = determine_target(
47
+ target,
48
+ parent,
49
+ @position,
50
+ )
51
+ end
52
+
53
+ target = T.cast(
54
+ target,
55
+ T.any(Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode),
56
+ )
57
+
58
+ name = constant_name(target)
59
+ return unless name
60
+
61
+ entries = @global_state.index.resolve(name, node_context.nesting)
62
+ return unless entries
63
+
64
+ if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
65
+ raise InvalidNameError, "The new name is already in use by #{T.must(conflict_entries.first).name}"
66
+ end
67
+
68
+ fully_qualified_name = T.must(entries.first).name
69
+ changes = collect_text_edits(fully_qualified_name, name)
70
+
71
+ # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
72
+ # text changes
73
+ unless @global_state.supported_resource_operations.include?("rename")
74
+ return Interface::WorkspaceEdit.new(changes: changes)
75
+ end
76
+
77
+ # Text edits must be applied before any resource operations, such as renaming files. Otherwise, the file is
78
+ # renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
79
+ document_changes = changes.map do |uri, edits|
80
+ Interface::TextDocumentEdit.new(
81
+ text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
82
+ edits: edits,
83
+ )
84
+ end
85
+
86
+ collect_file_renames(fully_qualified_name, document_changes)
87
+ Interface::WorkspaceEdit.new(document_changes: document_changes)
88
+ end
89
+
90
+ private
91
+
92
+ sig do
93
+ params(
94
+ fully_qualified_name: String,
95
+ document_changes: T::Array[T.any(Interface::RenameFile, Interface::TextDocumentEdit)],
96
+ ).void
97
+ end
98
+ def collect_file_renames(fully_qualified_name, document_changes)
99
+ # Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
100
+ # rename the files for the user.
101
+ #
102
+ # We also look for an associated test file and rename it too
103
+ short_name = T.must(fully_qualified_name.split("::").last)
104
+
105
+ T.must(@global_state.index[fully_qualified_name]).each do |entry|
106
+ # Do not rename files that are not part of the workspace
107
+ next unless entry.file_path.start_with?(@global_state.workspace_path)
108
+
109
+ case entry
110
+ when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
111
+ RubyIndexer::Entry::ConstantAlias, RubyIndexer::Entry::UnresolvedConstantAlias
112
+
113
+ file_name = file_from_constant_name(short_name)
114
+
115
+ if "#{file_name}.rb" == entry.file_name
116
+ new_file_name = file_from_constant_name(T.must(@new_name.split("::").last))
117
+
118
+ old_uri = URI::Generic.from_path(path: entry.file_path).to_s
119
+ new_uri = URI::Generic.from_path(path: File.join(
120
+ File.dirname(entry.file_path),
121
+ "#{new_file_name}.rb",
122
+ )).to_s
123
+
124
+ document_changes << Interface::RenameFile.new(kind: "rename", old_uri: old_uri, new_uri: new_uri)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ sig { params(fully_qualified_name: String, name: String).returns(T::Hash[String, T::Array[Interface::TextEdit]]) }
131
+ def collect_text_edits(fully_qualified_name, name)
132
+ changes = {}
133
+
134
+ Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
135
+ uri = URI::Generic.from_path(path: path)
136
+ # If the document is being managed by the client, then we should use whatever is present in the store instead
137
+ # of reading from disk
138
+ next if @store.key?(uri)
139
+
140
+ parse_result = Prism.parse_file(path)
141
+ edits = collect_changes(fully_qualified_name, parse_result, name, uri)
142
+ changes[uri.to_s] = edits unless edits.empty?
143
+ end
144
+
145
+ @store.each do |uri, document|
146
+ edits = collect_changes(fully_qualified_name, document.parse_result, name, document.uri)
147
+ changes[uri] = edits unless edits.empty?
148
+ end
149
+
150
+ changes
151
+ end
152
+
153
+ sig do
154
+ params(
155
+ fully_qualified_name: String,
156
+ parse_result: Prism::ParseResult,
157
+ name: String,
158
+ uri: URI::Generic,
159
+ ).returns(T::Array[Interface::TextEdit])
160
+ end
161
+ def collect_changes(fully_qualified_name, parse_result, name, uri)
162
+ dispatcher = Prism::Dispatcher.new
163
+ finder = RubyIndexer::ReferenceFinder.new(fully_qualified_name, @global_state.index, dispatcher)
164
+ dispatcher.visit(parse_result.value)
165
+
166
+ finder.references.uniq(&:location).map do |reference|
167
+ adjust_reference_for_edit(name, reference)
168
+ end
169
+ end
170
+
171
+ sig { params(name: String, reference: RubyIndexer::ReferenceFinder::Reference).returns(Interface::TextEdit) }
172
+ def adjust_reference_for_edit(name, reference)
173
+ # The reference may include a namespace in front. We need to check if the rename new name includes namespaces
174
+ # and then adjust both the text and the location to produce the correct edit
175
+ location = reference.location
176
+ new_text = reference.name.sub(name, @new_name)
177
+
178
+ Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
179
+ end
180
+
181
+ sig { params(constant_name: String).returns(String) }
182
+ def file_from_constant_name(constant_name)
183
+ constant_name
184
+ .gsub(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/, '\1\3_\2\4')
185
+ .downcase
186
+ end
187
+ end
188
+ end
189
+ end
@@ -5,8 +5,8 @@ module RubyLsp
5
5
  module Requests
6
6
  module Support
7
7
  module Common
8
- # WARNING: Methods in this class may be used by Ruby LSP addons such as
9
- # https://github.com/Shopify/ruby-lsp-rails, or addons by created by developers outside of Shopify, so be
8
+ # WARNING: Methods in this class may be used by Ruby LSP add-ons such as
9
+ # https://github.com/Shopify/ruby-lsp-rails, or add-ons by created by developers outside of Shopify, so be
10
10
  # cautious of changing anything.
11
11
  extend T::Sig
12
12
  extend T::Helpers
@@ -19,6 +19,13 @@ module URI
19
19
  T::Array[Symbol],
20
20
  )
21
21
 
22
+ # `uri` for Ruby 3.4 switched the default parser from RFC2396 to RFC3986. The new parser emits a deprecation
23
+ # warning on a few methods and delegates them to RFC2396, namely `extract`/`make_regexp`/`escape`/`unescape`.
24
+ # On earlier versions of the uri gem, the RFC2396_PARSER constant doesn't exist, so it needs some special
25
+ # handling to select a parser that doesn't emit deprecations. While it was backported to Ruby 3.1, users may
26
+ # have the uri gem in their own bundle and thus not use a compatible version.
27
+ PARSER = T.let(const_defined?(:RFC2396_PARSER) ? RFC2396_PARSER : DEFAULT_PARSER, RFC2396_Parser)
28
+
22
29
  T.unsafe(self).alias_method(:gem_name, :host)
23
30
  T.unsafe(self).alias_method(:line_number, :fragment)
24
31
 
@@ -41,7 +48,7 @@ module URI
41
48
  {
42
49
  scheme: "source",
43
50
  host: gem_name,
44
- path: DEFAULT_PARSER.escape("/#{gem_version}/#{path}"),
51
+ path: PARSER.escape("/#{gem_version}/#{path}"),
45
52
  fragment: line_number,
46
53
  }
47
54
  )
@@ -0,0 +1,47 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ class Scope
6
+ extend T::Sig
7
+
8
+ sig { returns(T.nilable(Scope)) }
9
+ attr_reader :parent
10
+
11
+ sig { params(parent: T.nilable(Scope)).void }
12
+ def initialize(parent = nil)
13
+ @parent = parent
14
+
15
+ # A hash of name => type
16
+ @locals = T.let({}, T::Hash[Symbol, Local])
17
+ end
18
+
19
+ # Add a new local to this scope. The types should only be `:parameter` or `:variable`
20
+ sig { params(name: T.any(String, Symbol), type: Symbol).void }
21
+ def add(name, type)
22
+ @locals[name.to_sym] = Local.new(type)
23
+ end
24
+
25
+ sig { params(name: T.any(String, Symbol)).returns(T.nilable(Local)) }
26
+ def lookup(name)
27
+ sym = name.to_sym
28
+ entry = @locals[sym]
29
+ return entry if entry
30
+ return unless @parent
31
+
32
+ @parent.lookup(sym)
33
+ end
34
+
35
+ class Local
36
+ extend T::Sig
37
+
38
+ sig { returns(Symbol) }
39
+ attr_reader :type
40
+
41
+ sig { params(type: Symbol).void }
42
+ def initialize(type)
43
+ @type = type
44
+ end
45
+ end
46
+ end
47
+ end