steep-relaxed 1.9.3.3

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.
Files changed (165) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.gitmodules +0 -0
  4. data/CHANGELOG.md +1032 -0
  5. data/LICENSE +21 -0
  6. data/README.md +260 -0
  7. data/Rakefile +227 -0
  8. data/STDGEM_DEPENDENCIES.txt +59 -0
  9. data/Steepfile +68 -0
  10. data/bin/console +14 -0
  11. data/bin/generate-diagnostics-docs.rb +112 -0
  12. data/bin/mem_graph.rb +67 -0
  13. data/bin/mem_prof.rb +102 -0
  14. data/bin/output_rebaseline.rb +34 -0
  15. data/bin/output_test.rb +60 -0
  16. data/bin/rbs +20 -0
  17. data/bin/rbs-inline +19 -0
  18. data/bin/setup +9 -0
  19. data/bin/stackprof_test.rb +19 -0
  20. data/bin/steep +19 -0
  21. data/bin/steep-check.rb +251 -0
  22. data/bin/steep-prof +16 -0
  23. data/doc/narrowing.md +195 -0
  24. data/doc/shape.md +194 -0
  25. data/exe/steep +18 -0
  26. data/guides/README.md +5 -0
  27. data/guides/src/gem-rbs-collection/gem-rbs-collection.md +126 -0
  28. data/guides/src/getting-started/getting-started.md +163 -0
  29. data/guides/src/nil-optional/nil-optional.md +195 -0
  30. data/lib/steep/annotation_parser.rb +199 -0
  31. data/lib/steep/ast/annotation/collection.rb +172 -0
  32. data/lib/steep/ast/annotation.rb +137 -0
  33. data/lib/steep/ast/builtin.rb +104 -0
  34. data/lib/steep/ast/ignore.rb +148 -0
  35. data/lib/steep/ast/node/type_application.rb +88 -0
  36. data/lib/steep/ast/node/type_assertion.rb +81 -0
  37. data/lib/steep/ast/types/any.rb +35 -0
  38. data/lib/steep/ast/types/boolean.rb +45 -0
  39. data/lib/steep/ast/types/bot.rb +35 -0
  40. data/lib/steep/ast/types/class.rb +43 -0
  41. data/lib/steep/ast/types/factory.rb +557 -0
  42. data/lib/steep/ast/types/helper.rb +40 -0
  43. data/lib/steep/ast/types/instance.rb +42 -0
  44. data/lib/steep/ast/types/intersection.rb +93 -0
  45. data/lib/steep/ast/types/literal.rb +59 -0
  46. data/lib/steep/ast/types/logic.rb +84 -0
  47. data/lib/steep/ast/types/name.rb +128 -0
  48. data/lib/steep/ast/types/nil.rb +41 -0
  49. data/lib/steep/ast/types/proc.rb +117 -0
  50. data/lib/steep/ast/types/record.rb +79 -0
  51. data/lib/steep/ast/types/self.rb +43 -0
  52. data/lib/steep/ast/types/shared_instance.rb +11 -0
  53. data/lib/steep/ast/types/top.rb +35 -0
  54. data/lib/steep/ast/types/tuple.rb +60 -0
  55. data/lib/steep/ast/types/union.rb +97 -0
  56. data/lib/steep/ast/types/var.rb +65 -0
  57. data/lib/steep/ast/types/void.rb +35 -0
  58. data/lib/steep/cli.rb +401 -0
  59. data/lib/steep/diagnostic/deprecated/else_on_exhaustive_case.rb +20 -0
  60. data/lib/steep/diagnostic/deprecated/unknown_constant_assigned.rb +28 -0
  61. data/lib/steep/diagnostic/helper.rb +18 -0
  62. data/lib/steep/diagnostic/lsp_formatter.rb +78 -0
  63. data/lib/steep/diagnostic/result_printer2.rb +48 -0
  64. data/lib/steep/diagnostic/ruby.rb +1221 -0
  65. data/lib/steep/diagnostic/signature.rb +570 -0
  66. data/lib/steep/drivers/annotations.rb +52 -0
  67. data/lib/steep/drivers/check.rb +339 -0
  68. data/lib/steep/drivers/checkfile.rb +210 -0
  69. data/lib/steep/drivers/diagnostic_printer.rb +105 -0
  70. data/lib/steep/drivers/init.rb +66 -0
  71. data/lib/steep/drivers/langserver.rb +56 -0
  72. data/lib/steep/drivers/print_project.rb +113 -0
  73. data/lib/steep/drivers/stats.rb +203 -0
  74. data/lib/steep/drivers/utils/driver_helper.rb +143 -0
  75. data/lib/steep/drivers/utils/jobs_option.rb +26 -0
  76. data/lib/steep/drivers/vendor.rb +27 -0
  77. data/lib/steep/drivers/watch.rb +194 -0
  78. data/lib/steep/drivers/worker.rb +58 -0
  79. data/lib/steep/equatable.rb +23 -0
  80. data/lib/steep/expectations.rb +228 -0
  81. data/lib/steep/index/rbs_index.rb +350 -0
  82. data/lib/steep/index/signature_symbol_provider.rb +185 -0
  83. data/lib/steep/index/source_index.rb +167 -0
  84. data/lib/steep/interface/block.rb +103 -0
  85. data/lib/steep/interface/builder.rb +843 -0
  86. data/lib/steep/interface/function.rb +1090 -0
  87. data/lib/steep/interface/method_type.rb +330 -0
  88. data/lib/steep/interface/shape.rb +239 -0
  89. data/lib/steep/interface/substitution.rb +159 -0
  90. data/lib/steep/interface/type_param.rb +115 -0
  91. data/lib/steep/located_value.rb +20 -0
  92. data/lib/steep/method_name.rb +42 -0
  93. data/lib/steep/module_helper.rb +24 -0
  94. data/lib/steep/node_helper.rb +273 -0
  95. data/lib/steep/path_helper.rb +30 -0
  96. data/lib/steep/project/dsl.rb +268 -0
  97. data/lib/steep/project/group.rb +31 -0
  98. data/lib/steep/project/options.rb +63 -0
  99. data/lib/steep/project/pattern.rb +59 -0
  100. data/lib/steep/project/target.rb +92 -0
  101. data/lib/steep/project.rb +78 -0
  102. data/lib/steep/rake_task.rb +132 -0
  103. data/lib/steep/range_extension.rb +29 -0
  104. data/lib/steep/server/base_worker.rb +97 -0
  105. data/lib/steep/server/change_buffer.rb +73 -0
  106. data/lib/steep/server/custom_methods.rb +77 -0
  107. data/lib/steep/server/delay_queue.rb +45 -0
  108. data/lib/steep/server/interaction_worker.rb +492 -0
  109. data/lib/steep/server/lsp_formatter.rb +455 -0
  110. data/lib/steep/server/master.rb +922 -0
  111. data/lib/steep/server/target_group_files.rb +205 -0
  112. data/lib/steep/server/type_check_controller.rb +366 -0
  113. data/lib/steep/server/type_check_worker.rb +303 -0
  114. data/lib/steep/server/work_done_progress.rb +64 -0
  115. data/lib/steep/server/worker_process.rb +176 -0
  116. data/lib/steep/services/completion_provider.rb +802 -0
  117. data/lib/steep/services/content_change.rb +61 -0
  118. data/lib/steep/services/file_loader.rb +74 -0
  119. data/lib/steep/services/goto_service.rb +441 -0
  120. data/lib/steep/services/hover_provider/rbs.rb +88 -0
  121. data/lib/steep/services/hover_provider/ruby.rb +221 -0
  122. data/lib/steep/services/hover_provider/singleton_methods.rb +20 -0
  123. data/lib/steep/services/path_assignment.rb +46 -0
  124. data/lib/steep/services/signature_help_provider.rb +202 -0
  125. data/lib/steep/services/signature_service.rb +428 -0
  126. data/lib/steep/services/stats_calculator.rb +68 -0
  127. data/lib/steep/services/type_check_service.rb +394 -0
  128. data/lib/steep/services/type_name_completion.rb +236 -0
  129. data/lib/steep/signature/validator.rb +651 -0
  130. data/lib/steep/source/ignore_ranges.rb +69 -0
  131. data/lib/steep/source.rb +691 -0
  132. data/lib/steep/subtyping/cache.rb +30 -0
  133. data/lib/steep/subtyping/check.rb +1113 -0
  134. data/lib/steep/subtyping/constraints.rb +341 -0
  135. data/lib/steep/subtyping/relation.rb +101 -0
  136. data/lib/steep/subtyping/result.rb +324 -0
  137. data/lib/steep/subtyping/variable_variance.rb +89 -0
  138. data/lib/steep/test.rb +9 -0
  139. data/lib/steep/thread_waiter.rb +43 -0
  140. data/lib/steep/type_construction.rb +5183 -0
  141. data/lib/steep/type_inference/block_params.rb +416 -0
  142. data/lib/steep/type_inference/case_when.rb +303 -0
  143. data/lib/steep/type_inference/constant_env.rb +56 -0
  144. data/lib/steep/type_inference/context.rb +195 -0
  145. data/lib/steep/type_inference/logic_type_interpreter.rb +613 -0
  146. data/lib/steep/type_inference/method_call.rb +193 -0
  147. data/lib/steep/type_inference/method_params.rb +531 -0
  148. data/lib/steep/type_inference/multiple_assignment.rb +194 -0
  149. data/lib/steep/type_inference/send_args.rb +712 -0
  150. data/lib/steep/type_inference/type_env.rb +341 -0
  151. data/lib/steep/type_inference/type_env_builder.rb +138 -0
  152. data/lib/steep/typing.rb +321 -0
  153. data/lib/steep/version.rb +3 -0
  154. data/lib/steep.rb +369 -0
  155. data/manual/annotations.md +181 -0
  156. data/manual/ignore.md +20 -0
  157. data/manual/ruby-diagnostics.md +1879 -0
  158. data/sample/Steepfile +22 -0
  159. data/sample/lib/conference.rb +49 -0
  160. data/sample/lib/length.rb +35 -0
  161. data/sample/sig/conference.rbs +42 -0
  162. data/sample/sig/generics.rbs +15 -0
  163. data/sample/sig/length.rbs +34 -0
  164. data/steep-relaxed.gemspec +56 -0
  165. metadata +340 -0
