sorbet-eraser 0.1.0 → 0.2.0

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: efd5822039bf577e0c4a37171d7e91abd9c7bed250c9d09c779f52faa0d84cf6
4
+ data.tar.gz: 1a88eab81b0932ab400f5aeaf08e43cb5107fc08c135e232cc444205f76fdcf3
5
5
  SHA512:
6
- metadata.gz: ae54ad36caaee0aaf2f70eba22fa6e1e9fe3b00b682d00a2007b50076e211d5e4922474a59820193a673e26f76f3b0f63bc2662021577e07cccc5f9d5a8ec55f
7
- data.tar.gz: 257cb54108d56dbe36bf10316a2191bc594c59bfa4c4eff358b005703b2a01767ca432c558cc6785f5ad0ac4a918eddef3fca3c15b1c3b17941ca085c8f784fd
6
+ metadata.gz: a64889bcbdf6affe397a394ed3a0841a166ff376e806e050967d4676910670a5f75d774e25c2490fae410c2b5e9263f07c07597ae6c0a0c08ae226343032ed0f
7
+ data.tar.gz: 50b223fde3f5901535888d6e06f685ba128c6bda051d516fe37f8b825ce9c328bfddaff54bb19a52aa1694309be11a74732ceb0b582e00179cf939d8eba760ba
@@ -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.1'
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,16 +1,19 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-eraser (0.1.0)
4
+ sorbet-eraser (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- minitest (5.14.4)
9
+ minitest (5.18.1)
10
10
  rake (13.0.6)
11
11
 
12
12
  PLATFORMS
13
+ arm64-darwin-22
13
14
  x86_64-darwin-19
15
+ x86_64-darwin-21
16
+ x86_64-linux
14
17
 
15
18
  DEPENDENCIES
16
19
  bundler
@@ -19,4 +22,4 @@ DEPENDENCIES
19
22
  sorbet-eraser!
20
23
 
21
24
  BUNDLED WITH
22
- 2.2.24
25
+ 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:
@@ -24,36 +27,56 @@ Or install it yourself as:
24
27
 
25
28
  ## Usage
26
29
 
27
- 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.
30
+ There are two ways to use this gem, depending on your needs.
31
+
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.
33
+
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:
35
+
36
+ ```bash
37
+ bundle exec sorbet-eraser '**/*.rb'
38
+ ```
28
39
 
29
- 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.
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.
30
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::InexactStruct` | 🛠 | Shimmed |
54
+ | `class Foo < T::Struct` | 🛠 | Shimmed |
55
+ | `class Foo < T::ImmutableStruct` | 🛠 | Shimmed |
56
+ | `include T::Props` | 🛠 | Shimmed |
57
+ | `include T::Props::Serializable` | 🛠 | Shimmed |
58
+ | `include T::Props::Constructor` | 🛠 | Shimmed |
59
+ | `sig` | ✅ | Removed |
60
+ | `T.absurd(foo)` | ✅ | Shimmed |
61
+ | `T.assert_type!(foo, bar)` | ✅ | `foo` |
62
+ | `T.bind(self, foo)` | ✅ | `self` |
63
+ | `T.cast(foo, bar)` | ✅ | `foo` |
64
+ | `T.let(foo, bar)` | ✅ | `foo` |
65
+ | `T.must(foo)` | ✅ | `foo` |
66
+ | `T.reveal_type(foo)` | ✅ | `foo` |
67
+ | `T.type_alias { foo }` | | Shimmed |
68
+ | `T.unsafe(foo)` | ✅ | `foo` |
69
+
70
+ In the above table, for `Status`:
71
+
72
+ * ✅ means that we are confident this is replaced 1:1.
73
+ * 🛠 means there may be APIs that are not entirely supported. If you run into something that is missing, please open an issue.
74
+
75
+ In the above table, for `Replacement`:
76
+
77
+ * `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.
78
+ * `Removed` means that the construct is removed entirely from the source.
79
+ * Anything else means that the inputted code is replaced with that output.
57
80
 
58
81
  ## Development
59
82
 
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,98 +52,133 @@ 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
+ # T.must foo => foo
93
+ # T.reveal_type foo => foo
94
+ # T.unsafe foo => foo
95
+ class TMustNoParensPattern < Pattern
96
+ def replace(segment)
97
+ segment.gsub(/(T\s*\.(?:must|reveal_type|unsafe)\s*)(.+)/) do
98
+ "#{blank($1)}#{$2}"
99
+ end
100
+ end
101
+ end
102
+
103
+ def on_method_add_arg(call, arg_paren)
69
104
  # T.must(foo)
70
105
  # T.reveal_type(foo)
71
- if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type)>>/) &&
106
+ # T.unsafe(foo)
107
+ if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type|unsafe)>>/) &&
72
108
  arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
