ruby-lsp-rails 0.3.31 → 0.4.8

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.
@@ -9,19 +9,13 @@ module RubyLsp
9
9
  # request allows users to navigate between associations, validations, callbacks and ActiveSupport test cases with
10
10
  # VS Code's "Go to Symbol" feature.
11
11
  class DocumentSymbol
12
- extend T::Sig
13
12
  include Requests::Support::Common
14
13
  include ActiveSupportTestCaseHelper
15
14
 
16
- sig do
17
- params(
18
- response_builder: ResponseBuilders::DocumentSymbol,
19
- dispatcher: Prism::Dispatcher,
20
- ).void
21
- end
15
+ #: (ResponseBuilders::DocumentSymbol response_builder, Prism::Dispatcher dispatcher) -> void
22
16
  def initialize(response_builder, dispatcher)
23
17
  @response_builder = response_builder
24
- @namespace_stack = T.let([], T::Array[String])
18
+ @namespace_stack = [] #: Array[String]
25
19
 
26
20
  dispatcher.register(
27
21
  self,
@@ -33,7 +27,7 @@ module RubyLsp
33
27
  )
34
28
  end
35
29
 
36
- sig { params(node: Prism::CallNode).void }
30
+ #: (Prism::CallNode node) -> void
37
31
  def on_call_node_enter(node)
38
32
  return if @namespace_stack.empty?
39
33
 
@@ -51,50 +45,52 @@ module RubyLsp
51
45
  return if receiver && !receiver.is_a?(Prism::SelfNode)
52
46
 
53
47
  message = node.message
48
+ return unless message
49
+
54
50
  case message
55
51
  when *Support::Callbacks::ALL, "validate"
56
- handle_all_arg_types(node, T.must(message))
52
+ handle_all_arg_types(node, message)
57
53
  when "validates", "validates!", "validates_each", "belongs_to", "has_one", "has_many",
58
54
  "has_and_belongs_to_many", "attr_readonly", "scope"
59
- handle_symbol_and_string_arg_types(node, T.must(message))
55
+ handle_symbol_and_string_arg_types(node, message)
60
56
  when "validates_with"
61
- handle_class_arg_types(node, T.must(message))
57
+ handle_class_arg_types(node, message)
62
58
  end
63
59
  end
64
60
 
65
- sig { params(node: Prism::ClassNode).void }
61
+ #: (Prism::ClassNode node) -> void
66
62
  def on_class_node_enter(node)
67
63
  add_to_namespace_stack(node)
68
64
  end
69
65
 
70
- sig { params(node: Prism::ClassNode).void }
66
+ #: (Prism::ClassNode node) -> void
71
67
  def on_class_node_leave(node)
72
68
  remove_from_namespace_stack(node)
73
69
  end
74
70
 
75
- sig { params(node: Prism::ModuleNode).void }
71
+ #: (Prism::ModuleNode node) -> void
76
72
  def on_module_node_enter(node)
77
73
  add_to_namespace_stack(node)
78
74
  end
79
75
 
80
- sig { params(node: Prism::ModuleNode).void }
76
+ #: (Prism::ModuleNode node) -> void
81
77
  def on_module_node_leave(node)
82
78
  remove_from_namespace_stack(node)
83
79
  end
84
80
 
85
81
  private
86
82
 
87
- sig { params(node: T.any(Prism::ClassNode, Prism::ModuleNode)).void }
83
+ #: ((Prism::ClassNode | Prism::ModuleNode) node) -> void
88
84
  def add_to_namespace_stack(node)
89
85
  @namespace_stack << node.constant_path.slice
90
86
  end
91
87
 
92
- sig { params(node: T.any(Prism::ClassNode, Prism::ModuleNode)).void }
88
+ #: ((Prism::ClassNode | Prism::ModuleNode) node) -> void
93
89
  def remove_from_namespace_stack(node)
94
90
  @namespace_stack.delete(node.constant_path.slice)
95
91
  end
96
92
 
97
- sig { params(node: Prism::CallNode, message: String).void }
93
+ #: (Prism::CallNode node, String message) -> void
98
94
  def handle_all_arg_types(node, message)
99
95
  block = node.block
100
96
 
