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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 839924a4d5d33c9f00287b6be5151b263ef6b4446b524568ee0bbb212f2a4584
4
- data.tar.gz: 5658154f4303ffa3f547992d679f62efa5f613eb0cf7cb871e51cecb21d7c329
3
+ metadata.gz: 54913e2de72273efae7221b3dc96923c1b45e7d17020ec47bd9a417bea9a62cc
4
+ data.tar.gz: 71f3cc0a28acefd20d9a4a1f3a06081a568c5b8276cb1241f34d2bb3e7f64fb8
5
5
  SHA512:
6
- metadata.gz: ae54ad36caaee0aaf2f70eba22fa6e1e9fe3b00b682d00a2007b50076e211d5e4922474a59820193a673e26f76f3b0f63bc2662021577e07cccc5f9d5a8ec55f
7
- data.tar.gz: 257cb54108d56dbe36bf10316a2191bc594c59bfa4c4eff358b005703b2a01767ca432c558cc6785f5ad0ac4a918eddef3fca3c15b1c3b17941ca085c8f784fd
6
+ metadata.gz: 06ca1f48f68eea0d10933f83ffe8ba4b296cda3c87240a5db6704ac27d9d6a12202a46b1af82ee3983ba335d4254f230a8f3d861578556ee76ea3544c551cd70
7
+ data.tar.gz: 547b88a3fb93e133c69cdd2d05ce684f62cc1650afe836fe9741059d1f567f4704bf11815b3170d8f4df2a9909fcc138fc6d940b45ef5ff615853f93118a2f93
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "bundler"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "daily"
@@ -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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-eraser (0.1.0)
4
+ sorbet-eraser (0.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -19,4 +19,4 @@ DEPENDENCIES
19
19
  sorbet-eraser!
20
20
 
21
21
  BUNDLED WITH
22
- 2.2.24
22
+ 2.2.15
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 'sorbet-eraser'
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 | Status | Replacement |
36
- | --------- | ------ | ----------- |
37
- | `include T::Generic` | 🛠 | |
38
- | `include T::Helpers` | 🛠 | |
39
- | `extend T::Sig` | ✅ | |
40
- | `class Foo < T::Enum` | 🛠 | `class Foo < ::Sorbet::Eraser::Enum` |
41
- | `class Foo < T::Struct` | 🛠 | `class Foo < ::Sorbet::Eraser::Struct` |
42
- | `abstract!` | 🛠 | |
43
- | `final!` | 🛠 | |
44
- | `interface!` | 🛠 | |
45
- | `mixes_in_class_methods(foo)` | 🛠 | `foo` |
46
- | `sig` | ✅ | |
47
- | `T.absurd(foo)` | ✅ | `raise ::Sorbet::Eraser::AbsurdError` |
48
- | `T.assert_type!(foo)` | ✅ | `foo` |
49
- | `T.bind(self, foo)` | ✅ | `self` |
50
- | `T.cast(foo, bar)` | ✅ | `foo` |
51
- | `T.let(foo, bar)` | ✅ | `foo` |
52
- | `T.must(foo)` | ✅ | `foo` |
53
- | `T.must foo` | ✅ | `foo` |
54
- | `T.reveal_type(foo)` | ✅ | `foo` |
55
- | `T.type_alias { foo }` | ✅ | `::Sorbet::Eraser::TypeAlias` |
56
- | `T.unsafe(foo)` | 🛠 | `foo` |
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,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.expand_path("../lib", __dir__))
4
+ require "sorbet/eraser"
5
+ require "sorbet/eraser/cli"
6
+
7
+ Sorbet::Eraser::CLI.start(ARGV)
@@ -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
@@ -35,7 +35,10 @@ module Sorbet
35
35
  end
36
36
 
37
37
  def [](byteindex)
38
- @indices[byteindex]
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
- attr_reader :line_counts, :patterns
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 "Invalid source"
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
- case event
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
- when /_add\z/
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 replace(segment)
26
- segment
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.gsub(/(T\s*\.absurd\(\s*.+\s*\))(.*)/) do
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
- "#{" " * $1.length}#{$2}#{" " * $3.length}#{$4}"
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.gsub(/(T\s*\.(?:assert_type!|bind|cast|let)\(\s*)(.+)(\s*,.+\))(.*)/) do
57
- "#{" " * $1.length}#{$2}#{" " * $3.length}#{$4}"
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
- def on_method_add_arg(call, arg_paren)
63
- # T.absurd(foo)
64
- if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident absurd>>/) &&
65
- arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
66
- patterns << TAbsurdParensPattern.new(call.range.begin..arg_paren.range.end)
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
- if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type)>>/) &&
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..arg_paren.range.end)
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?(/<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>/) &&
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 << TTwoArgMethodCallParensPattern.new(call.range.begin..arg_paren.range.end)
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 << TTwoArgMethodCallParensPattern.new(call.range.begin..arg_paren.range.end)
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
- super
91
- end
92
-
93
- # T.type_alias { foo } => ::Sorbet::Eraser::TypeAlias
94
- class TTypeAliasBraceBlockPattern < Pattern
95
- def replace(segment)
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
- super
111
- end
112
-
113
- # extend T::Sig =>
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
- "#{" " * $1.length}#{$2}"
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
- if ident.match?("<@ident must>") &&
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..args_add_block.range.end)
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
- "#{" " * $1.length}#{$2}"
169
+ segment.gsub(/(sig\s*\{.+\})(.*)/m) do
170
+ "#{blank($1)}#{$2}"
159
171
  end
160
172
  end
161
173
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sorbet
4
4
  module Eraser
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
7
7
  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
@@ -1,22 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path("../lib", __FILE__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require "sorbet/eraser/version"
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 = Sorbet::Eraser::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 = "https://github.com/kddnewton/sorbet-eraser"
15
+ spec.homepage = repository
15
16
  spec.license = "MIT"
16
17
 
17
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
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.0
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-08-17 00:00:00.000000000 Z
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: