steep 1.4.0.dev.2 → 1.4.0.dev.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -2
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +7 -9
  5. data/Gemfile.steep +1 -2
  6. data/Gemfile.steep.lock +9 -10
  7. data/README.md +7 -1
  8. data/Steepfile +0 -3
  9. data/bin/rbs +0 -1
  10. data/guides/README.md +5 -0
  11. data/guides/src/gem-rbs-collection/gem-rbs-collection.md +143 -0
  12. data/guides/src/getting-started/getting-started.md +164 -0
  13. data/guides/src/nil-optional/nil-optional.md +195 -0
  14. data/lib/steep/diagnostic/ruby.rb +79 -4
  15. data/lib/steep/drivers/check.rb +4 -4
  16. data/lib/steep/interface/block.rb +10 -0
  17. data/lib/steep/module_helper.rb +13 -11
  18. data/lib/steep/path_helper.rb +4 -0
  19. data/lib/steep/server/interaction_worker.rb +105 -92
  20. data/lib/steep/services/type_name_completion.rb +157 -0
  21. data/lib/steep/source.rb +1 -0
  22. data/lib/steep/type_construction.rb +402 -229
  23. data/lib/steep/type_inference/block_params.rb +13 -0
  24. data/lib/steep/type_inference/context.rb +3 -3
  25. data/lib/steep/type_inference/method_params.rb +42 -16
  26. data/lib/steep/type_inference/send_args.rb +79 -50
  27. data/lib/steep/type_inference/type_env.rb +7 -1
  28. data/lib/steep/version.rb +1 -1
  29. data/lib/steep.rb +1 -0
  30. data/rbs_collection.steep.lock.yaml +0 -28
  31. data/rbs_collection.steep.yaml +10 -9
  32. data/sample/lib/conference.rb +12 -0
  33. data/sample/sig/conference.rbs +5 -0
  34. data/sig/shims/language-server_protocol.rbs +12 -0
  35. data/sig/shims/parser/nodes.rbs +37 -0
  36. data/sig/shims/parser.rbs +1 -0
  37. data/sig/shims/string.rbs +4 -0
  38. data/sig/steep/ast/types/factory.rbs +10 -8
  39. data/sig/steep/diagnostic/lsp_formatter.rbs +1 -1
  40. data/sig/steep/diagnostic/ruby.rbs +38 -2
  41. data/sig/steep/drivers/check.rbs +1 -1
  42. data/sig/steep/drivers/checkfile.rbs +1 -1
  43. data/sig/steep/drivers/diagnostic_printer.rbs +1 -1
  44. data/sig/steep/drivers/watch.rbs +1 -1
  45. data/sig/steep/index/signature_symbol_provider.rbs +1 -1
  46. data/sig/steep/interface/block.rbs +2 -0
  47. data/sig/steep/interface/builder.rbs +5 -3
  48. data/sig/steep/interface/method_type.rbs +5 -3
  49. data/sig/steep/module_helper.rbs +9 -0
  50. data/sig/steep/path_helper.rbs +3 -1
  51. data/sig/steep/server/base_worker.rbs +1 -1
  52. data/sig/steep/server/interaction_worker.rbs +46 -17
  53. data/sig/steep/server/master.rbs +1 -1
  54. data/sig/steep/server/type_check_worker.rbs +7 -5
  55. data/sig/steep/server/worker_process.rbs +6 -4
  56. data/sig/steep/services/completion_provider.rbs +2 -0
  57. data/sig/steep/services/type_name_completion.rbs +122 -0
  58. data/sig/steep/type_construction.rbs +99 -30
  59. data/sig/steep/type_inference/block_params.rbs +4 -0
  60. data/sig/steep/type_inference/context.rbs +70 -22
  61. data/sig/steep/type_inference/method_params.rbs +43 -24
  62. data/sig/steep/type_inference/multiple_assignment.rbs +1 -1
  63. data/sig/steep/type_inference/send_args.rbs +13 -3
  64. data/sig/steep/typing.rbs +7 -2
  65. data/smoke/diagnostics/test_expectations.yml +1 -0
  66. data/steep.gemspec +0 -1
  67. metadata +10 -16