73
- patterns << TOneArgMethodCallParensPattern.new(call.range.begin..arg_paren.range.end)
109
+ patterns << TOneArgMethodCallParensPattern.new(call.range.begin...arg_paren.range.end)
74
110
  end
75
111
 
76
112
  # T.assert_type!(foo, bar)
77
113
  # T.cast(foo, bar)
78
114
  # T.let(foo, bar)
79
- if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>/) &&
115
+ if call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>\z/) &&
80
116
  arg_paren.match?(/<arg_paren <args_add_block <args .+> false>>/)
81
- patterns << TTwoArgMethodCallParensPattern.new(call.range.begin..arg_paren.range.end)
117
+ patterns <<
118
+ TTwoArgMethodCallParensPattern.new(
119
+ call.range.begin...arg_paren.range.end,
120
+ comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
121
+ )
82
122
  end
83
123
 
84
124
  # T.bind(self, foo)
85
125
  if call.match?(/<call <var_ref <@const T>> <@period \.> <@ident bind>>/) &&
86
126
  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)
127
+ patterns <<
128
+ TTwoArgMethodCallParensPattern.new(
129
+ call.range.begin...arg_paren.range.end,
130
+ comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
131
+ )
88
132
  end
89
133
 
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
134
+ # abstract!
135
+ # final!
136
+ # interface!
137
+ if call.match?(/<fcall <@ident (?:abstract|final|interface)!>>/) &&
138
+ arg_paren.match?("<args >")
139
+ patterns << DeclarationPattern.new(call.range.begin...arg_paren.range.end)
100
140
  end
101
- end
102
141
 
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)
142
+ # mixes_in_class_methods(foo)
143
+ if call.match?("<fcall <@ident mixes_in_class_methods>>") &&
144
+ arg_paren.match?(/<arg_paren <args_add_block <args <.+>>> false>>/)
145
+ patterns << MixesInClassMethodsPattern.new(call.range.begin...arg_paren.range.end)
108
146
  end
109
147
 
110
148
  super
111
149
  end
112
150
 
113
- # extend T::Sig =>
114
- class ExtendTSigPattern < Pattern
151
+ # prop :foo, String
152
+ # const :foo, String
153
+ class PropPattern < Pattern
115
154
  def replace(segment)
116
- segment.gsub(/(extend\s+T::Sig)(.*)/) do
117
- "#{" " * $1.length}#{$2}"
155
+ segment.gsub(/((?:prop|const)\s+:.+),(\s*)([^\n;,]+)(.*)/m) do
156
+ "#{$1} #{$2}#{blank($3)}#{$4}"
118
157
  end
119
158
  end
120
159
  end
121
160
 
122
161
  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)
162
+ if ident.match?(/<@ident (?:const|prop)>/)
163
+ # prop :foo, String
164
+ # const :foo, String
165
+ if args_add_block.match?(/<args_add_block <args <symbol_literal <symbol <@ident .+>>> <.+> false>/)
166
+ patterns << PropPattern.new(ident.range.begin...args_add_block.range.end)
167
+ end
127
168
  end
128
169
 
129
170
  super
130
171
  end