@@ -0,0 +1,126 @@
1
+ # Using RBS from gem_rbs_collection
2
+
3
+ gem_rbs_collection is a repository of type definitions of gems that are managed by the community. You may find the type definition of a gem that ships without RBS type definitions.
4
+
5
+ To use RBS files from the repository, you can use rbs-collection subcommand. This guide explains how the command works.
6
+
7
+ ## Quick start
8
+
9
+ Run rbs-collection-init to start setup.
10
+
11
+ ```
12
+ $ rbs collection init
13
+ ```
14
+
15
+ You have to edit your `Gemfile`. Specify `require: false` for gems for which you do not want type definitions.
16
+
17
+ ```ruby
18
+ gem 'rbs', require: false
19
+ gem 'steep', require: false
20
+ gem 'rbs_rails', require: false
21
+ gem 'rbs_protobuf', require: false
22
+ ```
23
+
24
+ Once you save the file, run the install command.
25
+
26
+ ```
27
+ $ rbs collection install
28
+ ```
29
+
30
+ That generates `rbs_collection.lock.yaml` and downloads the RBS files from the git repository.
31
+
32
+ Note that rbs-collection automatically downloads RBS files of gems included in your Bundler environment. You don't have to write all of the gems in your `Gemfile` in `rbs_collection.yaml`.
33
+
34
+ Finally, we recommend adding the `rbs_collection.yaml` and `rbs_collection.lock.yaml` to your repository, and ignoring `.gem_rbs_collection` directory.
35
+
36
+ ```
37
+ $ git add rbs_collection.yaml
38
+ $ git add rbs_collection.lock.yaml
39
+ $ echo /.gem_rbs_collection >> .gitignore
40
+ $ git commit -m "Set up rbs-collection"
41
+ ```
42
+
43
+ ## Updating RBS files
44
+
45
+ You may want to run rbs-collection-update to update the contents of `rbs_collection.lock.yaml`, when you add a new gem or some gems are updated.
46
+
47
+ You also need to run rbs-collection-update when the RBS files in the source repository are updated. The type definitions are updated with bug-fixes or improvements, and you need to update to apply the changes to your app.
48
+
49
+ ## Using rbs-collection with Steep
50
+
51
+ Steep automatically reads `rb_collection.yaml`. You can use Steep immediately without any modifications.
52
+
53
+ ```
54
+ $ bin/steep project # Show dependencies detected by Steep
55
+ $ bin/steep check # Run type check
56
+ ```
57
+
58
+ ## Migration
59
+
60
+ If you have used older versions of Steep or RBS, you may have configured libraries manually.
61
+
62
+ * You have library calls in `Steepfile`
63
+ * You have git submodules in your git repository to download gem_rbs_collection
64
+
65
+ These are steps to migrate to rbs-collection.
66
+
67
+ 1. Remove unnecessary library configuration
68
+ 2. Set up rbs-collection
69
+ 3. Validate the configuration
70
+ 4. Delete submodule
71
+
72
+ ### 1. Remove unnecessary library configurations
73
+
74
+ You may have `#library` calls in your Steepfile, or something equivalent in your scripts. We can group the configured libraries into three groups.
75
+
76
+ 1. Gems that is managed by Bundler
77
+ 2. Standard libraries (non-gem libs, default gems, and bundled gems)
78
+ * 2-1) That is a transitive dependency from libraries in group 1
79
+ * 2-2) That is included in Gemfile.lock
80
+ * 2-3) Implicitly installed gems – not included in Gemfile
81
+
82
+ You can delete library configs of 1, 2-1, and 2-2. So, you have to keep the configurations of libraries in 2-3.
83
+
84
+ Practically, you can remove all library configs and `#library` calls in `Steepfile`, go step 2, run steep check to test, and restore the configs of libraries if error is detected.
85
+
86
+ ### 2. Set up rbs-collection
87
+
88
+ See the quick start section above!
89
+
90
+ ### 3. Validate the configuration
91
+
92
+ Run steep check to validate the configuration.
93
+
94
+ ```
95
+ $ bin/steep check
96
+ ```
97
+
98
+ You may see the `UnknownTypeName` error if some libraries are missing. Or some errors that implies duplication or inconsistency of methods/class/module definitions, that may be caused by loading RBS files of a library twice.
99
+
100
+ Running steep project may help you too. It shows the source files and library directories recognized by Steep.
101
+
102
+ ```
103
+ $ bin/steep project
104
+ ```
105
+
106
+ Note that the type errors detected may change after migrating to rbs-collection, because it typically includes updating to newer versions of RBS files.
107
+
108
+ ### 4. Delete submodule
109
+
110
+ After you confirmed everything is working correctly, you can delete the submodule. deinit the submodule, remove the directory using git-rm, and delete $GIT_DIR/modules/<name>/.
111
+
112
+ ## What is the rbs_collection.yaml file?
113
+
114
+ The file mainly defines three properties – sources, gems and path. Sources is essential when you want to create a new RBS file repository, usually for RBS files of the private gems.
115
+
116
+ Another trick is ignoring RBS files of type checker toolchain. RBS and Steep ships with their own RBS files. However, these RBS files may be unnecessary for you, unless you are not a type checking toolchain developer. It requires some additional gems and RBS files. So adding ignore: true is recommended for the gems.
117
+
118
+ It seems like we need to add a feature to skip loading RBS files automatically and use the feature for RBS, Steep, and RBS Rails. It looks weird that the gems section is only used to ignore gems.
119
+
120
+ ## Versions of RBS files in gem_rbs_collection
121
+
122
+ Gem versions in rbs-collection are relatively loosely managed. If a gem is found but the version is different, rbs-collection simply uses the incorrect version.
123
+
124
+ It will load RBS files of activerecord/6.1 even if your Gemfile specifies activerecord-7.0.4. This is by design with an assumption that having RBS files with some incompatibility is better than having nothing. We see most APIs are compatible even after major version upgrade. Dropping everything for minor API incompatibilities would not make much sense.
125
+
126
+ That behavior will change in future versions when we see that the assumption is not reasonable and we have better coverage.
@@ -0,0 +1,163 @@
1
+ # Getting Started with Steep in 5 minutes
2
+
3
+ ## Installing Steep
4
+
5
+ Add the lines to Gemfile:
6
+
7
+ ```rb
8
+ group :development do
9
+ gem "steep", require: false
10
+ end
11
+ ```
12
+
13
+ and install the gems.
14
+
15
+ ```
16
+ $ bundle install
17
+ ```
18
+
19
+ Alternatively, you can install it with the gem command.
20
+
21
+ ```
22
+ $ gem install steep
23
+ ```
24
+
25
+ Execute the following command to confirm if the command is successfully installed.
26
+
27
+ ```
28
+ $ steep version
29
+ $ bundle exec steep version # If you install with bundler
30
+ ```
31
+
32
+ We omit the `bundle exec` prefix from the following commands. Run commands with the prefix if you installed Steep with Bundler.
33
+
34
+ ## Type checking your first Ruby script
35
+
36
+ Run the `steep init` command to generate the configuration file, `Steepfile`.
37
+
38
+ ```
39
+ $ steep init
40
+ ```
41
+
42
+ Open `Steepfile` in your text editor, and replace the content with the following lines:
43
+
44
+ ```rb
45
+ target :lib do
46
+ signature "sig"
47
+ check "lib"
48
+ end
49
+ ```
50
+
51
+ Type the following Ruby code in your editor, and save it as `lib/hello.rb`.
52
+
53
+ ```rb
54
+ currencies = { US: "$", JP: "¥", UK: "£" }
55
+ country = %w(US JP UK).sample()
56
+
57
+ puts "Hello! The price is #{currencies[country]}100. 💸"
58
+ ```
59
+
60
+ And type check it with Steep.
61
+
62
+ ```
63
+ $ steep check
64
+ ```
65
+
66
+ The output will report a type error.
67
+
68
+ ```
69
+ # Type checking files:
70
+
71
+ ........................................................F
72
+
73
+ lib/hello.rb:4:39: [error] Cannot pass a value of type `(::String | nil)` as an argument of type `::Symbol`
74
+ │ (::String | nil) <: ::Symbol
75
+ │ ::String <: ::Symbol
76
+ │ ::Object <: ::Symbol
77
+ │ ::BasicObject <: ::Symbol
78
+
79
+ │ Diagnostic ID: Ruby::ArgumentTypeMismatch
80
+
81
+ └ puts "Hello! The price is #{currencies[country]}100. 💸"
82
+ ~~~~~~~
83
+
84
+ Detected 1 problem from 1 file
85
+ ```
86
+
87
+ The error says that the type of the country variable causes a type error. It is expected to be a symbol, but string or `nil` will be given.
88
+
89
+ Let's see how we can fix the error.
90
+
91
+ ## Fixing the type error
92
+
93
+ The first step is converting the string value to a symbol. We can add a `#to_sym` call.
94
+
95
+ ```rb
96
+ currencies = { US: "$", JP: "¥", UK: "£" }
97
+ country = %w(US JP UK).sample()
98
+
99
+ puts "Hello! The price is #{currencies[country.to_sym]}100. 💸"
100
+ ```
101
+
102
+ The `#to_sym` call will convert a string into a symbol. Does it solve the problem??
103
+
104
+ ```
105
+ $ steep check
106
+ # Type checking files:
107
+
108
+ ........................................................F
109
+
110
+ lib/hello.rb:4:47: [error] Type `(::String | nil)` does not have method `to_sym`
111
+ │ Diagnostic ID: Ruby::NoMethod
112
+
113
+ └ puts "Hello! The price is #{currencies[country.to_sym]}100. 💸"
114
+ ~~~~~~
115
+
116
+ Detected 1 problem from 1 file
117
+ ```
118
+
119
+ It detects another problem. The first error was `Ruby::ArgumentTypeMismatch`, but the new error is `Ruby::NoMethod`. The value of `country` may be `nil`, and it doesn't have the `#to_sym` method.
120
+
121
+ This would be annoying, but one of the most common sources of a type error. The value of an expression may be `nil` unexpectedly, and using the value of the expression may cause an error.
122
+
123
+ In this case, the `sample()` call introduces the `nil`. `Array#sample()` returns `nil` when the array is empty. We know the receiver of the `sample` call cannot be `nil`, because it is an array literal. But the type checker doesn't know of it. The source code detects the `Array#sample()` method is called, but it ignores the fact that the receiver cannot be empty.
124
+
125
+ Instead, we can simply tell the type checker that the value of the country cannot be `nil`.
126
+
127
+ # Satisfying the type checker by adding a guard
128
+
129
+ The underlying type system supports flow-sensitive typing similar to TypeScript and Rust. It detects conditional expressions testing the value of a variable and propagates the knowledge that the value cannot be `nil`.
130
+
131
+ We can fix the type error with an `or` construct.
132
+
133
+ ```rb
134
+ currencies = { US: "$", JP: "¥", UK: "£" }
135
+ country = %w(US JP UK).sample() or raise
136
+
137
+ puts "Hello! The price is #{currencies[country.to_sym]}100. 💸"
138
+ ```
139
+
140
+ The change lets the type checking succeed.
141
+
142
+ ```
143
+ $ steep check
144
+ # Type checking files:
145
+
146
+ .........................................................
147
+
148
+ No type error detected. 🫖
149
+ ```
150
+
151
+ The `raise` method is called when `sample()` returns `nil`. Steep can reason the possible control flow based on the semantics of or in Ruby:
152
+
153
+ * The value of `country` is the return value of `sample()` method call
154
+ * The value of `country` may be `nil`
155
+ * If the value of `country` is `nil`, the right hand side of the or is evaluated
156
+ * It calls the `raise` method, which results in an exception and jumps to somewhere
157
+ * So, when the execution continues to the puts line, the value of `country` cannot be `nil`
158
+
159
+ There are two possibilities of the type of the result of the `sample()` call, `nil` or a string. We humans can reason that we can safely ignore the case of `nil`. But, the type checker cannot. We have to add `or raise` to tell the type checker it can stop considering the case of `nil` safely.
160
+
161
+ ## Next steps
162
+
163
+ This is a really quick introduction to using Steep. You may have noticed that I haven't explained anything about defining new classes or modules. See the RBS guide for more examples!
@@ -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.
@@ -0,0 +1,199 @@
1
+ module Steep
2
+ class AnnotationParser
3
+ VAR_NAME = /[a-z][A-Za-z0-9_]*/
4
+ METHOD_NAME = Regexp.union(
5
+ /[^.:\s]+/
6
+ )
7
+ CONST_NAME = Regexp.union(
8
+ /(::)?([A-Z][A-Za-z0-9_]*::)*[A-Z][A-Za-z0-9_]*/
9
+ )
10
+ DYNAMIC_NAME = /(self\??\.)?#{METHOD_NAME}/
11
+ IVAR_NAME = /@[^:\s]+/
12
+
13
+ attr_reader :factory
14
+
15
+ def initialize(factory:)
16
+ @factory = factory
17
+ end
18
+
19
+ class SyntaxError < StandardError
20
+ attr_reader :source
21
+ attr_reader :location
22
+
23
+ def initialize(source:, location:, exn: nil, message: nil)
24
+ @source = source
25
+ @location = location
26
+
27
+ if exn
28
+ message =
29
+ case exn
30
+ when RBS::ParsingError
31
+ Diagnostic::Signature::SyntaxError.parser_syntax_error_message(exn)
32
+ else
33
+ exn.message
34
+ end
35
+ end
36
+
37
+ super message
38
+ end
39
+ end
40
+
41
+ TYPE = /(?<type>.*)/
42
+ COLON = /\s*:\s*/
43
+
44
+ PARAM = /[A-Z][A-Za-z0-9_]*/
45
+ TYPE_PARAMS = /(\[(?<params>#{PARAM}(,\s*#{PARAM})*)\])?/
46
+
47
+ def parse_type(match, name = :type, location:)
48
+ string = match[name] or raise
49
+ st, en = match.offset(name)
50
+ st or raise
51
+ en or raise
52
+ loc = RBS::Location.new(location.buffer, location.start_pos + st, location.start_pos + en)
53
+
54
+ type =
55
+ begin
56
+ RBS::Parser.parse_type(string)
57
+ rescue RBS::ParsingError => exn
58
+ raise SyntaxError.new(source: string, location: loc, exn: exn)
59
+ end or raise
60
+
61
+ unless (type.location || raise).source == string.strip
62
+ raise SyntaxError.new(source: string, location: loc, message: "Failed to parse a type in annotation")
63
+ end
64
+
65
+ factory.type(type)
66
+ end
67
+
68
+ def keyword_subject_type(keyword, name)
69
+ /@type\s+#{keyword}\s+(?<name>#{name})#{COLON}#{TYPE}/
70
+ end
71
+
72
+ def keyword_and_type(keyword)
73
+ /@type\s+#{keyword}#{COLON}#{TYPE}/
74
+ end
75
+
76
+ def parse(src, location:)
77
+ case src
78
+ when keyword_subject_type("var", VAR_NAME)
79
+ Regexp.last_match.yield_self do |match|
80
+ match or raise
81
+ name = match[:name] or raise
82
+
83
+ AST::Annotation::VarType.new(name: name.to_sym,
84
+ type: parse_type(match, location: location),
85
+ location: location)
86
+ end
87
+
88
+ when keyword_subject_type("method", METHOD_NAME)
89
+ Regexp.last_match.yield_self do |match|
90
+ match or raise
91
+ name = match[:name] or raise
92
+ type = match[:type] or raise
93
+
94
+ method_type = factory.method_type(RBS::Parser.parse_method_type(type) || raise)
95
+
96
+ AST::Annotation::MethodType.new(name: name.to_sym,
97
+ type: method_type,
98
+ location: location)
99
+ end
100
+
101
+ when keyword_subject_type("const", CONST_NAME)
102
+ Regexp.last_match.yield_self do |match|
103
+ match or raise
104
+ name = match[:name] or raise
105
+ type = parse_type(match, location: location)
106
+
107
+ AST::Annotation::ConstType.new(name: RBS::TypeName.parse(name), type: type, location: location)
108
+ end
109
+
110
+ when keyword_subject_type("ivar", IVAR_NAME)
111
+ Regexp.last_match.yield_self do |match|
112
+ match or raise
113
+ name = match[:name] or raise
114
+ type = parse_type(match, location: location)
115
+
116
+ AST::Annotation::IvarType.new(name: name.to_sym,
117
+ type: type,
118
+ location: location)
119
+ end
120
+
121
+ when keyword_and_type("return")
122
+ Regexp.last_match.yield_self do |match|
123
+ match or raise
124
+ type = parse_type(match, location: location)
125
+ AST::Annotation::ReturnType.new(type: type, location: location)
126
+ end
127
+
128
+ when keyword_and_type("block")
129
+ Regexp.last_match.yield_self do |match|
130
+ match or raise
131
+ type = parse_type(match, location: location)
132
+ AST::Annotation::BlockType.new(type: type, location: location)
133
+ end
134
+
135
+ when keyword_and_type("self")
136
+ Regexp.last_match.yield_self do |match|
137
+ match or raise
138
+ type = parse_type(match, location: location)
139
+ AST::Annotation::SelfType.new(type: type, location: location)
140
+ end
141
+
142
+ when keyword_and_type("instance")
143
+ Regexp.last_match.yield_self do |match|
144
+ match or raise
145
+ type = parse_type(match, location: location)
146
+ AST::Annotation::InstanceType.new(type: type, location: location)
147
+ end
148
+
149
+ when keyword_and_type("module")
150
+ Regexp.last_match.yield_self do |match|
151
+ match or raise
152
+ type = parse_type(match, location: location)
153
+ AST::Annotation::ModuleType.new(type: type, location: location)
154
+ end
155
+
156
+ when keyword_and_type("break")
157
+ Regexp.last_match.yield_self do |match|
158
+ match or raise
159
+ type = parse_type(match, location: location)
160
+
161
+ AST::Annotation::BreakType.new(type: type, location: location)
162
+ end
163
+
164
+ when /@dynamic\s+(?<names>(#{DYNAMIC_NAME}\s*,\s*)*#{DYNAMIC_NAME})/
165
+ Regexp.last_match.yield_self do |match|
166
+ match or raise
167
+ names = (match[:names] || raise).split(/\s*,\s*/)
168
+
169
+ AST::Annotation::Dynamic.new(
170
+ names: names.map {|name|
171
+ case
172
+ when name.delete_prefix!("self.")
173
+ AST::Annotation::Dynamic::Name.new(name: name.to_sym, kind: :module)
174
+ when name.delete_prefix!("self?.")
175
+ AST::Annotation::Dynamic::Name.new(name: name.to_sym, kind: :module_instance)
176
+ else
177
+ AST::Annotation::Dynamic::Name.new(name: name.to_sym, kind: :instance)
178
+ end
179
+ },
180
+ location: location
181
+ )
182
+ end
183
+
184
+ when /@implements\s+(?<name>#{CONST_NAME})#{TYPE_PARAMS}$/
185
+ Regexp.last_match.yield_self do |match|
186
+ match or raise
187
+ type_name = RBS::TypeName.parse(match[:name] || raise)
188
+ params = match[:params]&.yield_self {|params| params.split(/,/).map {|param| param.strip.to_sym } } || []
189
+
190
+ name = AST::Annotation::Implements::Module.new(name: type_name, args: params)
191
+ AST::Annotation::Implements.new(name: name, location: location)
192
+ end
193
+ end
194
+
195
+ rescue RBS::ParsingError => exn
196
+ raise SyntaxError.new(source: src, location: location, exn: exn)
197
+ end
198
+ end
199
+ end