@@ -0,0 +1,195 @@
1
+ # nil and Optional Types
2
+
3
+ `nil`s have been the most common source of the errors you see when testing and after the deployment of your apps.
4
+
5
+ ```
6
+ NoMethodError (undefined method `save!' for nil:NilClass)
7
+ ```
8
+
9
+ Steep/RBS provides *optional types* to help you identify the problems while you are coding.
10
+
11
+ ## Preventing `nil` problems
12
+
13
+ Technically, there is only one way to prevent `nil` problems – test everytime if the value is `nil` before calling methods with the receiver.
14
+
15
+ ```rb
16
+ if account
17
+ account.save!
18
+ end
19
+ ```
20
+
21
+ Using the `if` statement is the most popular way to ensure the value is not `nil`. But you can do it with safe-navigation-operators, case-when (or case-in), and `#try` method.
22
+
23
+ ```rb
24
+ account&.save!
25
+
26
+ case account
27
+ when Account
28
+ account.save!
29
+ end
30
+
31
+ account.try {|account| account.save! }
32
+ ```
33
+
34
+ It's simple, but not easy to do in your code.
35
+
36
+ **You may forget testing.** This happens easily. You don't notice that you forget until you deploy the code into production and it crashes after loading an account that is old and satisfies complicated conditions that leads it to be `nil`.
37
+
38
+ **You may add redundant guards.** This won't get your app crash, but it will make understanding your code more difficult. It adds unnecessary noise that tells your teammates this can be `nil` in some place, and results in another redundant guard.
39
+
40
+ The `nil` problems can be solved by a tool that tells you:
41
+
42
+ * If the value can be `nil` or not, and
43
+ * You forget testing the value before using the value
44
+
45
+ RBS has a language construct to do this called optional types and Steep implements analysis to let you know if you forget testing the value.
46
+
47
+ ## Optional types
48
+
49
+ Optional types in RBS are denoted with a suffix `?` – Type?. It means the value of the type may be `nil`.
50
+
51
+ ```
52
+ Integer? # Integer or nil
53
+ Array[Account]? # An array of Account or nil
54
+ ```
55
+
56
+ Note that optional types can be included in other types as:
57
+
58
+ ```
59
+ Array[Account?]
60
+ ```
61
+
62
+ The value of the type above is always an array, but the element may be `nil`.
63
+
64
+ In other words, a non optional type in RBS means the value cannot be `nil`.
65
+
66
+ ```
67
+ Integer # Integer, cannot be nil
68
+ Array[Account] # An array, cannot be nil
69
+ ```
70
+
71
+ Let's see how Steep reports errors on optional and non-optional types.
72
+
73
+ ```rb
74
+ account = Account.find(1)
75
+ account.save!
76
+ ```
77
+
78
+ Assume the type of `account` is `Account` (non optional type), the code type checks successfully. There is no chance to be `nil` here. The `save!` method call never results in a `NoMethodError`.
79
+
80
+ ```rb
81
+ account = Account.find_by(email: "soutaro@squareup.com")
82
+ account.save!
83
+ ```
84
+
85
+ Steep reports a `NoMethod` error on the `save!` call. Because the value of the `account` may be `nil`, depending on the actual records in the `accounts` table. You cannot call the `save!` method without checking if the `account` is `nil`.
86
+
87
+ You cannot assign `nil` to a local variable with non-optional types.
88
+
89
+ ```rb
90
+ # @type var account: Account
91
+
92
+ account = nil
93
+ account = Account.find_by(email: "soutaro@squareup.com")
94
+ ```
95
+
96
+ Because the type of `account` is declared Account, non-optional type, it cannot be `nil`. And Steep detects a type error if you try to assign `nil`. Same for assigning an optional type value at the last line.
97
+
98
+ # Unwrapping optional types
99
+
100
+ There are several ways to unwrap optional types. The most common one is using if.
101
+
102
+ ```rb
103
+ account = Account.find_by(id: 1)
104
+ if account
105
+ account.save!
106
+ end
107
+ ```
108
+
109
+ The *if* statement tests if `account` is `nil`. Inside the then clause, `account` cannot be `nil`. Then Steep type checks the code.
110
+
111
+ This works for *else* clause of *unless*.
112
+
113
+ ```rb
114
+ account = Account.find_by(id: 1)
115
+ unless account
116
+ # Do something
117
+ else
118
+ account.save!
119
+ end
120
+ ```
121
+
122
+ This also type checks successfully.
123
+
124
+ Steep supports `nil?` predicate too.
125
+
126
+ ```rb
127
+ unless (account = Account.find_by(id: 1)).nil?
128
+ account.save!
129
+ end
130
+ ```
131
+
132
+ This assumes the `Account` class doesn't have a custom `nil?` method, but keeps the built-in `nil?` or equivalent.
133
+
134
+ The last one is using safe-nevigation-navigator. It checks if the receiver is `nil` and calls the method if it is not. Otherwise just evaluates to `nil`.
135
+
136
+ ```rb
137
+ account = Account.find_by(id: 1)
138
+ account&.save!
139
+ ```
140
+
141
+ This is a shorthand for the case you don't do any error handling case if it is `nil`.
142
+
143
+ ## What should I do for the case of `nil`?
144
+
145
+ There is no universal answer for this question. You may just stop the execution of the method by returning. You may want to insert a new account to ensure the record exists. Raising an exception with a detailed error message will help troubleshooting.
146
+
147
+ It depends on what the program is expected to do. Steep just checks if accessing `nil` may happen or not. The developers only know how to handle the `nil` cases.
148
+
149
+ # Handling unwanted `nil`s
150
+
151
+ When you start using Steep, you may see many unwanted `nil`s. This typically happens when you want to use Array methods, like `first` or `sample`.
152
+
153
+ ```rb
154
+ account = accounts.first
155
+ account.save!
156
+ ```
157
+
158
+ It returns `nil` if the array is empty. Steep cannot detect if the array is empty or not, and it conservatively assumes the return value of the methods may be `nil`. While you know the `account` array is not empty, Steep infer the `first` method may return `nil`.
159
+
160
+ This is one of the most frequently seen sources of unwanted `nil`s.
161
+
162
+ ## Raising an error
163
+
164
+ In this case, you have to add an extra code to let Steep unwrap it.
165
+
166
+ ```rb
167
+ account = accounts.first or raise
168
+ account.save!
169
+ ```
170
+
171
+ My recommendation is to raise an exception, `|| raise` or `or raise`. It raises an exception in the case of `nil`, and Steep unwraps the type of the `account` variable.
172
+
173
+ Exceptions are better than other control flow operators – `return`/`break`/`next`. It doesn't affect the control flow until it actually happens during execution, and the type checking result other than the unwrapping is changed.
174
+
175
+ An `#raise` call without argument is my favorite. It's short. It's uncommon in the Ruby code and it can tell the readers that something unexpected is happening.
176
+
177
+ But of course, you can add some message:
178
+
179
+ ```rb
180
+ account = accounts.first or raise("accounts cannot be empty")
181
+ account.save!
182
+ ```
183
+
184
+ ## Type assertions
185
+
186
+ You can also use a type assertion, that is introduced in Steep 1.3.
187
+
188
+ ```rb
189
+ account = accounts.first #: Account
190
+ account.save!
191
+ ```
192
+
193
+ It tells Steep that the right hand side of the assignment is `Account`. That overwrites the type checking algorithm, and the developer is responsible for making sure the value cannot be `nil`.
194
+
195
+ Note: Nothing happens during the execution. It just works for Steep and Ruby doesn't do any extra type checking on it. I recommend using the `or raise` idiom for most of the cases.
@@ -37,22 +37,72 @@ module Steep
37
37
  when relation.interface?
