sorbet-eraser 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ddbc396f3f164a3d0e67e9cf7046733ce52f792eb0abc17e9f5f056f415cd5e
4
- data.tar.gz: 965ef9955b75bba0a2702ea127d5923d7e4b1a55ed2b2e6dca239970adbb0fe1
3
+ metadata.gz: cbfc0c7e028bd4e712e64d29a7c74e65d45937087a3f510f01afab434250b3d5
4
+ data.tar.gz: 6a89286f19c7ae6ed8e838beba49fcb799426fb14a58ed4b5c3b32d9b59935dc
5
5
  SHA512:
6
- metadata.gz: 5deb006431e8e31ed089fe6e8fba281f3c65487fc6996a853b05bb0d9600ff034232e7319fb44912a69dc89b60b160900b9b503a17991d19da2bf02f5803c5f8
7
- data.tar.gz: a6a20f8aa21b2a2c9e75a4c7e83db5b28556516296d16f55eedd0113da807cbf0c3a827fbeda7eaf9fb95d2a29a9f0c4ddc9090d638e00469989520d2c492f1d
6
+ metadata.gz: efe378c67fa4749ac7461496d895c69e89ef28ca1f71d6c9385952a96320e8849a6ed15604d8219560903d047b0f9a1eda60fb938b07dc633f55da95a1082119
7
+ data.tar.gz: af3a02739fbbfdec2fd98c1bbb749b74a2a7f088c237f339966b186ad4b565e8cd353bac2bd4989f2fba8ae7435a096a058c4aca5cded0f209113826e284e994
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.1] - 2023-06-27
10
+
11
+ ### Added
12
+
13
+ - Shims for `T::Configuration`, `T::Private::RuntimeLevels`, and `T::Methods`.
14
+
15
+ ### Changed
16
+
17
+ - Fixed various parsing bugs due to incorrect location.
18
+
9
19
  ## [0.3.0] - 2023-06-27
10
20
 
