ruby-lsp 0.17.1 → 0.17.3

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.
@@ -128,6 +128,7 @@ module RubyLsp
128
128
  closest = node
129
129
  parent = T.let(nil, T.nilable(Prism::Node))
130
130
  nesting = T.let([], T::Array[T.any(Prism::ClassNode, Prism::ModuleNode)])
131
+ call_node = T.let(nil, T.nilable(Prism::CallNode))
131
132
 
132
133
  until queue.empty?
133
134
  candidate = queue.shift
@@ -159,6 +160,15 @@ module RubyLsp
159
160
  nesting << candidate
160
161
  end
161
162
 
163
+ if candidate.is_a?(Prism::CallNode)
164
+ arg_loc = candidate.arguments&.location
165
+ blk_loc = candidate.block&.location
166
+ if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
167
+ (blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
168
+ call_node = candidate
169
+ end
170
+ end
171
+
162
172
  # If there are node types to filter by, and the current node is not one of those types, then skip it
163
173
  next if node_types.any? && node_types.none? { |type| candidate.class == type }
164
174
 
@@ -170,7 +180,20 @@ module RubyLsp
170
180
  end
171
181
  end
172
182
 
173
- NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice })
183
+ # When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That
184
+ # is, when targeting Bar in the following example:
185
+ #
186
+ # ```ruby
187
+ # class Foo::Bar; end
188
+ # ```
189
+ # The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
190
+ # though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
191
+ if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
192
+ last_level = nesting.last
193
+ nesting.pop if last_level && last_level.constant_path == closest
194
+ end
195
+
196
+ NodeContext.new(closest, parent, nesting.map { |n| n.constant_path.location.slice }, call_node)
174
197
  end
175
198
 
176
199
  sig { returns(T::Boolean) }
@@ -54,18 +54,19 @@ module RubyLsp
54
54
 
55
55
  sig { params(options: T::Hash[Symbol, T.untyped]).void }
56
56
  def apply_options(options)
57
- dependencies = gather_dependencies
57
+ direct_dependencies = gather_direct_dependencies
58
+ all_dependencies = gather_direct_and_indirect_dependencies
58
59
  workspace_uri = options.dig(:workspaceFolders, 0, :uri)
59
60
  @workspace_uri = URI(workspace_uri) if workspace_uri
60
61
 
61
62
  specified_formatter = options.dig(:initializationOptions, :formatter)
62
63
  @formatter = specified_formatter if specified_formatter
63
- @formatter = detect_formatter(dependencies) if @formatter == "auto"
64
+ @formatter = detect_formatter(direct_dependencies, all_dependencies) if @formatter == "auto"
64
65
 
65
66
  specified_linters = options.dig(:initializationOptions, :linters)
66
- @linters = specified_linters || detect_linters(dependencies)
67
- @test_library = detect_test_library(dependencies)
68
- @typechecker = detect_typechecker(dependencies)
67
+ @linters = specified_linters || detect_linters(direct_dependencies)
68
+ @test_library = detect_test_library(direct_dependencies)
69
+ @typechecker = detect_typechecker(direct_dependencies)
69
70
 
70
71
  encodings = options.dig(:capabilities, :general, :positionEncodings)
71
72
  @encoding = if !encodings || encodings.empty?
@@ -103,16 +104,18 @@ module RubyLsp
103
104
 
104
105
  private
105
106
 
106
- sig { params(dependencies: T::Array[String]).returns(String) }
107
- def detect_formatter(dependencies)
107
+ sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
108
+ def detect_formatter(direct_dependencies, all_dependencies)
108
109
  # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
109
- if dependencies.any?(/^rubocop/)
110
- "rubocop"
111
- elsif dependencies.any?(/^syntax_tree$/)
112
- "syntax_tree"
113
- else
114
- "none"
115
- end
110
+ return "rubocop" if direct_dependencies.any?(/^rubocop/)
111
+
112
+ syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
113
+ return "syntax_tree" if syntax_tree_is_direct_dependency
114
+
115
+ rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
116
+ return "rubocop" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
117
+
118
+ "none"
116
119
  end