38
38
  nil
39
39
  when relation.block?
40
- nil
40
+ "(Blocks are incompatible)"
41
41
  when relation.function?
42
42
  nil
43
43
  when relation.params?
44
- nil
44
+ "(Params are incompatible)"
45
45
  end
46
46
  end
47
47
 
48
48
  def detail_lines
49
- StringIO.new.tap do |io|
50
- result.failure_path&.reverse_each.map do |result|
49
+ lines = StringIO.new.tap do |io|
50
+ failure_path = result.failure_path || []
51
+ failure_path.reverse_each.map do |result|
51
52
  relation_message(result.relation)
52
53
  end.compact.each.with_index(1) do |message, index|
53
54
  io.puts "#{" " * (index)}#{message}"
54
55
  end
55
56
  end.string.chomp
57
+
58
+ unless lines.empty?
59
+ lines
60
+ end
61
+ end
62
+ end
63
+
64
+ module ResultPrinter2
65
+ def result_line(result)
66
+ case result
67
+ when Subtyping::Result::Failure
68
+ case result.error
69
+ when Subtyping::Result::Failure::UnknownPairError
70
+ nil
71
+ when Subtyping::Result::Failure::UnsatisfiedConstraints
72
+ "Unsatisfied constraints: #{result.relation}"
73
+ when Subtyping::Result::Failure::MethodMissingError
74
+ "Method `#{result.error.name}` is missing"
75
+ when Subtyping::Result::Failure::BlockMismatchError
76
+ "Incomaptible block: #{result.relation}"
77
+ when Subtyping::Result::Failure::ParameterMismatchError
78
+ if result.relation.params?
79
+ "Incompatible arity: #{result.relation.super_type} and #{result.relation.sub_type}"
80
+ else
81
+ "Incompatible arity: #{result.relation}"
82
+ end
83
+ when Subtyping::Result::Failure::PolyMethodSubtyping
84
+ "Unsupported polymorphic method comparison: #{result.relation}"
85
+ when Subtyping::Result::Failure::SelfBindingMismatch
86
+ "Incompatible block self type: #{result.relation}"
87
+ end
88
+ else
89
+ result.relation.to_s
90
+ end
91
+ end
92
+
93
+ def detail_lines
94
+ lines = StringIO.new.tap do |io|
95
+ failure_path = result.failure_path || []
96
+ failure_path.reverse_each.filter_map do |result|
97
+ result_line(result)
98
+ end.each.with_index(1) do |message, index|
99
+ io.puts "#{" " * (index)}#{message}"
100
+ end
101
+ end.string.chomp
102
+
103
+ unless lines.empty?
104
+ lines
105
+ end
56
106
  end
