ruby-lsp 0.18.3 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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