sorbet-eraser 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/main.yml +32 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +2 -2
- data/README.md +36 -23
- data/exe/sorbet-eraser +7 -0
- data/lib/sorbet/eraser/cli.rb +52 -0
- data/lib/sorbet/eraser/parser.rb +21 -6
- data/lib/sorbet/eraser/patterns.rb +78 -66
- data/lib/sorbet/eraser/version.rb +1 -1
- data/lib/sorbet/eraser.rb +1 -8
- data/lib/t/enum.rb +252 -0
- data/lib/t/struct.rb +12 -0
- data/lib/t.rb +46 -0
- data/sorbet-eraser.gemspec +15 -6
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54913e2de72273efae7221b3dc96923c1b45e7d17020ec47bd9a417bea9a62cc
|
4
|
+
data.tar.gz: 71f3cc0a28acefd20d9a4a1f3a06081a568c5b8276cb1241f34d2bb3e7f64fb8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06ca1f48f68eea0d10933f83ffe8ba4b296cda3c87240a5db6704ac27d9d6a12202a46b1af82ee3983ba335d4254f230a8f3d861578556ee76ea3544c551cd70
|
7
|
+
data.tar.gz: 547b88a3fb93e133c69cdd2d05ce684f62cc1650afe836fe9741059d1f567f4704bf11815b3170d8f4df2a9909fcc138fc6d940b45ef5ff615853f93118a2f93
|
@@ -0,0 +1,32 @@
|
|
1
|
+
name: Main
|
2
|
+
on:
|
3
|
+
- push
|
4
|
+
- pull_request_target
|
5
|
+
jobs:
|
6
|
+
ci:
|
7
|
+
name: CI
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
env:
|
10
|
+
CI: true
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@master
|
13
|
+
- uses: ruby/setup-ruby@v1
|
14
|
+
with:
|
15
|
+
ruby-version: 3.0
|
16
|
+
bundler-cache: true
|
17
|
+
- name: Test
|
18
|
+
run: bundle exec rake test
|
19
|
+
automerge:
|
20
|
+
name: AutoMerge
|
21
|
+
needs: ci
|
22
|
+
runs-on: ubuntu-latest
|
23
|
+
if: github.event_name == 'pull_request_target' && (github.actor == github.repository_owner || github.actor == 'dependabot[bot]')
|
24
|
+
steps:
|
25
|
+
- uses: actions/github-script@v3
|
26
|
+
with:
|
27
|
+
script: |
|
28
|
+
github.pulls.merge({
|
29
|
+
owner: context.payload.repository.owner.login,
|
30
|
+
repo: context.payload.repository.name,
|
31
|
+
pull_number: context.payload.pull_request.number
|
32
|
+
})
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.1.1] - 2021-11-17
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- Require MFA for releasing.
|
14
|
+
|
15
|
+
[unreleased]: https://github.com/kddnewton/sorbet-eraser/compare/v0.1.1...HEAD
|
16
|
+
[0.1.1]: https://github.com/kddnewton/sorbet-eraser/compare/f6a712...v0.1.1
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# Sorbet::Eraser
|
2
2
|
|
3
|
+
[![Build Status](https://github.com/kddnewton/sorbet-eraser/workflows/Main/badge.svg)](https://github.com/kddnewton/sorbet-eraser/actions)
|
4
|
+
[![Gem](https://img.shields.io/gem/v/sorbet-eraser.svg)](https://rubygems.org/gems/sorbet-eraser)
|
5
|
+
|
3
6
|
Erase all traces of `sorbet-runtime` code.
|
4
7
|
|
5
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.
|
@@ -11,7 +14,7 @@ This gem takes a different approach, but entirely eliminating the `sig` method c
|
|
11
14
|
Add this line to your application's Gemfile:
|
12
15
|
|
13
16
|
```ruby
|
14
|
-
gem
|
17
|
+
gem "sorbet-eraser"
|
15
18
|
```
|
16
19
|
|
17
20
|
And then execute:
|
@@ -28,32 +31,42 @@ Before any code is loaded that would require a `sorbet-runtime` construct, call
|
|
28
31
|
|
29
32
|
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.
|
30
33
|
|
34
|
+
Finally, this gem ships with a CLI that you can use to modify source files. This is useful for development of this gem itself, but could be useful for others to ensure they see what this gem actually will be doing in production. To run it, run:
|
35
|
+
|
36
|
+
```sh
|
37
|
+
bundle exec sorbet-eraser '**/*.rb'
|
38
|
+
```
|
39
|
+
|
40
|
+
It accepts any number of filepaths/patterns on the command line and will modify the source files with their erased contents.
|
41
|
+
|
31
42
|
### Status
|
32
43
|
|
33
44
|
Below is a table of the status of each `sorbet-runtime` construct and its current support status.
|
34
45
|
|
35
|
-
| Construct
|
36
|
-
|
|
37
|
-
| `
|
38
|
-
| `
|
39
|
-
| `
|
40
|
-
| `
|
41
|
-
| `class Foo < T::
|
42
|
-
| `
|
43
|
-
| `
|
44
|
-
| `
|
45
|
-
| `
|
46
|
-
| `
|
47
|
-
| `T.
|
48
|
-
| `T.
|
49
|
-
| `T.
|
50
|
-
| `T.
|
51
|
-
| `T.
|
52
|
-
| `T.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
46
|
+
| Construct | Status | Replacement |
|
47
|
+
| --------------------------------------------------- | ------ | ----------- |
|
48
|
+
| `extend T::*` | ✅ | Shimmed |
|
49
|
+
| `abstract!`, `final!`, `interface!`, `sealed!` | ✅ | Shimmed |
|
50
|
+
| `mixes_in_class_methods(*)`, `requires_ancestor(*)` | ✅ | Shimmed |
|
51
|
+
| `type_member(*)`, `type_template(*)` | ✅ | Shimmed |
|
52
|
+
| `class Foo < T::Enum` | ✅ | Shimmed |
|
53
|
+
| `class Foo < T::Struct` | 🛠 | Shimmed |
|
54
|
+
| `sig` | ✅ | Removed |
|
55
|
+
| `T.absurd(foo)` | ✅ | Shimmed |
|
56
|
+
| `T.assert_type!(foo, bar)` | ✅ | `foo` |
|
57
|
+
| `T.bind(self, foo)` | ✅ | `self` |
|
58
|
+
| `T.cast(foo, bar)` | ✅ | `foo` |
|
59
|
+
| `T.let(foo, bar)` | ✅ | `foo` |
|
60
|
+
| `T.must(foo)` | ✅ | `foo` |
|
61
|
+
| `T.reveal_type(foo)` | ✅ | `foo` |
|
62
|
+
| `T.type_alias { foo }` | ✅ | Shimmed |
|
63
|
+
| `T.unsafe(foo)` | ✅ | `foo` |
|
64
|
+
|
65
|
+
In the above table:
|
66
|
+
|
67
|
+
* `Shimmed` means that this gem provides a replacement module that will simply do nothing when its respective methods are called. We do this in order to maintain the same interface in the case that someone is doing runtime reflection. Also because anything that is shimmed will not be called that much/will not be in a hot path so performance is not really a consideration for those cases.
|
68
|
+
* `Removed` means that the construct is removed entirely from the source.
|
69
|
+
* Anything else means that the inputted code is replaced with that output.
|
57
70
|
|
58
71
|
## Development
|
59
72
|
|
data/exe/sorbet-eraser
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sorbet
|
4
|
+
module Eraser
|
5
|
+
# A small CLI that takes filepaths and erases them by writing back to the
|
6
|
+
# original file.
|
7
|
+
class CLI
|
8
|
+
POOL_SIZE = 4
|
9
|
+
|
10
|
+
attr_reader :filepaths
|
11
|
+
|
12
|
+
def initialize(filepaths)
|
13
|
+
@filepaths = filepaths
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
queue = Queue.new
|
18
|
+
filepaths.each { |filepath| queue << filepath }
|
19
|
+
|
20
|
+
workers =
|
21
|
+
POOL_SIZE.times.map do
|
22
|
+
# push a symbol onto the queue for each thread so that it knows when
|
23
|
+
# the end of the queue is and will exit its infinite loop
|
24
|
+
queue << :eoq
|
25
|
+
|
26
|
+
Thread.new do
|
27
|
+
while filepath = queue.shift
|
28
|
+
break if filepath == :eoq
|
29
|
+
process(filepath)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
workers.each(&:join)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.start(argv)
|
38
|
+
new(argv.flat_map { |pattern| Dir.glob(pattern) }).start
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def process(filepath)
|
44
|
+
File.write(filepath, Eraser.erase(File.read(filepath)))
|
45
|
+
rescue Parser::ParsingError => error
|
46
|
+
warn("Could not parse #{filepath}: #{error}")
|
47
|
+
rescue => error
|
48
|
+
warn("Could not parse #{filepath}: #{error}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/sorbet/eraser/parser.rb
CHANGED
@@ -35,7 +35,10 @@ module Sorbet
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def [](byteindex)
|
38
|
-
|
38
|
+
# Why the || byteindex? I'm not sure. For some reason ripper is
|
39
|
+
# returning very odd column values when you have a multibyte line.
|
40
|
+
# This is the only way I could find to make it work.
|
41
|
+
@indices[byteindex] || byteindex
|
39
42
|
end
|
40
43
|
end
|
41
44
|
|
@@ -60,11 +63,16 @@ module Sorbet
|
|
60
63
|
end
|
61
64
|
end
|
62
65
|
|
63
|
-
|
66
|
+
# Raised in the case that source can't be parsed.
|
67
|
+
class ParsingError < StandardError
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :source, :line_counts, :errors, :patterns
|
64
71
|
|
65
72
|
def initialize(source)
|
66
73
|
super(source)
|
67
74
|
|
75
|
+
@source = source
|
68
76
|
@line_counts = []
|
69
77
|
last_index = 0
|
70
78
|
|
@@ -78,6 +86,7 @@ module Sorbet
|
|
78
86
|
last_index += line.size
|
79
87
|
end
|
80
88
|
|
89
|
+
@errors = []
|
81
90
|
@patterns = []
|
82
91
|
end
|
83
92
|
|
@@ -85,7 +94,7 @@ module Sorbet
|
|
85
94
|
parser = new(source)
|
86
95
|
|
87
96
|
if parser.parse.nil? || parser.error?
|
88
|
-
raise
|
97
|
+
raise ParsingError, parser.errors.join("\n")
|
89
98
|
else
|
90
99
|
parser.patterns.inject(source) do |current, pattern|
|
91
100
|
pattern.erase(current)
|
@@ -113,14 +122,13 @@ module Sorbet
|
|
113
122
|
# it's an _add method then just append to the array. If it's a normal
|
114
123
|
# method, then create a new node and determine its bounds.
|
115
124
|
PARSER_EVENT_TABLE.each do |event, arity|
|
116
|
-
|
117
|
-
when /\A(.+)_new\z/
|
125
|
+
if event =~ /\A(.+)_new\z/ && event != :assoc_new
|
118
126
|
prefix = $1.to_sym
|
119
127
|
|
120
128
|
define_method(:"on_#{event}") do
|
121
129
|
Node.new(prefix, [], loc.then { |start| start..start })
|
122
130
|
end
|
123
|
-
|
131
|
+
elsif event =~ /_add\z/
|
124
132
|
define_method(:"on_#{event}") do |node, value|
|
125
133
|
range =
|
126
134
|
if node.body.empty?
|
@@ -131,6 +139,8 @@ module Sorbet
|
|
131
139
|
|
132
140
|
node.class.new(node.event, node.body + [value], range)
|
133
141
|
end
|
142
|
+
elsif event == :parse_error
|
143
|
+
# skip this, as we're going to define it below
|
134
144
|
else
|
135
145
|
define_method(:"on_#{event}") do |*args|
|
136
146
|
first, *, last = args.grep(Node).map(&:range)
|
@@ -142,6 +152,11 @@ module Sorbet
|
|
142
152
|
end
|
143
153
|
end
|
144
154
|
end
|
155
|
+
|
156
|
+
# Track the parsing errors for nicer error messages.
|
157
|
+
def on_parse_error(error)
|
158
|
+
errors << "line #{lineno}: #{error}"
|
159
|
+
end
|
145
160
|
end
|
146
161
|
end
|
147
162
|
end
|
@@ -5,10 +5,11 @@ module Sorbet
|
|
5
5
|
module Patterns
|
6
6
|
# A pattern in code that represents a call to a special Sorbet method.
|
7
7
|
class Pattern
|
8
|
-
attr_reader :range
|
8
|
+
attr_reader :range, :metadata
|
9
9
|
|
10
|
-
def initialize(range)
|
10
|
+
def initialize(range, **metadata)
|
11
11
|
@range = range
|
12
|
+
@metadata = metadata
|
12
13
|
end
|
13
14
|
|
14
15
|
def erase(source)
|
@@ -22,27 +23,25 @@ module Sorbet
|
|
22
23
|
source
|
23
24
|
end
|
24
25
|
|
25
|
-
def
|
26
|
-
|
26
|
+
def blank(segment)
|
27
|
+
# This is deceptive in that it hides that it actually replaces
|
28
|
+
# everything with spaces _except_ newline characters, which is keeps
|
29
|
+
# in place.
|
30
|
+
segment.gsub(/./, " ")
|
27
31
|
end
|
28
|
-
end
|
29
32
|
|
30
|
-
# T.absurd(foo) => raise ::Sorbet::Eraser::AbsurdError
|
31
|
-
class TAbsurdParensPattern < Pattern
|
32
33
|
def replace(segment)
|
33
|
-
segment
|
34
|
-
replacement = "raise ::Sorbet::Eraser::AbsurdError"
|
35
|
-
"#{replacement}#{" " * [$1.length - replacement.length, 0].max}#{$2}"
|
36
|
-
end
|
34
|
+
segment
|
37
35
|
end
|
38
36
|
end
|
39
37
|
|
40
38
|
# T.must(foo) => foo
|
41
39
|
# T.reveal_type(foo) => foo
|
40
|
+
# T.unsafe(foo) => foo
|
42
41
|
class TOneArgMethodCallParensPattern < Pattern
|
43
42
|
def replace(segment)
|
44
|
-
segment.gsub(/(T\s*\.(?:must|reveal_type)\(\s*)(.+)(\s*\))(.*)/) do
|
45
|
-
"#{
|
43
|
+
segment.gsub(/(T\s*\.(?:must|reveal_type|unsafe)\(\s*)(.+)(\s*\))(.*)/m) do
|
44
|
+
"#{blank($1)}#{$2}#{blank($3)}#{$4}"
|
46
45
|
end
|
47
46
|
end
|
48
47
|
end
|
@@ -53,87 +52,98 @@ module Sorbet
|
|
53
52
|
# T.let(foo, bar) => let
|
54
53
|
class TTwoArgMethodCallParensPattern < Pattern
|
55
54
|
def replace(segment)
|
56
|
-
segment.
|
57
|
-
|
55
|
+
replacement = segment.dup
|
56
|
+
|
57
|
+
# We can't really rely on regex here because commas have semantic
|
58
|
+
# meaning and you might have some in the value of the first argument.
|
59
|
+
comma = metadata.fetch(:comma)
|
60
|
+
pre, post = 0..comma, (comma + 1)..-1
|
61
|
+
|
62
|
+
replacement[pre] =
|
63
|
+
replacement[pre].gsub(/(T\s*\.(?:assert_type!|bind|cast|let)\(\s*)(.+)(\s*,)(.*)/m) do
|
64
|
+
"#{blank($1)}#{$2}#{blank($3)}#{$4}"
|
65
|
+
end
|
66
|
+
|
67
|
+
replacement[post] = blank(replacement[post])
|
68
|
+
replacement
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# abstract! =>
|
73
|
+
# final! =>
|
74
|
+
# interface! =>
|
75
|
+
class DeclarationPattern < Pattern
|
76
|
+
def replace(segment)
|
77
|
+
segment.gsub(/((?:abstract|final|interface)!(?:\(\s*\))?)(.*)/) do
|
78
|
+
"#{blank($1)}#{$2}"
|
58
79
|
end
|
59
80
|
end
|
60
81
|
end
|
61
82
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
83
|
+
# mixes_in_class_methods(foo) => foo
|
84
|
+
class MixesInClassMethodsPattern < Pattern
|
85
|
+
def replace(segment)
|
86
|
+
segment.gsub(/(mixes_in_class_methods\(\s*)(.+)(\s*\))(.*)/m) do
|
87
|
+
"#{blank($1)}#{$2}#{blank($3)}#{$4}"
|
88
|
+
end
|
67
89
|
end
|
90
|
+
end
|
68
91
|
|
92
|
+
def on_method_add_arg(call, arg_paren)
|
69
93
|
# T.must(foo)
|
70
94
|
# T.reveal_type(foo)
|
71
|
-
|
95
|
+
# T.unsafe(foo)
|
96
|
+
if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type|unsafe)>>/) &&
|
72
97
|
arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
|
73
|
-
patterns << TOneArgMethodCallParensPattern.new(call.range.begin
|
98
|
+
patterns << TOneArgMethodCallParensPattern.new(call.range.begin...arg_paren.range.end)
|
74
99
|
end
|
75
100
|
|
76
101
|
# T.assert_type!(foo, bar)
|
77
102
|
# T.cast(foo, bar)
|
78
103
|
# T.let(foo, bar)
|
79
|
-
if call.match?(
|
104
|
+
if call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>\z/) &&
|
80
105
|
arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
|
81
|
-
patterns <<
|
106
|
+
patterns <<
|
107
|
+
TTwoArgMethodCallParensPattern.new(
|
108
|
+
call.range.begin...arg_paren.range.end,
|
109
|
+
comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
|
110
|
+
)
|
82
111
|
end
|
83
112
|
|
84
113
|
# T.bind(self, foo)
|
85
114
|
if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident bind>>/) &&
|
86
115
|
arg_paren.match?(/<arg_paren <args_add_block <args <var_ref <@kw self>> .+> false>>/)
|
87
|
-
patterns <<
|
116
|
+
patterns <<
|
117
|
+
TTwoArgMethodCallParensPattern.new(
|
118
|
+
call.range.begin...arg_paren.range.end,
|
119
|
+
comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
|
120
|
+
)
|
88
121
|
end
|
89
122
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
segment.gsub(/(T\s*\.type_alias\s*\{.*\})(.*)/) do
|
97
|
-
replacement = "::Sorbet::Eraser::TypeAlias"
|
98
|
-
"#{replacement}#{" " * [$1.length - replacement.length, 0].max}#{$2}"
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def on_method_add_block(method_add_arg, block)
|
104
|
-
# T.type_alias { foo }
|
105
|
-
if method_add_arg.match?("<call <var_ref <@const T>> <@period .> <@ident type_alias>>") &&
|
106
|
-
block.match?(/<brace_block <stmts .+>>/)
|
107
|
-
patterns << TTypeAliasBraceBlockPattern.new(method_add_arg.range.begin..block.range.end)
|
123
|
+
# abstract!
|
124
|
+
# final!
|
125
|
+
# interface!
|
126
|
+
if call.match?(/<fcall <@ident (?:abstract|final|interface)!>>/) &&
|
127
|
+
arg_paren.match?("<args >")
|
128
|
+
patterns << DeclarationPattern.new(call.range.begin...arg_paren.range.end)
|
108
129
|
end
|
109
130
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
class ExtendTSigPattern < Pattern
|
115
|
-
def replace(segment)
|
116
|
-
segment.gsub(/(extend\s+T::Sig)(.*)/) do
|
117
|
-
"#{" " * $1.length}#{$2}"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def on_command(ident, args_add_block)
|
123
|
-
# extend T::Sig
|
124
|
-
if ident.match?("<@ident extend>") &&
|
125
|
-
args_add_block.match?("<args_add_block <args <const_path_ref <var_ref <@const T>> <@const Sig>>> false>")
|
126
|
-
patterns << ExtendTSigPattern.new(ident.range.begin..args_add_block.range.end)
|
131
|
+
# mixes_in_class_methods(foo)
|
132
|
+
if call.match?("<fcall <@ident mixes_in_class_methods>>") &&
|
133
|
+
arg_paren.match?(/<arg_paren <args_add_block <args <.+>>> false>>/)
|
134
|
+
patterns << MixesInClassMethodsPattern.new(call.range.begin...arg_paren.range.end)
|
127
135
|
end
|
128
136
|
|
129
137
|
super
|
130
138
|
end
|
131
139
|
|
132
140
|
# T.must foo => foo
|
141
|
+
# T.reveal_type foo => foo
|
142
|
+
# T.unsafe foo => foo
|
133
143
|
class TMustNoParensPattern < Pattern
|
134
144
|
def replace(segment)
|
135
|
-
segment.gsub(/(T\s*\.must\s*)(.+)/) do
|
136
|
-
"#{
|
145
|
+
segment.gsub(/(T\s*\.(?:must|reveal_type|unsafe)\s*)(.+)/) do
|
146
|
+
"#{blank($1)}#{$2}"
|
137
147
|
end
|
138
148
|
end
|
139
149
|
end
|
@@ -141,10 +151,12 @@ module Sorbet
|
|
141
151
|
def on_command_call(var_ref, period, ident, args_add_block)
|
142
152
|
if var_ref.match?("<var_ref <@const T>>") && period.match?("<@period .>")
|
143
153
|
# T.must foo
|
144
|
-
|
154
|
+
# T.reveal_type foo
|
155
|
+
# T.unsafe foo
|
156
|
+
if ident.match?(/<@ident (?:must|reveal_type|unsafe)>/) &&
|
145
157
|
args_add_block.match?(/<args_add_block <args <.+>> false>/) &&
|
146
158
|
args_add_block.body[0].body.length == 1
|
147
|
-
patterns << TMustNoParensPattern.new(var_ref.range.begin
|
159
|
+
patterns << TMustNoParensPattern.new(var_ref.range.begin...args_add_block.range.end)
|
148
160
|
end
|
149
161
|
end
|
150
162
|
|
@@ -154,8 +166,8 @@ module Sorbet
|
|
154
166
|
# sig { foo } =>
|
155
167
|
class SigBracesPattern < Pattern
|
156
168
|
def replace(segment)
|
157
|
-
segment.gsub(/(sig\s*\{.+\})(.*)/) do
|
158
|
-
"#{
|
169
|
+
segment.gsub(/(sig\s*\{.+\})(.*)/m) do
|
170
|
+
"#{blank($1)}#{$2}"
|
159
171
|
end
|
160
172
|
end
|
161
173
|
end
|
data/lib/sorbet/eraser.rb
CHANGED
@@ -5,17 +5,10 @@ require "ripper"
|
|
5
5
|
require "sorbet/eraser/parser"
|
6
6
|
require "sorbet/eraser/patterns"
|
7
7
|
require "sorbet/eraser/version"
|
8
|
+
require "t"
|
8
9
|
|
9
10
|
module Sorbet
|
10
11
|
module Eraser
|
11
|
-
# This error gets raise in place of any T.absurd calls.
|
12
|
-
class AbsurdError < StandardError
|
13
|
-
end
|
14
|
-
|
15
|
-
# This class gets put in place of any existing T.type_alias calls.
|
16
|
-
class TypeAlias
|
17
|
-
end
|
18
|
-
|
19
12
|
# Hook the patterns into the parser so that the correct methods get
|
20
13
|
# overridden and will trigger replacements.
|
21
14
|
Parser.prepend(Patterns)
|
data/lib/t/enum.rb
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module T
|
4
|
+
# This is mostly copy-pasted from sorbet-runtime since we have to maintain the
|
5
|
+
# same behavior.
|
6
|
+
class T::Enum
|
7
|
+
## Enum class methods ##
|
8
|
+
|
9
|
+
def self.values
|
10
|
+
if @values.nil?
|
11
|
+
raise "Attempting to access values of #{self.class} before it has been initialized." \
|
12
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
13
|
+
end
|
14
|
+
@values
|
15
|
+
end
|
16
|
+
|
17
|
+
# This exists for compatibility with the interface of `Hash` & mostly to support
|
18
|
+
# the HashEachMethods Rubocop.
|
19
|
+
def self.each_value(&blk)
|
20
|
+
if blk
|
21
|
+
values.each(&blk)
|
22
|
+
else
|
23
|
+
values.each
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Convert from serialized value to enum instance
|
28
|
+
def self.try_deserialize(serialized_val)
|
29
|
+
if @mapping.nil?
|
30
|
+
raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
|
31
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
32
|
+
end
|
33
|
+
@mapping[serialized_val]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Convert from serialized value to enum instance.
|
37
|
+
#
|
38
|
+
# @return [self]
|
39
|
+
# @raise [KeyError] if serialized value does not match any instance.
|
40
|
+
def self.from_serialized(serialized_val)
|
41
|
+
res = try_deserialize(serialized_val)
|
42
|
+
if res.nil?
|
43
|
+
raise KeyError.new("Enum #{self} key not found: #{serialized_val.inspect}")
|
44
|
+
end
|
45
|
+
res
|
46
|
+
end
|
47
|
+
|
48
|
+
# Note: It would have been nice to make this method final before people started overriding it.
|
49
|
+
# @return [Boolean] Does the given serialized value correspond with any of this enum's values.
|
50
|
+
def self.has_serialized?(serialized_val)
|
51
|
+
if @mapping.nil?
|
52
|
+
raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
|
53
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
54
|
+
end
|
55
|
+
@mapping.include?(serialized_val)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.serialize(instance)
|
59
|
+
return nil if instance.nil?
|
60
|
+
|
61
|
+
if self == T::Enum
|
62
|
+
raise "Cannot call T::Enum.serialize directly. You must call on a specific child class."
|
63
|
+
end
|
64
|
+
if instance.class != self
|
65
|
+
raise "Cannot call #serialize on a value that is not an instance of #{self}."
|
66
|
+
end
|
67
|
+
instance.serialize
|
68
|
+
end
|
69
|
+
|
70
|
+
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
71
|
+
def self.deserialize(mongo_value)
|
72
|
+
if self == T::Enum
|
73
|
+
raise "Cannot call T::Enum.deserialize directly. You must call on a specific child class."
|
74
|
+
end
|
75
|
+
self.from_serialized(mongo_value)
|
76
|
+
end
|
77
|
+
|
78
|
+
## Enum instance methods ##
|
79
|
+
|
80
|
+
def dup
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
def clone
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def serialize
|
89
|
+
assert_bound!
|
90
|
+
@serialized_val
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_json(*args)
|
94
|
+
serialize.to_json(*args)
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_s
|
98
|
+
inspect
|
99
|
+
end
|
100
|
+
|
101
|
+
def inspect
|
102
|
+
"#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
|
103
|
+
end
|
104
|
+
|
105
|
+
def <=>(other)
|
106
|
+
case other
|
107
|
+
when self.class
|
108
|
+
self.serialize <=> other.serialize
|
109
|
+
else
|
110
|
+
nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# NB: Do not call this method. This exists to allow for a safe migration path in places where enum
|
115
|
+
# values are compared directly against string values.
|
116
|
+
#
|
117
|
+
# Ruby's string has a weird quirk where `'my_string' == obj` calls obj.==('my_string') if obj
|
118
|
+
# responds to the `to_str` method. It does not actually call `to_str` however.
|
119
|
+
#
|
120
|
+
# See https://ruby-doc.org/core-2.4.0/String.html#method-i-3D-3D
|
121
|
+
def to_str
|
122
|
+
msg = 'Implicit conversion of Enum instances to strings is not allowed. Call #serialize instead.'
|
123
|
+
raise NoMethodError.new(msg)
|
124
|
+
end
|
125
|
+
|
126
|
+
def ==(other)
|
127
|
+
case other
|
128
|
+
when String
|
129
|
+
false
|
130
|
+
else
|
131
|
+
super(other)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def ===(other)
|
136
|
+
case other
|
137
|
+
when String
|
138
|
+
false
|
139
|
+
else
|
140
|
+
super(other)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
## Private implementation ##
|
145
|
+
|
146
|
+
def initialize(serialized_val=nil)
|
147
|
+
raise 'T::Enum is abstract' if self.class == T::Enum
|
148
|
+
if !self.class.started_initializing?
|
149
|
+
raise "Must instantiate all enum values of #{self.class} inside 'enums do'."
|
150
|
+
end
|
151
|
+
if self.class.fully_initialized?
|
152
|
+
raise "Cannot instantiate a new enum value of #{self.class} after it has been initialized."
|
153
|
+
end
|
154
|
+
|
155
|
+
serialized_val = serialized_val.frozen? ? serialized_val : serialized_val.dup.freeze
|
156
|
+
@serialized_val = serialized_val
|
157
|
+
@const_name = nil
|
158
|
+
self.class._register_instance(self)
|
159
|
+
end
|
160
|
+
|
161
|
+
private def assert_bound!
|
162
|
+
if @const_name.nil?
|
163
|
+
raise "Attempting to access Enum value on #{self.class} before it has been initialized." \
|
164
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def _bind_name(const_name)
|
169
|
+
@const_name = const_name
|
170
|
+
@serialized_val = const_to_serialized_val(const_name) if @serialized_val.nil?
|
171
|
+
freeze
|
172
|
+
end
|
173
|
+
|
174
|
+
private def const_to_serialized_val(const_name)
|
175
|
+
# Historical note: We convert to lowercase names because the majority of existing calls to
|
176
|
+
# `make_accessible` were arrays of lowercase strings. Doing this conversion allowed for the
|
177
|
+
# least amount of repetition in migrated declarations.
|
178
|
+
const_name.to_s.downcase.freeze
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.started_initializing?
|
182
|
+
@started_initializing ||= false
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.fully_initialized?
|
186
|
+
@fully_initialized ||= false
|
187
|
+
end
|
188
|
+
|
189
|
+
# Maintains the order in which values are defined
|
190
|
+
def self._register_instance(instance)
|
191
|
+
@values ||= []
|
192
|
+
@values << instance
|
193
|
+
end
|
194
|
+
|
195
|
+
# Entrypoint for allowing people to register new enum values.
|
196
|
+
# All enum values must be defined within this block.
|
197
|
+
def self.enums(&blk)
|
198
|
+
raise "enums cannot be defined for T::Enum" if self == T::Enum
|
199
|
+
raise "Enum #{self} was already initialized" if @fully_initialized
|
200
|
+
raise "Enum #{self} is still initializing" if @started_initializing
|
201
|
+
|
202
|
+
@started_initializing = true
|
203
|
+
|
204
|
+
@values = nil
|
205
|
+
|
206
|
+
yield
|
207
|
+
|
208
|
+
@mapping = nil
|
209
|
+
@mapping = {}
|
210
|
+
|
211
|
+
# Freeze the Enum class and bind the constant names into each of the instances.
|
212
|
+
self.constants(false).each do |const_name|
|
213
|
+
instance = self.const_get(const_name, false)
|
214
|
+
if !instance.is_a?(self)
|
215
|
+
raise "Invalid constant #{self}::#{const_name} on enum. " \
|
216
|
+
"All constants defined for an enum must be instances itself (e.g. `Foo = new`)."
|
217
|
+
end
|
218
|
+
|
219
|
+
instance._bind_name(const_name)
|
220
|
+
serialized = instance.serialize
|
221
|
+
if @mapping.include?(serialized)
|
222
|
+
raise "Enum values must have unique serializations. Value '#{serialized}' is repeated on #{self}."
|
223
|
+
end
|
224
|
+
@mapping[serialized] = instance
|
225
|
+
end
|
226
|
+
@values.freeze
|
227
|
+
@mapping.freeze
|
228
|
+
|
229
|
+
orphaned_instances = T.must(@values) - @mapping.values
|
230
|
+
if !orphaned_instances.empty?
|
231
|
+
raise "Enum values must be assigned to constants: #{orphaned_instances.map {|v| v.instance_variable_get('@serialized_val')}}"
|
232
|
+
end
|
233
|
+
|
234
|
+
@fully_initialized = true
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.inherited(child_class)
|
238
|
+
super
|
239
|
+
|
240
|
+
raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum
|
241
|
+
end
|
242
|
+
|
243
|
+
# Marshal support
|
244
|
+
def _dump(_level)
|
245
|
+
Marshal.dump(serialize)
|
246
|
+
end
|
247
|
+
|
248
|
+
def self._load(args)
|
249
|
+
deserialize(Marshal.load(args))
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
data/lib/t/struct.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module T
|
4
|
+
# This is a shim for the T::Struct class because since you're actually
|
5
|
+
# inheriting from the class there's not really a way to remove it from the
|
6
|
+
# source.
|
7
|
+
class Struct
|
8
|
+
# include T::Props
|
9
|
+
# include T::Props::Serializable
|
10
|
+
# include T::Props::Constructor
|
11
|
+
end
|
12
|
+
end
|
data/lib/t.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "t/enum"
|
4
|
+
require "t/struct"
|
5
|
+
|
6
|
+
# For some constructs, it doesn't make as much sense to entirely remove them
|
7
|
+
# since they're actually used to change runtime behavior. For example, T.absurd
|
8
|
+
# will always raise an error. In this case instead of removing the content, we
|
9
|
+
# can just shim it.
|
10
|
+
module T
|
11
|
+
# These methods should really not be being called in a loop or any other kind
|
12
|
+
# of hot path, so here we're just going to shim them.
|
13
|
+
module Helpers
|
14
|
+
def abstract!; end
|
15
|
+
def interface!; end
|
16
|
+
def final!; end
|
17
|
+
def sealed!; end
|
18
|
+
def mixes_in_class_methods(*); end
|
19
|
+
def requires_ancestor(*); end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Similar to the Helpers module, these things should only be called a couple
|
23
|
+
# of times, so shimming them here.
|
24
|
+
module Generic
|
25
|
+
include Helpers
|
26
|
+
def type_member(*, **); end
|
27
|
+
def type_template(*, **); end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Keeping this module as a thing so that if there's any kind of weird
|
31
|
+
# reflection going on like is_a?(T::Sig) it will still work.
|
32
|
+
module Sig
|
33
|
+
end
|
34
|
+
|
35
|
+
# Type aliases don't actually do anything, but they are usually assigned to
|
36
|
+
# constants, so in that case we need to return something.
|
37
|
+
def self.type_alias
|
38
|
+
Object.new
|
39
|
+
end
|
40
|
+
|
41
|
+
# Absurd always raises a TypeError within Sorbet, so mirroring that behavior
|
42
|
+
# here when T.absurd is called.
|
43
|
+
def self.absurd(value)
|
44
|
+
raise TypeError, value
|
45
|
+
end
|
46
|
+
end
|
data/sorbet-eraser.gemspec
CHANGED
@@ -1,22 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require_relative "lib/sorbet/eraser/version"
|
4
|
+
|
5
|
+
version = Sorbet::Eraser::VERSION
|
6
|
+
repository = "https://github.com/kddnewton/sorbet-eraser"
|
6
7
|
|
7
8
|
Gem::Specification.new do |spec|
|
8
9
|
spec.name = "sorbet-eraser"
|
9
|
-
spec.version =
|
10
|
+
spec.version = version
|
10
11
|
spec.authors = ["Kevin Newton"]
|
11
12
|
spec.email = ["kddnewton@gmail.com"]
|
12
13
|
|
13
14
|
spec.summary = "Erase all traces of sorbet-runtime code."
|
14
|
-
spec.homepage =
|
15
|
+
spec.homepage = repository
|
15
16
|
spec.license = "MIT"
|
16
17
|
|
17
|
-
spec.
|
18
|
+
spec.metadata = {
|
19
|
+
"bug_tracker_uri" => "#{repository}/issues",
|
20
|
+
"changelog_uri" => "#{repository}/blob/v#{version}/CHANGELOG.md",
|
21
|
+
"source_code_uri" => repository,
|
22
|
+
"rubygems_mfa_required" => "true"
|
23
|
+
}
|
24
|
+
|
25
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
18
26
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
27
|
end
|
28
|
+
|
20
29
|
spec.bindir = "exe"
|
21
30
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
31
|
spec.require_paths = ["lib"]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sorbet-eraser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Newton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -55,11 +55,15 @@ dependencies:
|
|
55
55
|
description:
|
56
56
|
email:
|
57
57
|
- kddnewton@gmail.com
|
58
|
-
executables:
|
58
|
+
executables:
|
59
|
+
- sorbet-eraser
|
59
60
|
extensions: []
|
60
61
|
extra_rdoc_files: []
|
61
62
|
files:
|
63
|
+
- ".github/dependabot.yml"
|
64
|
+
- ".github/workflows/main.yml"
|
62
65
|
- ".gitignore"
|
66
|
+
- CHANGELOG.md
|
63
67
|
- Gemfile
|
64
68
|
- Gemfile.lock
|
65
69
|
- LICENSE
|
@@ -67,16 +71,25 @@ files:
|
|
67
71
|
- Rakefile
|
68
72
|
- bin/console
|
69
73
|
- bin/setup
|
74
|
+
- exe/sorbet-eraser
|
70
75
|
- lib/sorbet/eraser.rb
|
71
76
|
- lib/sorbet/eraser/autoload.rb
|
77
|
+
- lib/sorbet/eraser/cli.rb
|
72
78
|
- lib/sorbet/eraser/parser.rb
|
73
79
|
- lib/sorbet/eraser/patterns.rb
|
74
80
|
- lib/sorbet/eraser/version.rb
|
81
|
+
- lib/t.rb
|
82
|
+
- lib/t/enum.rb
|
83
|
+
- lib/t/struct.rb
|
75
84
|
- sorbet-eraser.gemspec
|
76
85
|
homepage: https://github.com/kddnewton/sorbet-eraser
|
77
86
|
licenses:
|
78
87
|
- MIT
|
79
|
-
metadata:
|
88
|
+
metadata:
|
89
|
+
bug_tracker_uri: https://github.com/kddnewton/sorbet-eraser/issues
|
90
|
+
changelog_uri: https://github.com/kddnewton/sorbet-eraser/blob/v0.1.1/CHANGELOG.md
|
91
|
+
source_code_uri: https://github.com/kddnewton/sorbet-eraser
|
92
|
+
rubygems_mfa_required: 'true'
|
80
93
|
post_install_message:
|
81
94
|
rdoc_options: []
|
82
95
|
require_paths:
|