57
107
  end
58
108
 
@@ -832,6 +882,31 @@ module Steep
832
882
  end
833
883
  end
834
884
 
885
+ class IncompatibleArgumentForwarding < Base
886
+ attr_reader :method_name, :params_pair, :block_pair, :result
887
+
888
+ def initialize(method_name:, node:, params_pair: nil, block_pair: nil, result:)
889
+ super(node: node)
890
+ @method_name = method_name
891
+ @result = result
892
+ @params_pair = params_pair
893
+ @block_pair = block_pair
894
+ end
895
+
896
+ include ResultPrinter2
897
+
898
+ def header_line
899
+ case
900
+ when params_pair
901
+ "Cannot forward arguments to `#{method_name}`:"
902
+ when block_pair
903
+ "Cannot forward block to `#{method_name}`:"
904
+ else
905
+ raise
906
+ end
907
+ end
908
+ end
909
+
835
910
  ALL = ObjectSpace.each_object(Class).with_object([]) do |klass, array|
836
911
  if klass < Base
837
912
  array << klass
@@ -30,11 +30,11 @@ module Steep
30
30
  client_read, server_write = IO.pipe
31
31
  server_read, client_write = IO.pipe
32
32
 
33
- client_reader = LanguageServer::Protocol::Transport::Io::Reader.new(client_read)
34
- client_writer = LanguageServer::Protocol::Transport::Io::Writer.new(client_write)
33
+ client_reader = LSP::Transport::Io::Reader.new(client_read)
34
+ client_writer = LSP::Transport::Io::Writer.new(client_write)
35
35
 