117
120
 
118
121
  # Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
@@ -132,7 +135,7 @@ module RubyLsp
132
135
  # by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
133
136
  # activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
134
137
  # of bin/rails to support these cases.
135
- elsif File.exist?(File.join(workspace_path, "bin/rails"))
138
+ elsif bin_rails_present
136
139
  "rails"
137
140
  # NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
138
141
  elsif dependencies.any?(/^minitest$/)
@@ -162,8 +165,18 @@ module RubyLsp
162
165
  false
163
166
  end
164
167
 
168
+ sig { returns(T::Boolean) }
169
+ def bin_rails_present
170
+ File.exist?(File.join(workspace_path, "bin/rails"))
171
+ end
172
+
173
+ sig { returns(T::Boolean) }
174
+ def dot_rubocop_yml_present
175
+ File.exist?(File.join(workspace_path, ".rubocop.yml"))
176
+ end
177
+
165
178
  sig { returns(T::Array[String]) }
166
- def gather_dependencies
179
+ def gather_direct_dependencies
167
180
  Bundler.with_original_env { Bundler.default_gemfile }
168
181
  Bundler.locked_gems.dependencies.keys + gemspec_dependencies
169
182
  rescue Bundler::GemfileNotFound
@@ -176,5 +189,13 @@ module RubyLsp
176
189
  .grep(Bundler::Source::Gemspec)
177
190
  .flat_map { _1.gemspec&.dependencies&.map(&:name) }
178
191
  end
192
+
193
+ sig { returns(T::Array[String]) }
194
+ def gather_direct_and_indirect_dependencies
195
+ Bundler.with_original_env { Bundler.default_gemfile }
196
+ Bundler.locked_gems.specs.map(&:name)
197
+ rescue Bundler::GemfileNotFound
198
+ []
199
+ end
179
200
  end
180
201
  end
@@ -18,6 +18,7 @@ require "set"
18
18
  require "prism"
19
19
  require "prism/visitor"
20
20
  require "language_server-protocol"
21
+ require "rbs"
21
22
 
22
23
  require "ruby-lsp"
23
24
  require "ruby_lsp/base_server"
@@ -236,7 +236,7 @@ module RubyLsp
236
236
  # so there must be something to the left of the available path.
237
237
  group_stack = T.must(group_stack[last_dynamic_reference_index + 1..])
238
238
  if method_name
239
- " --name " + "/::#{Shellwords.escape(group_stack.join("::") + "#" + method_name)}$/"
239
+ " --name " + "/::#{Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)}$/"
240
240
  else
241
241
  # When clicking on a CodeLens for `Test`, `(#|::)` will match all tests
242
242
  # that are registered on the class itself (matches after `#`) and all tests
@@ -245,7 +245,7 @@ module RubyLsp
245
245
  end
246
246
  elsif method_name
247
247
  # We know the entire path, do an exact match
248
- " --name " + Shellwords.escape(group_stack.join("::") + "#" + method_name)
248
+ " --name " + Shellwords.escape(group_stack.join("::")) + "#" + Shellwords.escape(method_name)
249
249
  elsif spec_name
250
250
  " --name " + "/#{Shellwords.escape(spec_name)}/"
251
251
  else
@@ -39,18 +39,27 @@ module RubyLsp
39
39
  :on_instance_variable_operator_write_node_enter,
40
40
  :on_instance_variable_or_write_node_enter,
41
41
  :on_instance_variable_target_node_enter,
42
+ :on_string_node_enter,
42
43
  )
43
44
  end
44
45
 
45
46
  sig { params(node: Prism::CallNode).void }
46
47
  def on_call_node_enter(node)
47
- message = node.name
48
+ message = node.message
49
+ return unless message
48
50
 