11
21
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-eraser (0.3.0)
4
+ sorbet-eraser (0.3.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -10,6 +10,7 @@ GEM
10
10
  rake (13.0.6)
11
11
 
12
12
  PLATFORMS
13
+ arm64-darwin-21
13
14
  arm64-darwin-22
14
15
  x86_64-darwin-19
15
16
  x86_64-darwin-21
@@ -22,4 +23,4 @@ DEPENDENCIES
22
23
  sorbet-eraser!
23
24
 
24
25
  BUNDLED WITH
25
- 2.2.15
26
+ 2.4.12
data/README.md CHANGED
@@ -5,9 +5,37 @@
5
5
 
6
6
  Erase all traces of `sorbet-runtime` code.
7
7
 
8
- `sorbet` is a great tool for development. However, in production, it incurs a penalty because it still functions as Ruby code. Even if you completely shim all `sorbet-runtime` method calls (for example by replacing `sig {} ` with a method that immediately returns) you still pay the cost of a method call in the first place.
8
+ [Sorbet](https://sorbet.org/) is a type checker for Ruby. To annotate types in your Ruby code, you use constructs like `sig` and `T.let`. Sorbet then uses a static analysis tool to check that your code is type safe. At runtime, these types are enforced by the `sorbet-runtime` gem that provides implementations of all of these constructs.
9
9
 
10
- This gem takes a different approach, but entirely eliminating the `sig` method call (as well as all the other `sorbet-runtime` constructs) from the source before Ruby compiles it.
10
+ Sometimes, you want to use Sorbet for development, but don't want to run `sorbet-runtime` in production. This may be because you have a performance-critical application, or because you're writing a library and you don't want to impose a runtime dependency on your users.
11
+
12
+ To handle these use cases, `sorbet-eraser` provides a way to erase all traces of `sorbet-runtime` code from your source code. This means that you can use Sorbet for development, but not have to worry about `sorbet-runtime` in production. For example,
13
+
14
+ ```ruby
15
+ class HelloWorld
16
+ extend T::Sig
17
+
18
+ sig { returns(String) }
19
+ def hello
20
+ T.let("World!", String)
21
+ end
22
+ end
23
+ ```
24
+
25
+ will be transformed into
26
+
27
+ ```ruby
28
+ class HelloWorld
29
+
30
+
31
+
32
+ def hello
33
+ "World!"
34
+ end
35
+ end
36
+ ```
37
+
38
+ Notice that the `extend T::Sig` and `sig` constructs have been removed from your source code. Notice also that all line and column information has been preserved 1:1, so that stack traces and tracepoints will still be accurate.
11
39
 
12
40
  ## Installation
13
41
 
@@ -27,19 +55,37 @@ Or install it yourself as:
27
55
 
28
56
  ## Usage
29
57
 
30
- There are two ways to use this gem, depending on your needs.
58
+ There are two ways to use this gem, depending on your needs. You can erase `sorbet-runtime` code ahead of time or just in time.
31
59
 
32
- The first is that you can hook into the Ruby compilation process and do just-in-time erasure. To do this — before any code is loaded that would require a `sorbet-runtime` construct — call `require "sorbet/eraser/autoload"`. This will hook into the autoload process to erase all `sorbet-runtime` code before it gets passed to Ruby to parse. This eliminates the need for a build step, but slows down your parse/boot time.
60
+ ### Ahead of time
33
61
 
34
- The second is that you can preprocess your Ruby files using either the CLI or the Ruby API. With the CLI, you would run:
62
+ To erase `sorbet-runtime` code ahead of time, you would either use the CLI provided with this gem or the Ruby API. With the CLI, you would run:
35
63
 
36
64
  ```bash
37
65
  bundle exec sorbet-eraser '**/*.rb'
38
66
  ```
39
67
 
40
- It accepts any number of filepaths/patterns on the command line and will modify the source files with their erased contents. Alternatively, you can programmatically use this gem through the `Sorbet::Eraser.erase(source)` API, where `source` is a string that represents valid Ruby code. Ruby code without the listed constructs will be returned.
68
+ It accepts any number of filepaths/patterns on the command line and will modify the source files in place with their erased contents. If you would instead prefer to script it yourself using the Ruby API, you would run:
69
+
70
+ ```ruby
71
+ Sorbet::Eraser.erase(source)
72
+ ```
73
+
74
+ where `source` is a string that represents valid Ruby code.
75
+
76
+ ### Just in time
77
+
78
+ If you're looking to avoid a build step like the one described above, you can instead erase your code immediately before it is compiled by the Ruby virtual machine. To do, call:
79
+
80
+ ```ruby
81
+ require "sorbet/eraser/autoload"
82
+ ```
83
+
84
+ as soon as possible when your application is first booting. This will hook into the autoload process to erase all `sorbet-runtime` code before it gets passed to Ruby to parse. Note that the tradeoff here is that it eliminates the need for a build step, but slows down your parse/boot time.
85
+
86
+ ### Runtime structures
41
87
 
42
- If you used any runtime structures like `T::Struct` or `T::Enum` you'll need a runtime shim. For that, you can add `require "t"` to your codebase, which ships with this gem, or in your gemfile `gem "sorbet-eraser", require: "t"`.
88
+ If you used any runtime structures like `T::Struct` or `T::Enum` you'll need a runtime shim. We provide very basic versions of these in the `sorbet-eraser` gem, and they are required automatically.
43
89
 
44
90
  ### Status
45
91
 
@@ -70,7 +70,7 @@ module Sorbet
70
70
  class ParsingError < StandardError
71
71
  end
72
72
 
73
- attr_reader :source, :line_counts, :errors, :patterns
73
+ attr_reader :source, :line_counts, :errors, :patterns, :heredocs
74
74
 
75
75
  def initialize(source)
76
76
  super(source)
@@ -91,6 +91,7 @@ module Sorbet
91
91
 
92
92
  @errors = []
93
93
  @patterns = []
94
+ @heredocs = []
94
95
  end
95
96
 
96
97
  def self.erase(source)
@@ -133,15 +134,6 @@ module Sorbet
133
134
  end
134
135
  end
135
136
 
136
- # Loop through all of the scanner events and define a basic method that
137
- # wraps everything into a node class.
138
- SCANNER_EVENTS.each do |event|
139
- define_method(:"on_#{event}") do |value|
140
- range = loc.then { |start| start...(start + (value&.size || 0)) }
141
- Node.new(:"@#{event}", [value], range)
142
- end
143
- end
144
-
145
137
  # Better location information for aref.
146
138
  def on_aref(recv, arg)
147
139
  rend = arg.range.end + source[arg.range.end..].index("]") + 1
@@ -150,23 +142,90 @@ module Sorbet
150
142
 
151
143
  # Better location information for arg_paren.
152
144
  def on_arg_paren(arg)
153
- rbegin = source[..arg.range.begin].rindex("(")
154
- rend = arg.range.end + source[arg.range.end..].index(")") + 1
155
- Node.new(:arg_paren, [arg], rbegin...rend)
145
+ if arg
146
+ rbegin = source[..arg.range.begin].rindex("(")
147
+ rend = arg.range.end + source[arg.range.end..].index(")") + 1
148
+ Node.new(:arg_paren, [arg], rbegin...rend)
149
+ else
150
+ segment = source[..loc]
151
+ Node.new(:arg_paren, [arg], segment.rindex("(")...(segment.rindex(")") + 1))
152
+ end
153
+ end
154
+
155
+ LISTS = { qsymbols: "%i", qwords: "%w", symbols: "%I", words: "%W" }.freeze
156
+ TERMINATORS = { "[" => "]", "{" => "}", "(" => ")", "<" => ">" }.freeze
157
+
158
+ # Better location information for array.
159
+ def on_array(arg)
160
+ case arg&.event
161
+ when nil
162
+ segment = source[..loc]
163
+ Node.new(:array, [arg], segment.rindex("[")...(segment.rindex("]") + 1))
164
+ when :qsymbols, :qwords, :symbols, :words
165
+ rbegin = source[...arg.range.begin].rindex(LISTS.fetch(arg.event))
166
+ rend = source[arg.range.end..].index(TERMINATORS.fetch(source[rbegin + 2]) { source[rbegin + 2] }) + arg.range.end + 1
167
+ Node.new(:array, [arg], rbegin...rend)
168
+ else
169
+ Node.new(:array, [arg], arg.range)
170
+ end
156
171
  end
157
172
 
158
173
  # Better location information for brace_block.
159
174
  def on_brace_block(params, body)
160
- rbegin = source[...(params || body).range.begin].rindex("{")
161
- rend = body.range.end + source[body.range.end..].index("}") + 1
162
- Node.new(:brace_block, [params, body], rbegin...rend)
175
+ if params || body.range
176
+ rbegin = source[...(params || body).range.begin].rindex("{")
177
+
178
+ rend = body.range&.end || params.range.end
179
+ rend = rend + source[rend..].index("}") + 1
180
+
181
+ Node.new(:brace_block, [params, body], rbegin...rend)
182
+ else
183
+ segment = source[..loc]
184
+ Node.new(:brace_block, [params, body], segment.rindex("{")...(segment.rindex("}") + 1))
185
+ end
163
186
  end
164
187
 
165
188
  # Better location information for do_block.
166
189
  def on_do_block(params, body)
167
- rbegin = source[...(params || body).range.begin].rindex("do")
168
- rend = body.range.end + source[body.range.end..].index("end") + 3
169
- Node.new(:do_block, [params, body], rbegin...rend)
190
+ if params || body.range
191
+ rbegin = source[...(params || body).range.begin].rindex("do")
192
+
193
+ rend = body.range&.end || params.range.end
194
+ rend = rend + source[rend..].index("end") + 3
195
+
196
+ Node.new(:do_block, [params, body], rbegin...rend)
197
+ else
198
+ segment = source[..loc]
199
+ Node.new(:do_block, [params, body], segment.rindex("do")...(segment.rindex("end") + 3))
200
+ end
201
+ end
202
+
203
+ # Better location information for hash.
204
+ def on_hash(arg)
205
+ if arg
206
+ Node.new(:hash, [arg], arg.range)
207
+ else
208
+ segment = source[..loc]
209
+ Node.new(:hash, [arg], segment.rindex("{")...(segment.rindex("}") + 1))
210
+ end
211
+ end
212
+
213
+ # Track the open heredocs so we can replace the string literal ranges with
214
+ # the range of their declarations.
215
+ def on_heredoc_beg(value)
216
+ range = loc.then { |start| start...(start + value.size) }
217
+ heredocs << [range, value, nil]
218
+
219
+ Node.new(:@heredoc_beg, [value], range)
220
+ end
221
+
222
+ # If a heredoc ends, then the next string literal event will be the
223
+ # heredoc.
224
+ def on_heredoc_end(value)
225
+ range = loc.then { |start| start...(start + value.size) }
226
+ heredocs.find { |(_, beg_arg, end_arg)| beg_arg.include?(value.strip) && end_arg.nil? }[2] = value
227
+
228
+ Node.new(:@heredoc_end, [value], range)
170
229
  end
171
230
 
172
231
  # Track the parsing errors for nicer error messages.
@@ -174,12 +233,33 @@ module Sorbet
174
233
  errors << "line #{lineno}: #{error}"
175
234
  end
176
235
 
236
+ # Better location information for string_literal taking into account
237
+ # heredocs.
238
+ def on_string_literal(arg)
239
+ if heredoc = heredocs.find { |(_, _, end_arg)| end_arg }
240
+ Node.new(:string_literal, [arg], heredocs.delete(heredoc)[0])
241
+ else
242
+ Node.new(:string_literal, [arg], arg.range)
243
+ end
244
+ end
245
+
246
+ handled = private_instance_methods(false)
247
+
248
+ # Loop through all of the scanner events and define a basic method that
249
+ # wraps everything into a node class.
250
+ SCANNER_EVENTS.each do |event|
251
+ next if handled.include?(:"on_#{event}")
252
+
253
+ define_method(:"on_#{event}") do |value|
254
+ range = loc.then { |start| start...(start + (value&.size || 0)) }
255
+ Node.new(:"@#{event}", [value], range)
256
+ end
257
+ end
258
+
177
259
  # Loop through the parser events and generate a method for each event. If
178
260
  # it's one of the _new methods, then use arrays like SexpBuilderPP. If
179
261
  # it's an _add method then just append to the array. If it's a normal
180
262
  # method, then create a new node and determine its bounds.
181
- handled = private_instance_methods(false)
182
-
183
263
  PARSER_EVENT_TABLE.each do |event, arity|
184
264
  next if handled.include?(:"on_#{event}")
185
265
 
@@ -194,8 +274,10 @@ module Sorbet
194
274
  range =
195
275
  if node.body.empty?
196
276
  value.range
197
- else
277
+ elsif node.range && value.range
198
278
  (node.range.begin...value.range.end)
279
+ else
280
+ node.range || value.range
199
281
  end
200
282
 
201
283
  node.class.new(node.event, node.body + [value], range)
@@ -101,12 +101,12 @@ module Sorbet
101
101
  end
102
102
 
103
103
  def on_method_add_arg(call, arg_paren)
104
- if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type|unsafe)>>/) && arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
104
+ if call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type|unsafe)>>\z/) && arg_paren.match?(/\A<arg_paren <args_add_block <args .+> false>>\z/)
105
105
  # T.must(foo)