36
- server_reader = LanguageServer::Protocol::Transport::Io::Reader.new(server_read)
37
- server_writer = LanguageServer::Protocol::Transport::Io::Writer.new(server_write)
36
+ server_reader = LSP::Transport::Io::Reader.new(server_read)
37
+ server_writer = LSP::Transport::Io::Writer.new(server_write)
38
38
 
39
39
  typecheck_workers = Server::WorkerProcess.start_typecheck_workers(
40
40
  steepfile: project.steepfile_path,
@@ -69,6 +69,16 @@ module Steep
69
69
  )
70
70
  end
71
71
 
72
+ def to_proc_type
73
+ proc = AST::Types::Proc.new(type: type, self_type: self_type, block: nil)
74
+
75
+ if optional?
76
+ AST::Types::Union.build(types: [proc, AST::Builtin.nil_type])
77
+ else
78
+ proc
79
+ end
80
+ end
81
+
72
82
  def +(other)
73
83
  optional = self.optional? || other.optional?
74
84
  type = Function.new(
@@ -1,21 +1,23 @@
1
1
  module Steep
2
2
  module ModuleHelper
3
3
  def module_name_from_node(parent_node, constant_name)
4
- namespace = namespace_from_node(parent_node) or return
5
- name = constant_name
6
- RBS::TypeName.new(name: name, namespace: namespace)
4
+ if namespace = namespace_from_node(parent_node)
5
+ RBS::TypeName.new(name: constant_name, namespace: namespace)
6
+ end
7
7
  end
8
8
 
9
9
  def namespace_from_node(node)
10
- case node&.type
11
- when nil
12
- RBS::Namespace.empty
13
- when :cbase
14
- RBS::Namespace.root
15
- when :const
16
- namespace_from_node(node.children[0])&.yield_self do |parent|
17
- parent.append(node.children[1])
10
+ if node
11
+ case node.type
12
+ when :cbase
13
+ RBS::Namespace.root
14
+ when :const
15
+ if parent = namespace_from_node(node.children[0])
16
+ parent.append(node.children[1])
17
+ end
18
18
  end
19
+ else
20
+ RBS::Namespace.empty
19
21
  end
20
22
  end
21
23
  end
@@ -11,6 +11,10 @@ module Steep
11
11
  end
12
12
  end
13
13
 
14
+ def to_pathname!(uri, dosish: Gem.win_platform?)
15
+ to_pathname(uri, dosish: dosish) or raise "Cannot translate a URI to pathname: #{uri}"
16
+ end
17
+
14
18
  def to_uri(path, dosish: Gem.win_platform?)
15
19
  str_path = path.to_s
16
20
  if dosish
@@ -3,9 +3,9 @@ module Steep
3
3
  class InteractionWorker < BaseWorker
4
4
  include ChangeBuffer
5
5
 
6
- ApplyChangeJob = Class.new()
7
- HoverJob = Struct.new(:id, :path, :line, :column, keyword_init: true)
8
- CompletionJob = Struct.new(:id, :path, :line, :column, :trigger, keyword_init: true)
6
+ ApplyChangeJob = _ = Class.new()
7
+ HoverJob = _ = Struct.new(:id, :path, :line, :column, keyword_init: true)
8
+ CompletionJob = _ = Struct.new(:id, :path, :line, :column, :trigger, keyword_init: true)
9
9
 
10
10
  LSP = LanguageServer::Protocol
11
11
 
@@ -53,7 +53,7 @@ module Steep
53
53
  when "textDocument/hover"
54
54
  id = request[:id]
55
55
 
56
- path = project.relative_path(Steep::PathHelper.to_pathname(request[:params][:textDocument][:uri]))
56
+ path = project.relative_path(Steep::PathHelper.to_pathname!(request[:params][:textDocument][:uri]))
57
57
  line = request[:params][:position][:line]+1
58
58
  column = request[:params][:position][:character]
59
59
 
@@ -63,7 +63,8 @@ module Steep
63
63
  id = request[:id]
64
64
 
65
65
  params = request[:params]
66
- path = project.relative_path(Steep::PathHelper.to_pathname(params[:textDocument][:uri]))
66
+
67
+ path = project.relative_path(Steep::PathHelper.to_pathname!(params[:textDocument][:uri]))
67
68
  line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
68
69
  trigger = params.dig(:context, :triggerCharacter)
69
70
 
@@ -104,6 +105,7 @@ module Steep
104
105
  Steep.logger.tagged("#response_to_completion") do
105
106
  Steep.measure "Generating response" do
106
107
  Steep.logger.info "path: #{job.path}, line: #{job.line}, column: #{job.column}, trigger: #{job.trigger}"
108
+
107
109
  case
108
110
  when target = project.target_for_source_path(job.path)
109
111
  file = service.source_files[job.path] or return
@@ -127,56 +129,78 @@ module Steep
127
129
  items: completion_items
128
130
  )