49
- if message == :require || message == :require_relative
50
- handle_require_definition(node)
51
- else
52
- handle_method_definition(message.to_s, self_receiver?(node))
53
- end
51
+ handle_method_definition(message, self_receiver?(node))
52
+ end
53
+
54
+ sig { params(node: Prism::StringNode).void }
55
+ def on_string_node_enter(node)
56
+ enclosing_call = @node_context.call_node
57
+ return unless enclosing_call
58
+
59
+ name = enclosing_call.name
60
+ return unless name == :require || name == :require_relative
61
+
62
+ handle_require_definition(node, name)
54
63
  end
55
64
 
56
65
  sig { params(node: Prism::BlockArgumentNode).void }
@@ -159,19 +168,12 @@ module RubyLsp
159
168
  end
160
169
  end
161
170
 
162
- sig { params(node: Prism::CallNode).void }
163
- def handle_require_definition(node)
164
- message = node.name
165
- arguments = node.arguments
166
- return unless arguments
167
-
168
- argument = arguments.arguments.first
169
- return unless argument.is_a?(Prism::StringNode)
170
-
171
+ sig { params(node: Prism::StringNode, message: Symbol).void }
172
+ def handle_require_definition(node, message)
171
173
  case message
172
174
  when :require
173
- entry = @index.search_require_paths(argument.content).find do |indexable_path|
174
- indexable_path.require_path == argument.content
175
+ entry = @index.search_require_paths(node.content).find do |indexable_path|
176
+ indexable_path.require_path == node.content
175
177
  end
176
178
 
177
179
  if entry
@@ -186,7 +188,7 @@ module RubyLsp
186
188
  )
187
189
  end
188
190
  when :require_relative
189
- required_file = "#{argument.content}.rb"
191
+ required_file = "#{node.content}.rb"
190
192
  path = @uri.to_standardized_path
191
193
  current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
192
194
  candidate = File.expand_path(File.join(current_folder, required_file))
@@ -19,6 +19,8 @@ module RubyLsp
19
19
  Prism::InstanceVariableOrWriteNode,
20
20
  Prism::InstanceVariableTargetNode,
21
21
  Prism::InstanceVariableWriteNode,
22
+ Prism::SymbolNode,
23
+ Prism::StringNode,
22
24
  ],
23
25
  T::Array[T.class_of(Prism::Node)],
24
26
  )
@@ -2,8 +2,8 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module RubyLsp
5
- # This class allows listeners to access contextual information about a node in the AST, such as its parent
6
- # and its namespace nesting.
5
+ # This class allows listeners to access contextual information about a node in the AST, such as its parent,
6
+ # its namespace nesting, and the surrounding CallNode (e.g. a method call).
7
7
  class NodeContext
8
8
  extend T::Sig
9
9
 
@@ -13,11 +13,22 @@ module RubyLsp
13
13
  sig { returns(T::Array[String]) }
14
14
  attr_reader :nesting
15
15
 
16
- sig { params(node: T.nilable(Prism::Node), parent: T.nilable(Prism::Node), nesting: T::Array[String]).void }
17
- def initialize(node, parent, nesting)
16
+ sig { returns(T.nilable(Prism::CallNode)) }
17
+ attr_reader :call_node
18
+
19
+ sig do
20
+ params(
21
+ node: T.nilable(Prism::Node),
22
+ parent: T.nilable(Prism::Node),
23
+ nesting: T::Array[String],
24
+ call_node: T.nilable(Prism::CallNode),
25
+ ).void
26
+ end
27
+ def initialize(node, parent, nesting, call_node)
18
28
  @node = node
19
29
  @parent = parent
20
30
  @nesting = nesting
31
+ @call_node = call_node
21
32
  end
22
33
 
23
34
  sig { returns(String) }
@@ -60,6 +60,8 @@ module RubyLsp
60
60
  Prism::InstanceVariableOrWriteNode,
61
61
  Prism::InstanceVariableTargetNode,
62
62
  Prism::InstanceVariableWriteNode,
63
+ Prism::SymbolNode,
64
+ Prism::StringNode,
63
65
  ],
64
66
  )
65
67
 
@@ -79,6 +81,9 @@ module RubyLsp
79
81
  # If the target is a method call, we need to ensure that the requested position is exactly on top of the