106
106
  # T.reveal_type(foo)
107
107
  # T.unsafe(foo)
108
108
  patterns << TOneArgMethodCallParensPattern.new(call.range.begin...arg_paren.range.end)
109
- elsif call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>\z/) && arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
109
+ elsif call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>\z/) && arg_paren.match?(/\A<arg_paren <args_add_block <args .+> false>>\z/)
110
110
  # T.assert_type!(foo, bar)
111
111
  # T.cast(foo, bar)
112
112
  # T.let(foo, bar)
@@ -114,18 +114,18 @@ module Sorbet
114
114
  call.range.begin...arg_paren.range.end,
115
115
  comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
116
116
  )
117
- elsif call.match?(/<call <var_ref <@const T>> <@period \.> <@ident bind>>/) && arg_paren.match?(/<arg_paren <args_add_block <args <var_ref <@kw self>> .+> false>>/)
117
+ elsif call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident bind>>\z/) && arg_paren.match?(/\A<arg_paren <args_add_block <args <var_ref <@kw self>> .+> false>>\z/)
118
118
  # T.bind(self, foo)
119
119
  patterns << TTwoArgMethodCallParensPattern.new(
120
120
  call.range.begin...arg_paren.range.end,
121
121
  comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
122
122
  )
123
- elsif call.match?(/<fcall <@ident (?:abstract|final|interface)!>>/) && arg_paren.match?("<args >")
123
+ elsif call.match?(/\A<fcall <@ident (?:abstract|final|interface)!>>\z/) && arg_paren.match?("<args >")
124
124
  # abstract!
