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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +3 -2
- data/README.md +53 -7
- data/lib/sorbet/eraser/parser.rb +104 -22
- data/lib/sorbet/eraser/patterns.rb +12 -12
- data/lib/sorbet/eraser/version.rb +1 -1
- data/lib/t/props.rb +1 -7
- data/lib/t.rb +28 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cbfc0c7e028bd4e712e64d29a7c74e65d45937087a3f510f01afab434250b3d5
|
4
|
+
data.tar.gz: 6a89286f19c7ae6ed8e838beba49fcb799426fb14a58ed4b5c3b32d9b59935dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
60
|
+
### Ahead of time
|
33
61
|
|
34
|
-
|
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.
|
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.
|
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
|
|
data/lib/sorbet/eraser/parser.rb
CHANGED
@@ -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
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
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?(
|
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?(
|
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?(
|
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?(
|
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
|
128
|
-
elsif call.match?("<fcall <@ident mixes_in_class_methods>>") && arg_paren.match?(
|
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?(
|
163
|
-
if args_add_block.match?(
|
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?(
|
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?(
|
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?(
|
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?(
|
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
|
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
|
-
|
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.
|
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.
|
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:
|