80
82
  # method identifier. Otherwise, we risk showing definitions for unrelated things
81
83
  target = nil
84
+ # For methods with block arguments using symbol-to-proc
85
+ elsif target.is_a?(Prism::SymbolNode) && parent.is_a?(Prism::BlockArgumentNode)
86
+ target = parent
82
87
  end
83
88
 
84
89
  if target
@@ -19,6 +19,16 @@ module RubyLsp
19
19
  T::Hash[Symbol, Integer],
20
20
  )
21
21
 
22
+ ENHANCED_DOC_URL = T.let(
23
+ begin
24
+ gem("rubocop", ">= 1.64.0")
25
+ true
26
+ rescue LoadError
27
+ false
28
+ end,
29
+ T::Boolean,
30
+ )
31
+
22
32
  # TODO: avoid passing document once we have alternative ways to get at
23
33
  # encoding and file source
24
34
  sig { params(document: Document, offense: RuboCop::Cop::Offense, uri: URI::Generic).void }
@@ -38,8 +48,8 @@ module RubyLsp
38
48
  code_actions
39
49
  end
40
50
 
41
- sig { returns(Interface::Diagnostic) }
42
- def to_lsp_diagnostic
51
+ sig { params(config: RuboCop::Config).returns(Interface::Diagnostic) }
52
+ def to_lsp_diagnostic(config)
43
53
  # highlighted_area contains the begin and end position of the first line
44
54
  # This ensures that multiline offenses don't clutter the editor
45
55
  highlighted = @offense.highlighted_area
@@ -47,7 +57,7 @@ module RubyLsp
47
57
  message: message,
48
58
  source: "RuboCop",
49
59
  code: @offense.cop_name,
50
- code_description: code_description,
60
+ code_description: code_description(config),
51
61
  severity: severity,