129
131
  when (targets = project.targets_for_path(job.path)).is_a?(Array)
130
- target = targets[0] or return
131
- sig_service = service.signature_services[target.name] #: Services::SignatureService
132
+ target = targets[0] or raise
133
+ sig_service = service.signature_services[target.name] or raise
132
134
  relative_path = job.path
133
- buffer = RBS::Buffer.new(name: relative_path, content: sig_service.files[relative_path].content)
134
- pos = buffer.loc_to_pos([job.line, job.column])
135
- prefix = buffer.content[0...pos].reverse[/\A[\w\d]*/].reverse
135
+
136
+ context = nil #: RBS::Resolver::context
136
137
 
137
138
  case sig_service.status
138
139
  when Steep::Services::SignatureService::SyntaxErrorStatus, Steep::Services::SignatureService::AncestorErrorStatus
139
- return
140
- end
141
140
 
142
- sig = sig_service.files[relative_path].signature
143
- sig.is_a?(Array) or raise
144
- decls = sig[2]
145
- locator = RBS::Locator.new(buffer: sig[0], dirs: sig[1], decls: decls)
146
-
147
- (_hd, tail = locator.find2(line: job.line, column: job.column)) or return []
148
-
149
- namespace = []
150
- tail.each do |t|
151
- case t
152
- when RBS::AST::Declarations::Module, RBS::AST::Declarations::Class
153
- namespace << t.name.to_namespace
141
+ if buffer = sig_service.latest_env.buffers.find {|buf| Pathname(buf.name) == Pathname(relative_path) }
142
+ dirs = sig_service.latest_env.signatures[buffer][0]
143
+ else
144
+ dirs = [] #: Array[RBS::AST::Directives::t]
145
+ end
146
+ else
147
+ signature = sig_service.files[relative_path].signature
148
+ signature.is_a?(Array) or raise
149
+ buffer, dirs, decls = signature
150
+
151
+ locator = RBS::Locator.new(buffer: buffer, dirs: dirs, decls: decls)
152
+
153
+ _hd, tail = locator.find2(line: job.line, column: job.column)
154
+ tail ||= []
155
+
156
+ tail.reverse_each do |t|
157
+ case t
158
+ when RBS::AST::Declarations::Module, RBS::AST::Declarations::Class
159
+ if (last_type_name = context&.[](1)).is_a?(RBS::TypeName)
160
+ context = [context, last_type_name + t.name]
161
+ else
162
+ context = [context, t.name.absolute!]
163
+ end
164
+ end
154
165
  end