131
172
 
132
- # T.must foo => foo
133
- class TMustNoParensPattern < Pattern
134
- def replace(segment)
135
- segment.gsub(/(T\s*\.must\s*)(.+)/) do
136
- "#{" " * $1.length}#{$2}"
137
- end
138
- end
139
- end
140
-
141
173
  def on_command_call(var_ref, period, ident, args_add_block)
142
174
  if var_ref.match?("<var_ref <@const T>>") && period.match?("<@period .>")
143
175
  # T.must foo
144
- if ident.match?("<@ident must>") &&
176
+ # T.reveal_type foo
177
+ # T.unsafe foo
178
+ if ident.match?(/<@ident (?:must|reveal_type|unsafe)>/) &&
145
179
  args_add_block.match?(/<args_add_block <args <.+>> false>/) &&
146
180
  args_add_block.body[0].body.length == 1
147
- patterns << TMustNoParensPattern.new(var_ref.range.begin..args_add_block.range.end)
181
+ patterns << TMustNoParensPattern.new(var_ref.range.begin...args_add_block.range.end)
148
182
  end
149
183
  end
150
184
 
@@ -154,8 +188,8 @@ module Sorbet
154
188
  # sig { foo } =>
155
189
  class SigBracesPattern < Pattern
156
190
  def replace(segment)
157
- segment.gsub(/(sig\s*\{.+\})(.*)/) do
158
- "#{" " * $1.length}#{$2}"
191
+ segment.gsub(/(sig\s*\{.+\})(.*)/m) do
192
+ "#{blank($1)}#{$2}"
159
193
  end
160
194
  end