52
62
  range: Interface::Range.new(
53
63
  start: Interface::Position.new(
@@ -80,9 +90,16 @@ module RubyLsp
80
90
  RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name]
81
91
  end
82
92
 
83
- sig { returns(T.nilable(Interface::CodeDescription)) }
84
- def code_description
85
- doc_url = RuboCopRunner.find_cop_by_name(@offense.cop_name)&.documentation_url
93
+ sig { params(config: RuboCop::Config).returns(T.nilable(Interface::CodeDescription)) }
94
+ def code_description(config)
95
+ cop = RuboCopRunner.find_cop_by_name(@offense.cop_name)
96
+ return unless cop
97
+
98
+ doc_url = if ENHANCED_DOC_URL
99
+ cop.documentation_url(config)
100
+ else
101
+ cop.documentation_url
102
+ end
86
103
  Interface::CodeDescription.new(href: doc_url) if doc_url
87
104
  end
88
105
 
@@ -38,7 +38,11 @@ module RubyLsp
38
38
  @diagnostic_runner.run(filename, document.source)
39
39
 
40
40
  @diagnostic_runner.offenses.map do |offense|
41
- Support::RuboCopDiagnostic.new(document, offense, uri).to_lsp_diagnostic
41
+ Support::RuboCopDiagnostic.new(
42
+ document,
43
+ offense,
44
+ uri,
45
+ ).to_lsp_diagnostic(@diagnostic_runner.config_for_working_directory)
42
46
  end
43
47
  end
44
48
  end
@@ -50,6 +50,9 @@ module RubyLsp
50
50
  sig { returns(T::Array[RuboCop::Cop::Offense]) }
51
51
  attr_reader :offenses
52
52
 
53
+ sig { returns(::RuboCop::Config) }
54
+ attr_reader :config_for_working_directory
55
+
53
56
  DEFAULT_ARGS = T.let(
54
57
  [
55
58
  "--stderr", # Print any output to stderr so that our stdout does not get polluted
@@ -78,6 +81,7 @@ module RubyLsp
78
81
  args += DEFAULT_ARGS
79
82
  rubocop_options = ::RuboCop::Options.new.parse(args).first
80
83
  config_store = ::RuboCop::ConfigStore.new
84
+ @config_for_working_directory = T.let(config_store.for_pwd, ::RuboCop::Config)
81
85
 
82
86
  super(rubocop_options, config_store)
83
87
  end
@@ -33,10 +33,11 @@ module RubyLsp
33
33
  sig { override.returns(T::Array[Interface::WorkspaceSymbol]) }
34
34
  def perform
35
35
  @index.fuzzy_search(@query).filter_map do |entry|
36
- # If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, but
37
- # we still return entries defined in gems to allow developers to jump directly to the source
38
36
  file_path = entry.file_path
39
- next if @global_state.typechecker && not_in_dependencies?(file_path)
37
+
38
+ # We only show symbols declared in the workspace
39
+ in_dependencies = !not_in_dependencies?(file_path)
40
+ next if in_dependencies
40
41
 
41
42
  # We should never show private symbols when searching the entire workspace
42
43
  next if entry.private?
@@ -175,7 +175,7 @@ module RubyLsp
175
175
  completion_provider: completion_provider,
176
176
  code_lens_provider: code_lens_provider,
177
177
  definition_provider: enabled_features["definition"],
178
- workspace_symbol_provider: enabled_features["workspaceSymbol"],
178
+ workspace_symbol_provider: enabled_features["workspaceSymbol"] && !@global_state.typechecker,
179
179
  signature_help_provider: signature_help_provider,
180
180
  ),
181
181
  serverInfo: {
@@ -690,6 +690,17 @@ module RubyLsp
690
690
 
691
691
  sig { params(config_hash: T::Hash[String, T.untyped]).void }
692
692
  def perform_initial_indexing(config_hash)
693
+ index_ruby_core
694
+ index_ruby_code(config_hash)
695
+ end
696
+
697
+ sig { void }
698
+ def index_ruby_core
699
+ RubyIndexer::RBSIndexer.new(@global_state.index).index_ruby_core
700
+ end
701
+
702
+ sig { params(config_hash: T::Hash[String, T.untyped]).void }
703
+ def index_ruby_code(config_hash)
693
704
  # The begin progress invocation happens during `initialize`, so that the notification is sent before we are
694
705
  # stuck indexing files
695
706
  RubyIndexer.configuration.apply_config(config_hash)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-31 00:00:00.000000000 Z
11
+ date: 2024-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -33,7 +33,7 @@ dependencies:
33
33
  version: 0.29.0
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: '0.30'
36
+ version: '0.31'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -43,7 +43,27 @@ dependencies:
43
43
  version: 0.29.0
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.30'
46
+ version: '0.31'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rbs
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '4'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '3'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '4'
47
67
  - !ruby/object:Gem::Dependency
48
68
  name: sorbet-runtime
49
69
  requirement: !ruby/object:Gem::Requirement
@@ -85,6 +105,7 @@ files:
85
105
  - lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb
86
106
  - lib/ruby_indexer/lib/ruby_indexer/location.rb
87
107
  - lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb
108
+ - lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
88
109
  - lib/ruby_indexer/ruby_indexer.rb
89
110
  - lib/ruby_indexer/test/classes_and_modules_test.rb
90
111
  - lib/ruby_indexer/test/configuration_test.rb
@@ -93,6 +114,7 @@ files:
93
114
  - lib/ruby_indexer/test/instance_variables_test.rb
94
115
  - lib/ruby_indexer/test/method_test.rb
95
116
  - lib/ruby_indexer/test/prefix_tree_test.rb
117
+ - lib/ruby_indexer/test/rbs_indexer_test.rb
96
118
  - lib/ruby_indexer/test/test_case.rb
97
119
  - lib/ruby_lsp/addon.rb
98
120
  - lib/ruby_lsp/base_server.rb
@@ -180,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
180
202
  - !ruby/object:Gem::Version
181
203
  version: '0'
182
204
  requirements: []
183
- rubygems_version: 3.5.10
205
+ rubygems_version: 3.5.11
184
206
  signing_key:
185
207
  specification_version: 4
186
208
  summary: An opinionated language server for Ruby