155
166
  end
156
- context = []
157
167
 
158
- namespace.each do |ns|
159
- context.map! { |n| ns + n }
160
- context << ns
168
+ buffer = RBS::Buffer.new(name: relative_path, content: sig_service.files[relative_path].content)
169
+ prefix = Services::TypeNameCompletion::Prefix.parse(buffer, line: job.line, column: job.column)
170
+
171
+ completion = Services::TypeNameCompletion.new(env: sig_service.latest_env, context: context, dirs: dirs)
172
+ type_names = completion.find_type_names(prefix)
173
+ prefix_size = prefix ? prefix.size : 0
174
+
175
+ completion_items = type_names.map {|type_name|
176
+ absolute_name, relative_name = completion.resolve_name_in_context(type_name)
177
+
178
+ format_completion_item_for_rbs(sig_service, absolute_name, job, relative_name.to_s, prefix_size)
179
+ }
180
+
181
+ ["untyped", "void", "bool", "class", "module", "instance", "nil"].each do |name|
182
+ completion_items << LanguageServer::Protocol::Interface::CompletionItem.new(
183
+ label: name,
184
+ detail: "(builtin type)",
185
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
186
+ range: LanguageServer::Protocol::Interface::Range.new(
187
+ start: LanguageServer::Protocol::Interface::Position.new(
188
+ line: job.line - 1,
189
+ character: job.column - prefix_size
190
+ ),
191
+ end: LanguageServer::Protocol::Interface::Position.new(
192
+ line: job.line - 1,
193
+ character: job.column
194
+ )
195
+ ),
196
+ new_text: name
197
+ ),
198
+ kind: LSP::Constant::CompletionItemKind::KEYWORD,
199
+ filter_text: name,
200
+ sort_text: "zz__#{name}"
201
+ )
161
202
  end
162
203
 
163
- context.map!(&:absolute!)
164
-
165
- class_names = sig_service.latest_env.class_decls.keys + sig_service.latest_env.class_alias_decls.keys
166
- class_items = class_names.map { |type_name|
167
- format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
168
- }.compact
169
-
170
- alias_items = sig_service.latest_env.type_alias_decls.keys.map { |type_name|
171
- format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
172
- }.compact
173
-
174
- interface_items = sig_service.latest_env.interface_decls.keys.map {|type_name|
175
- format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
176
- }.compact
177
-
178
- completion_items = class_items + alias_items + interface_items
179
-
180
204
  LSP::Interface::CompletionList.new(
181
205
  is_incomplete: false,
182
206
  items: completion_items
@@ -186,11 +210,11 @@ module Steep
186
210
  end
187
211
  end
188
212
 
189
- def format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
213
+ def format_completion_item_for_rbs(sig_service, type_name, job, complete_text, prefix_size)
190
214
  range = LanguageServer::Protocol::Interface::Range.new(
191
215
  start: LanguageServer::Protocol::Interface::Position.new(
192
216
  line: job.line - 1,
193
- character: job.column - prefix.size
217
+ character: job.column - prefix_size
194
218
  ),
195
219
  end: LanguageServer::Protocol::Interface::Position.new(
196
220
  line: job.line - 1,
@@ -198,64 +222,64 @@ module Steep
198
222
  )
199
223
  )
200
224
 
201
- name = relative_name_in_context(type_name, context).to_s
202
-
203
- return unless name.start_with?(prefix)
204
-
205
225
  case type_name.kind
206
226
  when :class
207
227
  env = sig_service.latest_env #: RBS::Environment
208
228
  class_entry = env.module_class_entry(type_name) or raise
209
229
 
210
- case class_entry
211
- when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
212
- LanguageServer::Protocol::Interface::CompletionItem.new(
213
- label: "#{name}",
214
- documentation: format_comment(class_entry.primary.decl.comment),
215
- text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
216
- range: range,
217
- new_text: name
218
- ),
219
- kind: LSP::Constant::CompletionItemKind::CLASS,
220
- insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
221
- )
222
- when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry
223
- LanguageServer::Protocol::Interface::CompletionItem.new(
224
- label: "#{name}",
225
- documentation: format_comment(class_entry.decl.comment),
226
- text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
227
- range: range,
228
- new_text: name
229
- ),
230
- kind: LSP::Constant::CompletionItemKind::CLASS,
231
- insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
232
- )
233
- end
230
+ comment =
231
+ case class_entry
232
+ when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
233
+ class_entry.decls.flat_map {|decl| [decl.decl.comment] }.first
234
+ when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry
235
+ class_entry.decl.comment
236
+ end
237
+
238
+ LanguageServer::Protocol::Interface::CompletionItem.new(
239
+ label: complete_text,
240
+ detail: type_name.to_s,
241
+ documentation: format_comment(comment),
242
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
243
+ range: range,
244
+ new_text: complete_text
245
+ ),
246
+ kind: LSP::Constant::CompletionItemKind::CLASS,
247
+ insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET,
248
+ sort_text: complete_text,
249
+ filter_text: complete_text
250
+ )
234
251
  when :alias