161
195
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sorbet
4
4
  module Eraser
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
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 = @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/props.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module T
4
+ # Here is the place we don't match up to sorbet because we simply don't
5
+ # implement as much as they do in the runtime. If there are pieces of this
6
+ # that folks would like implemented I'd be happy to include them. This is here
7
+ # just to get a baseline for folks using T::Struct with basic const/prop
8
+ # calls.
9
+ module Props
10
+ def self.included(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ # Here we're implementing a very basic version of the prop/const methods
15
+ # that are in sorbet-runtime. These are only here to allow consumers to call
16
+ # them and not raise errors and for bookkeeping.
17
+ module ClassMethods
18
+ def props
19
+ @props ||= []
20
+ end
21
+
22
+ def prop(name, rules = {})
23
+ create_prop(name)
24
+ attr_accessor name
25
+ end
26
+
27
+ def const(name, rules = {})
28
+ create_prop(name)
29
+ attr_reader name
30
+ end
31
+
32
+ private
33
+
34
+ def create_prop(name)
35
+ props << name
36
+ props.sort!
37
+ end
38
+ end
39
+
40
+ # Here we're going to check against the props that have been defined on the
41
+ # class level and set appropriate values.
42
+ def initialize(hash = {})
43
+ if self.class.props == hash.keys.sort
44
+ hash.each { |key, value| instance_variable_set("@#{key}", value) }
45
+ else
46
+ raise ArgumentError, "Expected keys #{self.class.props} but got #{hash.keys.sort}"
47
+ end
48
+ end
49
+
50
+ # This module is entirely empty because we haven't implemented anything from
51
+ # sorbet-runtime here.
52
+ module Serializable
53
+ end
54
+
55
+ # This is empty for the same reason.
56
+ module Constructor
57
+ end
58
+ end
59
+ end
data/lib/t/struct.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module T
4
+ # This is the actual parent class of T::Struct. It's here to match up the
5
+ # inheritance chain in case someone is doing reflection.
6
+ class InexactStruct
7
+ include Props
8
+ include Props::Serializable
9
+ include Props::Constructor
10
+ end
11
+
12
+ # This is a shim for the T::Struct class because since you're actually
13
+ # inheriting from the class there's not really a way to remove it from the
14
+ # source.
15
+ class Struct < InexactStruct
16
+ def self.inherited(child)
17
+ super(child)
18
+
19
+ child.define_singleton_method(:inherited) do |grandchild|
20
+ super(grandchild)
21
+ raise "#{grandchild.name} is a subclass of T::Struct and cannot be subclassed"
22
+ end
23
+ end
24
+ end
25
+
26
+ class ImmutableStruct < InexactStruct
27
+ def self.inherited(child)
28
+ super(child)
29
+
30
+ child.define_singleton_method(:inherited) do |grandchild|
31
+ super(grandchild)
32
+ raise "#{grandchild.name} is a subclass of T::ImmutableStruct and cannot be subclassed"
33
+ end
34
+ end
35
+
36
+ def initialize(hash = {})
37
+ super
38
+ freeze
39
+ end
40
+
41
+ # Matches the signature in Props, but raises since this is an immutable struct and only const is allowed
42
+ def self.prop(name, rules = {})
43
+ return super if rules[:immutable]
44
+
45
+ raise "Cannot use `prop` in #{self.name} because it is an immutable struct. Use `const` instead"
46
+ end
47
+
48
+ def with(changed_props)
49
+ raise "Cannot use `with` in #{self.class.name} because it is an immutable struct"
50
+ end
51
+ end
52
+ end
data/lib/t.rb ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "t/enum"
4
+ require "t/props"
5
+ require "t/struct"
6
+
7
+ # For some constructs, it doesn't make as much sense to entirely remove them
8
+ # since they're actually used to change runtime behavior. For example, T.absurd
9
+ # will always raise an error. In this case instead of removing the content, we
10
+ # can just shim it.
11
+ module T
12
+ # These methods should really not be being called in a loop or any other kind
13
+ # of hot path, so here we're just going to shim them.
14
+ module Helpers
15
+ def abstract!; end
16
+ def interface!; end
17
+ def final!; end
18
+ def sealed!; end
19
+ def mixes_in_class_methods(*); end
20
+ def requires_ancestor(*); end
21
+ end
22
+
23
+ # Similar to the Helpers module, these things should only be called a couple
24
+ # of times, so shimming them here.
25
+ module Generic
26
+ include Helpers
27
+ def type_member(*, **); end
28
+ def type_template(*, **); end
29
+ end
30
+
31
+ # Keeping this module as a thing so that if there's any kind of weird
32
+ # reflection going on like is_a?(T::Sig) it will still work.
33
+ module Sig
34
+ end
35
+
36
+ # Type aliases don't actually do anything, but they are usually assigned to
37
+ # constants, so in that case we need to return something.
38
+ def self.type_alias
39
+ Object.new
40
+ end
41
+
42
+ # Absurd always raises a TypeError within Sorbet, so mirroring that behavior
43
+ # here when T.absurd is called.
44
+ def self.absurd(value)
45
+ raise TypeError, value
46
+ end
47
+ 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.2.0
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: 2023-06-27 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,26 @@ 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/props.rb
84
+ - lib/t/struct.rb
75
85
  - sorbet-eraser.gemspec
76
86
  homepage: https://github.com/kddnewton/sorbet-eraser
77
87
  licenses:
78
88
  - MIT
79
- metadata: {}
89
+ metadata:
90
+ bug_tracker_uri: https://github.com/kddnewton/sorbet-eraser/issues
91
+ changelog_uri: https://github.com/kddnewton/sorbet-eraser/blob/v0.2.0/CHANGELOG.md
92
+ source_code_uri: https://github.com/kddnewton/sorbet-eraser
93
+ rubygems_mfa_required: 'true'
80
94
  post_install_message:
81
95
  rdoc_options: []
82
96
  require_paths:
@@ -92,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
106
  - !ruby/object:Gem::Version
93
107
  version: '0'
94
108
  requirements: []
95
- rubygems_version: 3.2.3
109
+ rubygems_version: 3.4.1
96
110
  signing_key:
97
111
  specification_version: 4
98
112
  summary: Erase all traces of sorbet-runtime code.