125
125
  # final!
126
126
  # interface!
127
- patterns << DeclarationPattern.new(call.range.begin...arg_paren.range.end)
128
- elsif call.match?("<fcall <@ident mixes_in_class_methods>>") && arg_paren.match?(/<arg_paren <args_add_block <args <.+>>> false>>/)
127
+ patterns << DeclarationPattern.new(call.range)
128
+ elsif call.match?("<fcall <@ident mixes_in_class_methods>>") && arg_paren.match?(/\A<arg_paren <args_add_block <args <.+>>> false>>\z/)
129
129
  # mixes_in_class_methods(foo)
130
130
  patterns << MixesInClassMethodsPattern.new(call.range.begin...arg_paren.range.end)
131
131
  end
@@ -159,8 +159,8 @@ module Sorbet
159
159
  end
160
160
 
161
161
  def on_command(ident, args_add_block)
162
- if ident.match?(/<@ident (?:const|prop)>/)
163
- if args_add_block.match?(/<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> <bare_assoc_hash .+> false>/)
162
+ if ident.match?(/\A<@ident (?:const|prop)>\z/)
163
+ if args_add_block.match?(/\A<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> <bare_assoc_hash .+> false>\z/)
164
164
  # prop :foo, String, default: ""
165
165
  # const :foo, String, default: ""