235
252
  alias_decl = sig_service.latest_env.type_alias_decls[type_name]&.decl or raise
253
+
236
254
  LanguageServer::Protocol::Interface::CompletionItem.new(
237
- label: "#{name}",
255
+ label: complete_text,
256
+ detail: type_name.to_s,
238
257
  text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
239
258
  range: range,
240
- new_text: name
259
+ new_text: complete_text
241
260
  ),
242
261
  documentation: format_comment(alias_decl.comment),
243
262
  # https://github.com/microsoft/vscode-languageserver-node/blob/6d78fc4d25719b231aba64a721a606f58b9e0a5f/client/src/common/client.ts#L624-L650
244
263
  kind: LSP::Constant::CompletionItemKind::FIELD,
245
- insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
264
+ insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET,
265
+ sort_text: complete_text,
266
+ filter_text: complete_text
246
267
  )
247
268
  when :interface
248
269
  interface_decl = sig_service.latest_env.interface_decls[type_name]&.decl or raise
249
270
 
250
271
  LanguageServer::Protocol::Interface::CompletionItem.new(
251
- label: "#{name}",
272
+ label: complete_text,
273
+ detail: type_name.to_s,
252
274
  text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
253
275
  range: range,
254
- new_text: name
276
+ new_text: complete_text
255
277
  ),
256
278
  documentation: format_comment(interface_decl.comment),
257
279
  kind: LanguageServer::Protocol::Constant::CompletionItemKind::INTERFACE,
258
- insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET
280
+ insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET,
281
+ sort_text: complete_text,
282
+ filter_text: complete_text
259
283
  )
260
284
  end
261
285
  end
@@ -417,17 +441,6 @@ module Steep
417
441
 
418
442
  params.join(", ")
419
443
  end
420
-
421
- def relative_name_in_context(type_name, context)
422
- context.each do |namespace|
423
- if (type_name.to_s == namespace.to_type_name.to_s || type_name.namespace.to_s == "::")
424
- return RBS::TypeName.new(namespace: RBS::Namespace.empty, name: type_name.name)
425
- elsif type_name.to_s.start_with?(namespace.to_s)
426
- return TypeName(type_name.to_s.sub(namespace.to_type_name.to_s, '')).relative!
427
- end
428
- end
429
- type_name
430
- end
431
444
  end
432
445
  end
433
446
  end