@@ -119,7 +115,9 @@ module RubyLsp
119
115
  append_document_symbol(
120
116
  name: "#{message} :#{name}",
121
117
  range: range_from_location(argument.location),
122
- selection_range: range_from_location(T.must(argument.value_loc)),
118
+ selection_range: range_from_location(
119
+ argument.value_loc, #: as !nil
120
+ ),
123
121
  )
124
122
  when Prism::StringNode
125
123
  name = argument.content
@@ -164,7 +162,7 @@ module RubyLsp
164
162
  end
165
163
  end
166
164
 
167
- sig { params(node: Prism::CallNode, message: String).void }
165
+ #: (Prism::CallNode node, String message) -> void
168
166
  def handle_symbol_and_string_arg_types(node, message)
169
167
  arguments = node.arguments&.arguments
170
168
  return unless arguments&.any?
@@ -178,7 +176,9 @@ module RubyLsp
178
176
  append_document_symbol(
179
177
  name: "#{message} :#{name}",
180
178
  range: range_from_location(argument.location),
181
- selection_range: range_from_location(T.must(argument.value_loc)),
179
+ selection_range: range_from_location(
180
+ argument.value_loc, #: as !nil
181
+ ),
182
182
  )
183
183
  when Prism::StringNode
184
184
  name = argument.content
@@ -193,7 +193,7 @@ module RubyLsp
193
193
  end
194
194
  end
195
195
 
196
- sig { params(node: Prism::CallNode, message: String).void }
196
+ #: (Prism::CallNode node, String message) -> void
197
197
  def handle_class_arg_types(node, message)
198
198
  arguments = node.arguments&.arguments
199
199
  return unless arguments&.any?
@@ -213,13 +213,7 @@ module RubyLsp
213
213
  end
214
214
  end
215
215
 
216
- sig do
217
- params(
218
- name: String,
219
- range: RubyLsp::Interface::Range,
220
- selection_range: RubyLsp::Interface::Range,
221
- ).void
222
- end
216
+ #: (name: String, range: RubyLsp::Interface::Range, selection_range: RubyLsp::Interface::Range) -> void
223
217
  def append_document_symbol(name:, range:, selection_range:)
224
218
  @response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
225
219
  name: name,
@@ -15,46 +15,50 @@ module RubyLsp
15
15
  # # ^ hovering here will show information about the User model
16
16
  # ```
17
17
  class Hover
18
- extend T::Sig
19
18
  include Requests::Support::Common
20
19
 
21
- sig do
22
- params(
23
- client: RunnerClient,
24
- response_builder: ResponseBuilders::Hover,
25
- node_context: NodeContext,
26
- global_state: GlobalState,
27
- dispatcher: Prism::Dispatcher,
28
- ).void
29
- end
20
+ #: (RunnerClient client, ResponseBuilders::Hover response_builder, NodeContext node_context, GlobalState global_state, Prism::Dispatcher dispatcher) -> void
30
21
  def initialize(client, response_builder, node_context, global_state, dispatcher)
31
22
  @client = client
32
23
  @response_builder = response_builder
33
- @nesting = T.let(node_context.nesting, T::Array[String])
34
- @index = T.let(global_state.index, RubyIndexer::Index)
35
- dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
24
+ @node_context = node_context
25
+ @nesting = node_context.nesting #: Array[String]
26
+ @index = global_state.index #: RubyIndexer::Index
27
+ dispatcher.register(
28
+ self,
29
+ :on_constant_path_node_enter,
30
+ :on_constant_read_node_enter,
31
+ :on_symbol_node_enter,
32
+ )
36
33
  end
37
34
 
38
- sig { params(node: Prism::ConstantPathNode).void }
35
+ #: (Prism::ConstantPathNode node) -> void
39
36
  def on_constant_path_node_enter(node)
40
37
  entries = @index.resolve(node.slice, @nesting)
41
- return unless entries
38
+ item = entries&.first
39
+ return unless item
42
40
 
43
- name = T.must(entries.first).name
41
+ name = item.name
44
42
  generate_column_content(name)
45
43
  end
46
44
 
47
- sig { params(node: Prism::ConstantReadNode).void }
45
+ #: (Prism::ConstantReadNode node) -> void
48
46
  def on_constant_read_node_enter(node)
49
47
  entries = @index.resolve(node.name.to_s, @nesting)