166
166
  patterns << PropWithOptionsPattern.new(
@@ -168,7 +168,7 @@ module Sorbet
168
168
  first_comma: args_add_block.body[0].body[0].range.end - ident.range.begin,
169
169
  second_comma: args_add_block.body[0].body[1].range.end - ident.range.begin
170
170
  )
171
- elsif args_add_block.match?(/<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> false>/)
171
+ elsif args_add_block.match?(/\A<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> false>\z/)
172
172
  # prop :foo, String
173
173
  # const :foo, String
174
174
  patterns << PropWithoutOptionsPattern.new(
@@ -182,7 +182,7 @@ module Sorbet
182
182
  end
183
183
 
184
184
  def on_command_call(var_ref, period, ident, args_add_block)
185
- if var_ref.match?("<var_ref <@const T>>") && period.match?("<@period .>") && ident.match?(/<@ident (?:must|reveal_type|unsafe)>/) && args_add_block.match?(/<args_add_block <args <.+>> false>/) && args_add_block.body[0].body.length == 1
185
+ if var_ref.match?("<var_ref <@const T>>") && period.match?("<@period .>") && ident.match?(/\A<@ident (?:must|reveal_type|unsafe)>\z/) && args_add_block.match?(/\A<args_add_block <args <.+>> false>\z/) && args_add_block.body[0].body.length == 1
186
186
  # T.must foo
187
187
  # T.reveal_type foo
188
188
  # T.unsafe foo
@@ -211,10 +211,10 @@ module Sorbet
211
211
  end
212
212
 
213
213
  def on_stmts_add(node, value)
214
- if value.match?(/<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <brace_block <stmts .+>>>/)
214
+ if value.match?(/\A<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <brace_block <stmts .+>>>\z/)
215
215
  # sig { foo }
216
216
  patterns << SigBracesPattern.new(value.range)
217
- elsif value.match?(/<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <do_block <bodystmt .+>>>/)
217
+ elsif value.match?(/\A<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <do_block <bodystmt .+>>>\z/)
218
218
  # sig do foo end
219
219
  patterns << SigBlockPattern.new(value.range)
220
220
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sorbet
4
4
  module Eraser
5
- VERSION = "0.3.0"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
data/lib/t/props.rb CHANGED
@@ -41,13 +41,7 @@ module T
41
41
  # class level and set appropriate values.
42
42
  def initialize(hash = {})
43
43
  self.class.props.each do |name, rules|
44
- if hash.key?(name)
45
- instance_variable_set("@#{name}", hash.delete(name))
46
- elsif rules.key?(:default)
47
- instance_variable_set("@#{name}", rules[:default])
48
- else
49
- raise ArgumentError, "missing keyword: #{name}"
50
- end
44
+ instance_variable_set("@#{name}", hash.key?(name) ? hash.delete(name) : rules[:default])
51
45
  end
52
46
 
53
47
  raise ArgumentError, "unknown keyword: #{hash.keys.first}" unless hash.empty?
data/lib/t.rb CHANGED
@@ -33,6 +33,34 @@ module T
33
33
  module Sig
34
34
  end
35
35
 
36
+ # I really don't want to be shimming this, but there are places where people
37
+ # try to reference these values.
38
+ module Private
39
+ module RuntimeLevels
40
+ def self.default_checked_level; :never; end
41
+ end
42
+
43
+ module Methods
44
+ module MethodHooks
45
+ end
46
+
47
+ module SingletonMethodHooks
48
+ end
49
+
50
+ def self.signature_for_method(method); method; end
51
+ end
52
+ end
53
+
54
+ # I also don't want to shim this, but there are places where people will
55
+ # reference it.
56
+ module Configuration
57
+ class << self
58
+ attr_accessor :inline_type_error_handler,
59
+ :call_validation_error_handler,
60
+ :sig_builder_error_handler
61
+ end
62
+ end
63
+
36
64
  # Type aliases don't actually do anything, but they are usually assigned to
37
65
  # constants, so in that case we need to return something.
38
66
  def self.type_alias
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-eraser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Newton
@@ -88,7 +88,7 @@ licenses:
88
88
  - MIT
89
89
  metadata:
90
90
  bug_tracker_uri: https://github.com/kddnewton/sorbet-eraser/issues
91
- changelog_uri: https://github.com/kddnewton/sorbet-eraser/blob/v0.3.0/CHANGELOG.md
91
+ changelog_uri: https://github.com/kddnewton/sorbet-eraser/blob/v0.3.1/CHANGELOG.md
92
92
  source_code_uri: https://github.com/kddnewton/sorbet-eraser
93
93
  rubygems_mfa_required: 'true'
94
94
  post_install_message: