-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathStaticTypingGuide.html
187 lines (172 loc) · 9.91 KB
/
StaticTypingGuide.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<link rel="stylesheet" href="toc.css">
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
</head>
<body>
<ul class="toc"><li>
<a href="#static-typing-guide">Static Typing Guide</a><ul>
<li>
<a href="#quick-start">Quick start</a><ul>
<li>
<a href="#run-the-type-check">Run the type check</a><ul></ul>
</li>
<li>
<a href="#generate-type-signature-skeleton">Generate type signature skeleton</a><ul></ul>
</li>
</ul>
</li>
<li>
<a href="#layout">Layout</a><ul></ul>
</li>
<li>
<a href="#progressive-typing">Progressive typing</a><ul>
<li>
<a href="#type-checking-vs-signature-loading">Type checking vs signature loading</a><ul></ul>
</li>
<li>
<a href="#dependency-signatures">Dependency signatures</a><ul></ul>
</li>
<li>
<a href="#measuring-progress">Measuring progress</a><ul></ul>
</li>
</ul>
</li>
<li>
<a href="#typing-a-file">Typing a file</a><ul>
<li>
<a href="#basics">Basics</a><ul></ul>
</li>
<li>
<a href="#type-profiling">Type profiling</a><ul></ul>
</li>
</ul>
</li>
<li>
<a href="#useful-commands">Useful commands</a><ul>
<li>
<a href="#list-stale-.rbs">List stale .rbs</a><ul></ul>
</li>
<li>
<a href="#list-missing-.rbs">List missing .rbs</a><ul></ul>
</li>
<li>
<a href="#generate-.rbs-skeletons">Generate .rbs skeletons</a><ul></ul>
</li>
</ul>
</li>
</ul>
</li></ul>
<h1 id="static-typing-guide">Static Typing Guide</h1>
<p>Static typing description is achieved via Ruby core <a href="https://github.com/ruby/rbs">RBS</a>.</p>
<p>Static type checking is achieved via <a href="https://github.com/soutaro/steep">Steep</a>.</p>
<h2 id="quick-start">Quick start</h2>
<h3 id="run-the-type-check">Run the type check</h3>
<pre><code>bundle exec steep check [sources]</code></pre>
<p>The <code>sources</code> arguments are optional and used to scope type checking to a smaller set of files or directories.</p>
<h3 id="generate-type-signature-skeleton">Generate type signature skeleton</h3>
<pre><code>bundle exec rbs prototype rb [source files]</code></pre>
<p>Outputs <code>.rbs</code> content on stdout. The <code>source files</code> arguments lists the files from which the skeleton will be statically built from parsing. No evaluation occurs, therefore this has limited typing analysis capability, typically resulting in a lot of <code>untyped</code>.</p>
<p>Note: Comments are reproduced as is which is useful for a visual check but should be manually removed when moving the skeleton to a <code>.rbs</code> file, because of the duplication and risk of comments getting desynced.</p>
<h2 id="layout">Layout</h2>
<p>Ruby code in <code>.rb</code> files are, as is customary for a gem, stored in <code>lib</code>.</p>
<p>While RBS type annotations could be put inline, this creates a lot of noise, hampering “pure Ruby” readability. While the closeness with the code itself may look like an advantage, such annotations live in comments, which are harder to read and mix with other comments.</p>
<p>RBS types can be described in any number of <code>.rbs</code> files, stored in <code>sig</code>. These files can be generated, syntax highlighted, checked, linted, and more. This is therefore the chosen approach.</p>
<p>While the presence of <code>.rb</code> and <code>.rbs</code> files is entirely decoupled, here we choose to have one <code>.rbs</code> file per <code>.rb</code> file, mirroring the <code>lib</code> structure in <code>sig</code>. This has a number of advantages such as tracking typing progress, noticing stale files, generating new files without messing with existing type information, configuring IDEs and editors to jump from source to signature and back…</p>
<p>Tools such as <code>rbs prototype</code> output comments. These should be removed, and only comments relevant to typing should end up in <code>.rbs</code> files.</p>
<h2 id="progressive-typing">Progressive typing</h2>
<p>Similar to many other Ruby tools, Steep reads project configuration from a DSL in <code>Steepfile</code>. We will use that to allow progressive typing.</p>
<h3 id="type-checking-vs-signature-loading">Type checking vs signature loading</h3>
<p>Steep distinguishes between loading signatures and actually checking code for signatures. This is extremely useful to progressively type code, limiting check scope while still being able to provide signatures to code that can’t be fully checked yet.</p>
<pre><code>target :default do
signature "sig" # ALL signatures from this directory will be loaded
check "lib/foo/bar" # ONLY this source code folder will be checked against, using ALL signatures above
ignore "lib/foo/bar/baz" # EXCEPT this subfolder
end</code></pre>
<h3 id="dependency-signatures">Dependency signatures</h3>
<p>Steep starts with a <a href="https://github.com/ruby/rbs/tree/master/core">minimal core</a> loaded type signatures. Adding more <a href="https://github.com/ruby/rbs/tree/master/stdlib">types from the Ruby stdlib</a> should be done progressively as required:</p>
<pre><code> library "set" # adds typing for Ruby stdlib's Set</code></pre>
<p>Note: These signatures are part of <a href=""><code>rbs</code></a>, which is included in Ruby releases since Ruby 3.0.</p>
<p>Gems can embed a <code>sig</code> directory, which can be used directly:</p>
<pre><code> library "some_gem_with_a_sig_dir"</code></pre>
<p>Some gems don’t have typing information.</p>
<p>In addition, a <a href="https://github.com/ruby/gem_rbs_collection">vast collection of gems</a> have been typed. These can be fetched via a Rubygems/Bundler-like feature of RBS called <a href="https://github.com/ruby/rbs/blob/e91be7275f4005b1aeac8eadc2faa2b4ad5fdfef/docs/collection.md">collections</a></p>
<pre><code> collection_config "rbs_collection.steep.yaml"</code></pre>
<p>This yaml file is akin to a Gemfile, describes the sources and gem signatures to fetch, and also has a lockfile mechanism. It can also integrate with <code>bundler</code> to match the signatures with the gem versions in use.</p>
<p>Otherwise signatures can be vendored:</p>
<pre><code> repo_path "vendor/rbs"
library "subdir"</code></pre>
<p>Typically these are be written as needed for gems entirely missing signatures, and ideally contributed back either upstream to the gem project itself or to the gem rbs collection project.</p>
<h3 id="measuring-progress">Measuring progress</h3>
<p>With the described layout and 1:1 match, it becomes easy to track coarse-grained coverage, additions, removals, changes through refactorings, in a similar way as is usually done with unit tests or specs.</p>
<p>In addition, to output typing detailed coverage statistics:</p>
<pre><code>bundle exec steep stats</code></pre>
<h2 id="typing-a-file">Typing a file</h2>
<h3 id="basics">Basics</h3>
<p>To type a <code>.rb</code> file without a matching <code>.rbs</code> file, start with the skeleton:</p>
<pre><code>mkdir -p sig/foo
bundle exec rbs prototype rb lib/foo/bar.rb > sig/foo/bar.rbs</code></pre>
<p>One can then proceed to <a href="https://github.com/ruby/rbs/blob/e91be7275f4005b1aeac8eadc2faa2b4ad5fdfef/docs/syntax.md">adjusting the signatures</a> (<a href="https://github.com/ruby/rbs/blob/e91be7275f4005b1aeac8eadc2faa2b4ad5fdfef/docs/rbs_by_example.md">by example</a>), removing as much <code>untyped</code> as possible.</p>
<h3 id="type-profiling">Type profiling</h3>
<p>To discover types, one can leverage <a href="https://github.com/ruby/typeprof"><code>typeprof</code></a>. Contrary to <code>rbs prototype rb</code> which relies solely on static parsing, <code>typeprof</code> is a Ruby interpreter, except it doesn’t <em>execute</em> Ruby code, merely evaluates it to track types. Entry point calls to explore the various codepaths are required.</p>
<p>With this file:</p>
<pre><code># test.rb
def foo(x)
p x # reveal type of x
if x > 10
x.to_s
else
nil
end
end
foo(42) # this call is needed otherwise there's nothing evaluated!
foo(3) # make sure to explore as many codepaths as possible to get best coverage</code></pre>
<p>The following is evaluated:</p>
<pre><code>$ typeprof test.rb
# TypeProf 0.21.2
# Revealed types
# foo.rb:3 #=> Integer
# Classes
class Object
private
def foo: (Integer x) -> String?
end</code></pre>
<p>One quick hackish way to type a class is to add a bunch of calls all the way down the file defining that class and run <code>typeprof</code> on it exploring the most interesting codepaths. This can also be achieved with a separate file requiring the one we want to type and performing calls there. In theory <code>typeprof</code> could be run on unit test files having 100% coverage and output precise type information for the tested code.</p>
<p>See the <a href="https://github.com/ruby/typeprof/blob/26ab9108860d9a4ce050acb3422ee7721d4d50b0/doc/demo.md">demo doc</a> for more examples and features.</p>
<h2 id="useful-commands">Useful commands</h2>
<h3 id="list-stale-.rbs">List stale <code>.rbs</code>
</h3>
<pre><code># check everything
bundle exec rake rbs:stale
# check one file
bundle exec rake rbs:stale[sig/foo/bar.rbs]
# check a directory
bundle exec rake rbs:stale[sig/foo]
# clean stale files and empty directories
bundle exec rake rbs:clean</code></pre>
<h3 id="list-missing-.rbs">List missing <code>.rbs</code>
</h3>
<pre><code># check everything
bundle exec rake rbs:missing
# check one file
bundle exec rake rbs:missing[lib/foo/bar.rb]
# check a directory
bundle exec rake rbs:missing[lib/foo]</code></pre>
<h3 id="generate-.rbs-skeletons">Generate <code>.rbs</code> skeletons</h3>
<pre><code># prototype one file if missing
bundle exec rake rbs:prototype[lib/foo/bar.rb]
# prototype one file unconditionally
bundle exec rake rbs:prototype[force,lib/foo/bar.rb]
# prototype missing signatures in a directory
bundle exec rake rbs:prototype[lib/foo]
# prototype all files in a directory
bundle exec rake rbs:prototype[force, lib/foo]
# prototype every missing file
bundle exec rake rbs:prototype
# prototype every file
bundle exec rake rbs:prototype[force]</code></pre>
</body>
</html>