steep 1.4.0.dev.2 → 1.4.0.dev.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -2
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +9 -11
  5. data/Gemfile.steep +1 -2
  6. data/Gemfile.steep.lock +11 -14
  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 +80 -6
  15. data/lib/steep/drivers/check.rb +4 -4
  16. data/lib/steep/interface/block.rb +10 -0
  17. data/lib/steep/interface/builder.rb +3 -3
  18. data/lib/steep/method_name.rb +8 -0
  19. data/lib/steep/module_helper.rb +13 -11
  20. data/lib/steep/path_helper.rb +4 -0
  21. data/lib/steep/server/interaction_worker.rb +197 -230
  22. data/lib/steep/server/lsp_formatter.rb +308 -154
  23. data/lib/steep/server/master.rb +4 -1
  24. data/lib/steep/services/completion_provider.rb +140 -103
  25. data/lib/steep/services/hover_provider/rbs.rb +37 -32
  26. data/lib/steep/services/signature_help_provider.rb +108 -0
  27. data/lib/steep/services/type_name_completion.rb +165 -0
  28. data/lib/steep/source.rb +1 -0
  29. data/lib/steep/type_construction.rb +460 -266
  30. data/lib/steep/type_inference/block_params.rb +13 -0
  31. data/lib/steep/type_inference/context.rb +3 -3
  32. data/lib/steep/type_inference/method_call.rb +1 -1
  33. data/lib/steep/type_inference/method_params.rb +42 -16
  34. data/lib/steep/type_inference/send_args.rb +80 -51
  35. data/lib/steep/type_inference/type_env.rb +12 -4
  36. data/lib/steep/version.rb +1 -1
  37. data/lib/steep.rb +2 -0
  38. data/rbs_collection.steep.lock.yaml +0 -28
  39. data/rbs_collection.steep.yaml +10 -9
  40. data/sample/Steepfile +2 -0
  41. data/sample/lib/conference.rb +12 -0
  42. data/sample/sig/conference.rbs +5 -0
  43. data/sig/shims/language-server_protocol.rbs +277 -0
  44. data/sig/shims/parser/nodes.rbs +37 -0
  45. data/sig/shims/parser.rbs +4 -0
  46. data/sig/shims/string.rbs +4 -0
  47. data/sig/steep/ast/types/factory.rbs +10 -8
  48. data/sig/steep/diagnostic/lsp_formatter.rbs +1 -1
  49. data/sig/steep/diagnostic/ruby.rbs +38 -2
  50. data/sig/steep/drivers/check.rbs +1 -1
  51. data/sig/steep/drivers/checkfile.rbs +1 -1
  52. data/sig/steep/drivers/diagnostic_printer.rbs +1 -1
  53. data/sig/steep/drivers/watch.rbs +1 -1
  54. data/sig/steep/index/signature_symbol_provider.rbs +1 -1
  55. data/sig/steep/interface/block.rbs +2 -0
  56. data/sig/steep/interface/builder.rbs +5 -3
  57. data/sig/steep/interface/method_type.rbs +5 -3
  58. data/sig/steep/method_name.rbs +5 -1
  59. data/sig/steep/module_helper.rbs +9 -0
  60. data/sig/steep/path_helper.rbs +3 -1
  61. data/sig/steep/server/base_worker.rbs +1 -1
  62. data/sig/steep/server/interaction_worker.rbs +52 -17
  63. data/sig/steep/server/lsp_formatter.rbs +43 -18
  64. data/sig/steep/server/master.rbs +1 -1
  65. data/sig/steep/server/type_check_worker.rbs +7 -5
  66. data/sig/steep/server/worker_process.rbs +6 -4
  67. data/sig/steep/services/completion_provider.rbs +106 -28
  68. data/sig/steep/services/hover_provider/rbs.rbs +13 -9
  69. data/sig/steep/services/signature_help_provider.rbs +39 -0
  70. data/sig/steep/services/type_name_completion.rbs +122 -0
  71. data/sig/steep/type_construction.rbs +99 -30
  72. data/sig/steep/type_inference/block_params.rbs +4 -0
  73. data/sig/steep/type_inference/context.rbs +70 -22
  74. data/sig/steep/type_inference/method_call.rbs +1 -1
  75. data/sig/steep/type_inference/method_params.rbs +43 -24
  76. data/sig/steep/type_inference/multiple_assignment.rbs +1 -1
  77. data/sig/steep/type_inference/send_args.rbs +19 -5
  78. data/sig/steep/typing.rbs +8 -3
  79. data/smoke/diagnostics/test_expectations.yml +1 -0
  80. data/steep.gemspec +0 -1
  81. metadata +12 -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
@@ -839,8 +914,7 @@ module Steep
839
914
  end
840
915
 
841
916
  def self.all_error
842
- @all_error ||= ALL.each.with_object({}) do |klass, hash|
843
- # @type var hash: Hash[singleton(Base), LSPFormatter::severity]
917
+ @all_error ||= ALL.each.with_object({}) do |klass, hash| #$ Hash[singleton(Base), LSPFormatter::severity]
844
918
  hash[klass] = LSPFormatter::ERROR
845
919
  end.freeze
846
920
  end
@@ -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(
@@ -373,15 +373,15 @@ module Steep
373
373
  types1 = shape1.methods[name]&.method_types or raise
374
374
  types2 = shape2.methods[name]&.method_types or raise
375
375
 
376
- if types1 == types2
376
+ if types1 == types2 && types1.map {|type| type.method_decls.to_a }.to_set == types2.map {|type| type.method_decls.to_a }.to_set
377
377
  shape.methods[name] = (shape1.methods[name] or raise)
378
378
  else
379
- method_types = {}
379
+ method_types = {} #: Hash[MethodType, true]
380
380
 
381
381
  types1.each do |type1|
382
382
  types2.each do |type2|
383
383
  if type1 == type2
384
- method_types[type1] = true
384
+ method_types[type1.with(method_decls: type1.method_decls + type2.method_decls)] = true
385
385
  else
386
386
  if type = MethodType.union(type1, type2, subtyping)
387
387
  method_types[type] = true
@@ -4,6 +4,10 @@ module Steep
4
4
  def to_s
5
5
  "#{type_name}##{method_name}"
6
6
  end
7
+
8
+ def relative
9
+ InstanceMethodName.new(type_name: type_name.relative!, method_name: method_name)
10
+ end
7
11
  end
8
12
 
9
13
  SingletonMethodName = _ = Struct.new(:type_name, :method_name, keyword_init: true) do
@@ -11,6 +15,10 @@ module Steep
11
15
  def to_s
12
16
  "#{type_name}.#{method_name}"
13
17
  end
18
+
19
+ def relative
20
+ SingletonMethodName.new(type_name: type_name.relative!, method_name: method_name)
21
+ end
14
22
  end
15
23
 
16
24
  class ::Object
@@ -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