50
- return unless entries
48
+ item = entries&.first
49
+ return unless item
51
50
 
52
- generate_column_content(T.must(entries.first).name)
51
+ generate_column_content(item.name)
52
+ end
53
+
54
+ #: (Prism::SymbolNode node) -> void
55
+ def on_symbol_node_enter(node)
56
+ handle_possible_dsl(node)
53
57
  end
54
58
 
55
59
  private
56
60
 
57
- sig { params(name: String).void }
61
+ #: (String name) -> void
58
62
  def generate_column_content(name)
59
63
  model = @client.model(name)
60
64
  return if model.nil?
@@ -66,20 +70,41 @@ module RubyLsp
66
70
  category: :documentation,
67
71
  ) if schema_file
68
72
 
69
- @response_builder.push(
70
- model[:columns].map do |name, type, default_value, nullable|
71
- primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
72
- suffixes = []
73
- suffixes << "default: #{format_default(default_value, type)}" if default_value
74
- suffixes << "not null" unless nullable || primary_key_suffix
75
- suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
76
- "**#{name}**: #{type}#{primary_key_suffix}#{suffix_string}\n"
77
- end.join("\n"),
78
- category: :documentation,
79
- )
73
+ if model[:columns].any?
74
+ @response_builder.push(
75
+ "### Columns",
76
+ category: :documentation,
77
+ )
78
+ @response_builder.push(
79
+ model[:columns].map do |name, type, default_value, nullable|
80
+ primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
81
+ foreign_key_suffix = " (FK)" if model[:foreign_keys].include?(name)
82
+ suffixes = []
83
+ suffixes << "default: #{format_default(default_value, type)}" if default_value
84
+ suffixes << "not null" unless nullable || primary_key_suffix
85
+ suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
86
+ "- **#{name}**: #{type}#{primary_key_suffix}#{foreign_key_suffix}#{suffix_string}\n"
87
+ end.join("\n"),
88
+ category: :documentation,
89
+ )
90
+ end
91
+
92
+ if model[:indexes].any?
93
+ @response_builder.push(
94
+ "### Indexes",
95
+ category: :documentation,
96
+ )
97
+ @response_builder.push(
98
+ model[:indexes].map do |index|
99
+ uniqueness = index[:unique] ? " (unique)" : ""
100
+ "- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
101
+ end.join("\n"),
102
+ category: :documentation,
103
+ )
104
+ end
80
105
  end
81
106
 
82
- sig { params(default_value: String, type: String).returns(String) }
107
+ #: (String default_value, String type) -> String
83
108
  def format_default(default_value, type)
84
109
  case type
85
110
  when "boolean"
@@ -90,6 +115,55 @@ module RubyLsp
90
115
  default_value
91
116
  end
92
117
  end
118
+
119
+ #: (Prism::SymbolNode node) -> void
120
+ def handle_possible_dsl(node)
121
+ node = @node_context.call_node
122
+ return unless node
123
+ return unless self_receiver?(node)
124
+
125
+ message = node.message
126
+
127
+ return unless message
128
+
129
+ if Support::Associations::ALL.include?(message)
130
+ handle_association(node)
131
+ end
132
+ end
133
+
134
+ #: (Prism::CallNode node) -> void
135
+ def handle_association(node)
136
+ first_argument = node.arguments&.arguments&.first
137
+ return unless first_argument.is_a?(Prism::SymbolNode)
138
+
139
+ association_name = first_argument.unescaped
140
+
141
+ result = @client.association_target(
142
+ model_name: @nesting.join("::"),
143
+ association_name: association_name,
144
+ )
145
+
146
+ return unless result
147
+
148
+ generate_hover(result[:name])
149
+ end
150
+
151
+ # Copied from `RubyLsp::Listeners::Hover#generate_hover`
152
+ #: (String name) -> void
153
+ def generate_hover(name)
154
+ entries = @index.resolve(name, @node_context.nesting)
155
+ return unless entries
156
+
157
+ # We should only show hover for private constants if the constant is defined in the same namespace as the
158
+ # reference
159
+ first_entry = entries.first #: as !nil
160
+ full_name = first_entry.name
161
+ return if first_entry.private? && full_name != "#{@node_context.fully_qualified_name}::#{name}"
162
+
163
+ categorized_markdown_from_index_entries(full_name, entries).each do |category, content|
164
+ @response_builder.push(content, category: category)
165
+ end
166
+ end
93
167
  end
