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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +7 -9
- data/Gemfile.steep +1 -2
- data/Gemfile.steep.lock +9 -10
- data/README.md +7 -1
- data/Steepfile +0 -3
- data/bin/rbs +0 -1
- data/guides/README.md +5 -0
- data/guides/src/gem-rbs-collection/gem-rbs-collection.md +143 -0
- data/guides/src/getting-started/getting-started.md +164 -0
- data/guides/src/nil-optional/nil-optional.md +195 -0
- data/lib/steep/diagnostic/ruby.rb +79 -4
- data/lib/steep/drivers/check.rb +4 -4
- data/lib/steep/interface/block.rb +10 -0
- data/lib/steep/module_helper.rb +13 -11
- data/lib/steep/path_helper.rb +4 -0
- data/lib/steep/server/interaction_worker.rb +105 -92
- data/lib/steep/services/type_name_completion.rb +157 -0
- data/lib/steep/source.rb +1 -0
- data/lib/steep/type_construction.rb +402 -229
- data/lib/steep/type_inference/block_params.rb +13 -0
- data/lib/steep/type_inference/context.rb +3 -3
- data/lib/steep/type_inference/method_params.rb +42 -16
- data/lib/steep/type_inference/send_args.rb +79 -50
- data/lib/steep/type_inference/type_env.rb +7 -1
- data/lib/steep/version.rb +1 -1
- data/lib/steep.rb +1 -0
- data/rbs_collection.steep.lock.yaml +0 -28
- data/rbs_collection.steep.yaml +10 -9
- data/sample/lib/conference.rb +12 -0
- data/sample/sig/conference.rbs +5 -0
- data/sig/shims/language-server_protocol.rbs +12 -0
- data/sig/shims/parser/nodes.rbs +37 -0
- data/sig/shims/parser.rbs +1 -0
- data/sig/shims/string.rbs +4 -0
- data/sig/steep/ast/types/factory.rbs +10 -8
- data/sig/steep/diagnostic/lsp_formatter.rbs +1 -1
- data/sig/steep/diagnostic/ruby.rbs +38 -2
- data/sig/steep/drivers/check.rbs +1 -1
- data/sig/steep/drivers/checkfile.rbs +1 -1
- data/sig/steep/drivers/diagnostic_printer.rbs +1 -1
- data/sig/steep/drivers/watch.rbs +1 -1
- data/sig/steep/index/signature_symbol_provider.rbs +1 -1
- data/sig/steep/interface/block.rbs +2 -0
- data/sig/steep/interface/builder.rbs +5 -3
- data/sig/steep/interface/method_type.rbs +5 -3
- data/sig/steep/module_helper.rbs +9 -0
- data/sig/steep/path_helper.rbs +3 -1
- data/sig/steep/server/base_worker.rbs +1 -1
- data/sig/steep/server/interaction_worker.rbs +46 -17
- data/sig/steep/server/master.rbs +1 -1
- data/sig/steep/server/type_check_worker.rbs +7 -5
- data/sig/steep/server/worker_process.rbs +6 -4
- data/sig/steep/services/completion_provider.rbs +2 -0
- data/sig/steep/services/type_name_completion.rbs +122 -0
- data/sig/steep/type_construction.rbs +99 -30
- data/sig/steep/type_inference/block_params.rbs +4 -0
- data/sig/steep/type_inference/context.rbs +70 -22
- data/sig/steep/type_inference/method_params.rbs +43 -24
- data/sig/steep/type_inference/multiple_assignment.rbs +1 -1
- data/sig/steep/type_inference/send_args.rbs +13 -3
- data/sig/steep/typing.rbs +7 -2
- data/smoke/diagnostics/test_expectations.yml +1 -0
- data/steep.gemspec +0 -1
- 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
|
-
|
40
|
+
"(Blocks are incompatible)"
|
41
41
|
when relation.function?
|
42
42
|
nil
|
43
43
|
when relation.params?
|
44
|
-
|
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
|
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
|
data/lib/steep/drivers/check.rb
CHANGED
@@ -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 =
|
34
|
-
client_writer =
|
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 =
|
37
|
-
server_writer =
|
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(
|
data/lib/steep/module_helper.rb
CHANGED
@@ -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)
|
5
|
-
|
6
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
data/lib/steep/path_helper.rb
CHANGED
@@ -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
|
-
|
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
|
131
|
-
sig_service = service.signature_services[target.name]
|
132
|
+
target = targets[0] or raise
|
133
|
+
sig_service = service.signature_services[target.name] or raise
|
132
134
|
relative_path = job.path
|
133
|
-
|
134
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
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,
|
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 -
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
)
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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:
|
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:
|
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:
|
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:
|
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
|