ruby-lsp-rails 0.4.0 → 0.4.2

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,37 @@ 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)
24
+ @nesting = node_context.nesting #: Array[String]
25
+ @index = global_state.index #: RubyIndexer::Index
35
26
  dispatcher.register(self, :on_constant_path_node_enter, :on_constant_read_node_enter)
36
27
  end
37
28
 
38
- sig { params(node: Prism::ConstantPathNode).void }
29
+ #: (Prism::ConstantPathNode node) -> void
39
30
  def on_constant_path_node_enter(node)
40
31
  entries = @index.resolve(node.slice, @nesting)
41
32
  return unless entries
42
33
 
43
- name = T.must(entries.first).name
34
+ name = entries.first.name
44
35
  generate_column_content(name)
45
36
  end
46
37
 
47
- sig { params(node: Prism::ConstantReadNode).void }
38
+ #: (Prism::ConstantReadNode node) -> void
48
39
  def on_constant_read_node_enter(node)
49
40
  entries = @index.resolve(node.name.to_s, @nesting)
50
41
  return unless entries
51
42
 
52
- generate_column_content(T.must(entries.first).name)
43
+ generate_column_content(entries.first.name)
53
44
  end
54
45
 
55
46
  private
56
47
 
57
- sig { params(name: String).void }
48
+ #: (String name) -> void
58
49
  def generate_column_content(name)
59
50
  model = @client.model(name)
60
51
  return if model.nil?
@@ -66,20 +57,41 @@ module RubyLsp
66
57
  category: :documentation,
67
58
  ) if schema_file
68
59
 
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
- )
60
+ if model[:columns].any?
61
+ @response_builder.push(
62
+ "### Columns",
63
+ category: :documentation,
64
+ )
65
+ @response_builder.push(
66
+ model[:columns].map do |name, type, default_value, nullable|
67
+ primary_key_suffix = " (PK)" if model[:primary_keys].include?(name)
68
+ foreign_key_suffix = " (FK)" if model[:foreign_keys].include?(name)
69
+ suffixes = []
70
+ suffixes << "default: #{format_default(default_value, type)}" if default_value
71
+ suffixes << "not null" unless nullable || primary_key_suffix
72
+ suffix_string = " - #{suffixes.join(" - ")}" if suffixes.any?
73
+ "- **#{name}**: #{type}#{primary_key_suffix}#{foreign_key_suffix}#{suffix_string}\n"
74
+ end.join("\n"),
75
+ category: :documentation,
76
+ )
77
+ end
78
+
79
+ if model[:indexes].any?
80
+ @response_builder.push(
81
+ "### Indexes",
82
+ category: :documentation,
83
+ )
84
+ @response_builder.push(
85
+ model[:indexes].map do |index|
86
+ uniqueness = index[:unique] ? " (unique)" : ""
87
+ "- **#{index[:name]}** (#{index[:columns].join(",")})#{uniqueness}"
88
+ end.join("\n"),
89
+ category: :documentation,
90
+ )
91
+ end
80
92
  end
81
93
 
82
- sig { params(default_value: String, type: String).returns(String) }
94
+ #: (String default_value, String type) -> String
83
95
  def format_default(default_value, type)
84
96
  case type
85
97
  when "boolean"
@@ -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,150 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Rails
6
+ class RailsTestStyle < Listeners::TestDiscovery
7
+ BASE_COMMAND = "#{RbConfig.ruby} 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
+ #: (RunnerClient client, ResponseBuilders::TestCollection response_builder, GlobalState global_state, Prism::Dispatcher dispatcher, URI::Generic uri) -> void
54
+ def initialize(client, response_builder, global_state, dispatcher, uri)
55
+ super(response_builder, global_state, dispatcher, uri)
56
+
57
+ dispatcher.register(
58
+ self,
59
+ :on_class_node_enter,
60
+ :on_call_node_enter,
61
+ :on_def_node_enter,
62
+ )
63
+ end
64
+
65
+ #: (Prism::ClassNode node) -> void
66
+ def on_class_node_enter(node)
67
+ with_test_ancestor_tracking(node) do |name, ancestors|
68
+ if declarative_minitest?(ancestors, name)
69
+ test_item = Requests::Support::TestItem.new(
70
+ name,
71
+ name,
72
+ @uri,
73
+ range_from_node(node),
74
+ framework: :rails,
75
+ )
76
+
77
+ @response_builder.add(test_item)
78
+ end
79
+ end
80
+ end
81
+
82
+ #: (Prism::CallNode node) -> void
83
+ def on_call_node_enter(node)
84
+ return unless node.name == :test
85
+ return unless node.block
86
+
87
+ arguments = node.arguments&.arguments
88
+ first_arg = arguments&.first
89
+ return unless first_arg.is_a?(Prism::StringNode)
90
+
91
+ test_name = first_arg.unescaped
92
+ test_name = "<empty test name>" if test_name.empty?
93
+
94
+ # Rails' `test "foo bar"` helper defines a method `def test_foo_bar`. We normalize test names
95
+ # the same way (spaces to underscores, prefix with `test_`) to match the actual method names
96
+ # Rails uses at runtime, ensuring proper test discovery and execution.
97
+ rails_normalized_name = "test_#{test_name.gsub(/\s+/, "_")}"
98
+
99
+ add_test_item(node, rails_normalized_name)
100
+ end
101
+
102
+ #: (Prism::DefNode node) -> void
103
+ def on_def_node_enter(node)
104
+ return if @visibility_stack.last != :public
105
+
106
+ name = node.name.to_s
107
+ return unless name.start_with?("test_")
108
+
109
+ add_test_item(node, name)
110
+ end
111
+
112
+ private
113
+
114
+ #: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
115
+ def declarative_minitest?(attached_ancestors, fully_qualified_name)
116
+ # The declarative test style is present as long as the class extends
117
+ # ActiveSupport::Testing::Declarative
118
+ name_parts = fully_qualified_name.split("::")
119
+ singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>"
120
+ @index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
121
+ rescue RubyIndexer::Index::NonExistingNamespaceError
122
+ false
123
+ end
124
+
125
+ #: (Prism::Node node, String test_name) -> void
126
+ def add_test_item(node, test_name)
127
+ test_item = group_test_item
128
+ return unless test_item
129
+
130
+ test_item.add(Requests::Support::TestItem.new(
131
+ "#{test_item.id}##{test_name}",
132
+ test_name,
133
+ @uri,
134
+ range_from_node(node),
135
+ framework: :rails,
136
+ ))
137
+ end
138
+
139
+ #: -> Requests::Support::TestItem?
140
+ def group_test_item
141
+ current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::")
142
+
143
+ # If we're finding a test method, but for the wrong framework, then the group test item will not have been
144
+ # previously pushed and thus we return early and avoid adding items for a framework this listener is not
145
+ # interested in
146
+ @response_builder[current_group_name]
147
+ end
148
+ end
149
+ end
150
+ end