94
168
  end
95
169
  end
@@ -4,13 +4,8 @@
4
4
  module RubyLsp
5
5
  module Rails
6
6
  class IndexingEnhancement < RubyIndexer::Enhancement
7
- extend T::Sig
8
-
9
- sig do
10
- override.params(
11
- call_node: Prism::CallNode,
12
- ).void
13
- end
7
+ # @override
8
+ #: (Prism::CallNode call_node) -> void
14
9
  def on_call_node_enter(call_node)
15
10
  owner = @listener.current_owner
16
11
  return unless owner
@@ -26,11 +21,8 @@ module RubyLsp
26
21
  end
27
22
  end
28
23
 
29
- sig do
30
- override.params(
31
- call_node: Prism::CallNode,
32
- ).void
33
- end
24
+ # @override
25
+ #: (Prism::CallNode call_node) -> void
34
26
  def on_call_node_leave(call_node)
35
27
  if call_node.name == :class_methods && call_node.block
36
28
  @listener.pop_namespace_stack
@@ -39,12 +31,7 @@ module RubyLsp
39
31
 
40
32
  private
41
33
 
42
- sig do
43
- params(
44
- owner: RubyIndexer::Entry::Namespace,
45
- call_node: Prism::CallNode,
46
- ).void
47
- end
34
+ #: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
48
35
  def handle_association(owner, call_node)
49
36
  arguments = call_node.arguments&.arguments
50
37
  return unless arguments
@@ -73,7 +60,7 @@ module RubyLsp
73
60
  @listener.add_method("#{name}=", loc, writer_signatures)
74
61
  end
75
62
 
76
- sig { params(owner: RubyIndexer::Entry::Namespace, call_node: Prism::CallNode).void }
63
+ #: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
77
64
  def handle_concern_extend(owner, call_node)
78
65
  arguments = call_node.arguments&.arguments
79
66
  return unless arguments
@@ -98,7 +85,7 @@ module RubyLsp
98
85
  end
99
86
  end
100
87
 
101
- sig { params(owner: RubyIndexer::Entry::Namespace, call_node: Prism::CallNode).void }
88
+ #: (RubyIndexer::Entry::Namespace owner, Prism::CallNode call_node) -> void
102
89
  def handle_class_methods(owner, call_node)
103
90
  return unless call_node.block
104
91
 
@@ -0,0 +1,171 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ class RailsTestStyle < Listeners::TestDiscovery
7
+ BASE_COMMAND = "bundle exec ruby -r#{Listeners::TestStyle::MINITEST_REPORTER_PATH} bin/rails test" #: String
8
+
9
+ class << self
10
+ #: (Array[Hash[Symbol, untyped]]) -> Array[String]
11
+ def resolve_test_commands(items)
12
+ commands = []
13
+ queue = items.dup
14
+
15
+ full_files = []
16
+
17
+ until queue.empty?
18
+ item = queue.shift #: as !nil
19
+ tags = Set.new(item[:tags])
20
+ next unless tags.include?("framework:rails")
21
+
22
+ children = item[:children]
23
+ uri = URI(item[:uri])
24
+ path = uri.full_path
25
+ next unless path
26
+
27
+ if tags.include?("test_dir")
28
+ if children.empty?
29
+ full_files.concat(Dir.glob(
30
+ "#{path}/**/{*_test,test_*}.rb",
31
+ File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
32
+ ))
33
+ end
34
+ elsif tags.include?("test_file")
35
+ full_files << path if children.empty?
36
+ elsif tags.include?("test_group")
37
+ commands << "#{BASE_COMMAND} #{path} --name \"/#{Shellwords.escape(item[:id])}(#|::)/\""
38
+ else
39
+ full_files << "#{path}:#{item.dig(:range, :start, :line) + 1}"
40
+ end
41
+
42
+ queue.concat(children)
43
+ end
44
+
45
+ unless full_files.empty?
46
+ commands << "#{BASE_COMMAND} #{full_files.join(" ")}"
47
+ end
48
+
49
+ commands
50
+ end
51
+ end
52
+
53
+ #: (ResponseBuilders::TestCollection response_builder, GlobalState global_state, Prism::Dispatcher dispatcher, URI::Generic uri) -> void
54
+ def initialize(response_builder, global_state, dispatcher, uri)
55
+ @parent_stack = [response_builder] #: Array[(Requests::Support::TestItem | ResponseBuilders::TestCollection)?]
56
+ super(response_builder, global_state, uri)
57
+
58
+ register_events(
59
+ dispatcher,
60
+ :on_class_node_enter,
61
+ :on_call_node_enter,
62
+ :on_def_node_enter,
63
+ )
64
+ end
65
+
66
+ #: (Prism::ClassNode node) -> void
67
+ def on_class_node_enter(node)
68
+ with_test_ancestor_tracking(node) do |name, ancestors|
69
+ if declarative_minitest?(ancestors, name)
70
+ test_item = Requests::Support::TestItem.new(
71
+ name,
72
+ name,
73
+ @uri,
74
+ range_from_node(node),
75
+ framework: :rails,
76
+ )
77
+
78
+ last_test_group.add(test_item)
79
+ @response_builder.add_code_lens(test_item)
80
+ @parent_stack << test_item
81
+ else
82
+ @parent_stack << nil
83
+ end
84
+ end
85
+ end
86
+
87
+ #: (Prism::ClassNode node) -> void
88
+ def on_class_node_leave(node)
89
+ @parent_stack.pop
90
+ super
91
+ end
92
+
93
+ #: (Prism::ModuleNode node) -> void
94
+ def on_module_node_enter(node)
95
+ @parent_stack << nil
96
+ super
97
+ end
98
+
99
+ #: (Prism::ModuleNode node) -> void
100
+ def on_module_node_leave(node)
101
+ @parent_stack.pop
102
+ super
103
+ end
104
+
105
+ #: (Prism::CallNode node) -> void
106
+ def on_call_node_enter(node)
107
+ return unless node.name == :test
108
+ return unless node.block
109
+
110
+ arguments = node.arguments&.arguments
111
+ first_arg = arguments&.first
112
+ return unless first_arg.is_a?(Prism::StringNode)
113
+
114
+ test_name = first_arg.unescaped
115
+ test_name = "<empty test name>" if test_name.empty?
116
+
117
+ # Rails' `test "foo bar"` helper defines a method `def test_foo_bar`. We normalize test names
118
+ # the same way (spaces to underscores, prefix with `test_`) to match the actual method names
119
+ # Rails uses at runtime, ensuring proper test discovery and execution.
120
+ rails_normalized_name = "test_#{test_name.gsub(/\s+/, "_")}"
121
+
122
+ add_test_item(node, rails_normalized_name)
123
+ end
124
+
125
+ #: (Prism::DefNode node) -> void
126
+ def on_def_node_enter(node)
127
+ return if @visibility_stack.last != :public
128
+
129
+ name = node.name.to_s
130
+ return unless name.start_with?("test_")
131
+
132
+ add_test_item(node, name)
133
+ end
134
+
135
+ private
136
+
137
+ #: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
138
+ def declarative_minitest?(attached_ancestors, fully_qualified_name)
139
+ # The declarative test style is present as long as the class extends
140
+ # ActiveSupport::Testing::Declarative
141
+ name_parts = fully_qualified_name.split("::")
142
+ singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>"
143
+ @index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
144
+ rescue RubyIndexer::Index::NonExistingNamespaceError
145
+ false
146
+ end
147
+
148
+ #: (Prism::Node node, String test_name) -> void
149
+ def add_test_item(node, test_name)
150
+ parent = @parent_stack.last
151
+ return unless parent.is_a?(Requests::Support::TestItem)
152
+
153
+ example_item = Requests::Support::TestItem.new(
154
+ "#{parent.id}##{test_name}",
155
+ test_name,
156
+ @uri,
157
+ range_from_node(node),
158
+ framework: :rails,
159
+ )
160
+ parent.add(example_item)
161
+ @response_builder.add_code_lens(example_item)
162
+ end
163
+
164
+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)
165
+ def last_test_group
166
+ index = @parent_stack.rindex { |i| i } #: as !nil
167
+ @parent_stack[index] #: as Requests::Support::TestItem | ResponseBuilders::TestCollection
168
+ end
169
+ end
170
+ end
171
+ end