sorbet-eraser 0.4.0 → 0.6.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: 3859ee512cb01ed5b84df15af3dedaa629c30d122e80a7436821841f87184562
4
- data.tar.gz: 8392610b8fb03ea78d2572b684c2ccf9b4806f082043e3b2614550a370eeea13
3
+ metadata.gz: fb18fb992ca140f69b3971850f1aa3da26091b6d01ba133656c13d6c2427a80a
4
+ data.tar.gz: 7e00f8f81e9b90b23bec44a9e943322eddbf51d75f5ae6491787a7674ce3ca6f
5
5
  SHA512:
6
- metadata.gz: 02ee32fc8bcf3d1951ddd9577a4f46c181a01f196eb6c2af34bb8bb0b68a0ebc3fb28515d0ca1e391d2d1ee305d3012aafd943918df643dd2a3a44c5272a38b2
7
- data.tar.gz: fa7b5e80e3fda49d50df015532110f6b4050450c5348f4d9f0ac6f40c8e2b411882a5348b38e305419b74e0bf6e833b887997bb6757e440fb3acc552e286d7e4
6
+ metadata.gz: c5309fc61086dcd2371ab5784847ffa4695ed99fffe18ee25e74b568bf75e75c6ba8d79baedfd3403471fc11008242ccbc453a39517d1b1ff9863d23240f7e6d
7
+ data.tar.gz: b8599cb21d63143a9ed9ff27df14849c40422d8425b07552b874a69efee908168f01b6862ac05033551b992f998fd934cdd24c5bd0f177094ca63bbb6dafe92d
@@ -0,0 +1,22 @@
1
+ name: Dependabot auto-merge
2
+ on: pull_request
3
+
4
+ permissions:
5
+ contents: write
6
+ pull-requests: write
7
+
8
+ jobs:
9
+ dependabot:
10
+ runs-on: ubuntu-latest
11
+ if: ${{ github.actor == 'dependabot[bot]' }}
12
+ steps:
13
+ - name: Dependabot metadata
14
+ id: metadata
15
+ uses: dependabot/fetch-metadata@v1.3.6
16
+ with:
17
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
18
+ - name: Enable auto-merge for Dependabot PRs
19
+ run: gh pr merge --auto --merge "$PR_URL"
20
+ env:
21
+ PR_URL: ${{github.event.pull_request.html_url}}
22
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
@@ -1,32 +1,24 @@
1
1
  name: Main
2
+
2
3
  on:
3
4
  - push
4
- - pull_request_target
5
+ - pull_request
6
+
5
7
  jobs:
6
8
  ci:
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ ruby:
13
+ - '3.1'
14
+ - '3.2'
7
15
  name: CI
8
16
  runs-on: ubuntu-latest
9
- env:
10
- CI: true
11
17
  steps:
12
18
  - uses: actions/checkout@master
13
19
  - uses: ruby/setup-ruby@v1
14
20
  with:
15
- ruby-version: '3.1'
21
+ ruby-version: ${{ matrix.ruby }}
16
22
  bundler-cache: true
17
23
  - name: Test
18
24
  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/.gitignore CHANGED
@@ -6,3 +6,5 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+
10
+ test.rb
data/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.0] - 2023-09-11
10
+
11
+ ### Added
12
+
13
+ - Swapped out `ripper` for `YARP`.
14
+
15
+ ## [0.5.0] - 2023-07-13
16
+
17
+ ### Added
18
+
19
+ - Replace `typed: strict` comments with empty comments.
20
+ - Replace all `typed:` sigil comments with empty comments instead of `typed: ignore`. Do this because `typed: ignore` longer than other options, which can cause issues with byte ranges, and violates an assumption by this gem that it is only erasing, not adding content.
21
+ - Add a `--verify` option to the CLI to ensure output is valid Ruby.
22
+ - Enhance `sorbet/eraser/autoload` to hook into `load_iseq` even if bootsnap is not present.
23
+
9
24
  ## [0.4.0] - 2023-07-03
10
25
 
11
26
  ### Added
@@ -41,7 +56,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
41
56
 
42
57
  - Require MFA for releasing.
43
58
 
44
- [unreleased]: https://github.com/kddnewton/sorbet-eraser/compare/v0.4.0...HEAD
59
+ [unreleased]: https://github.com/kddnewton/sorbet-eraser/compare/v0.6.0...HEAD
60
+ [0.6.0]: https://github.com/kddnewton/sorbet-eraser/compare/v0.5.0...v0.6.0
61
+ [0.5.0]: https://github.com/kddnewton/sorbet-eraser/compare/v0.4.0...v0.5.0
45
62
  [0.4.0]: https://github.com/kddnewton/sorbet-eraser/compare/v0.3.1...v0.4.0
46
63
  [0.3.1]: https://github.com/kddnewton/sorbet-eraser/compare/v0.3.0...v0.3.1
47
64
  [0.3.0]: https://github.com/kddnewton/sorbet-eraser/compare/v0.2.0...v0.3.0
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-eraser (0.4.0)
4
+ sorbet-eraser (0.6.0)
5
+ yarp
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
- minitest (5.18.1)
10
+ minitest (5.20.0)
10
11
  rake (13.0.6)
12
+ yarp (0.11.0)
11
13
 
12
14
  PLATFORMS
13
15
  arm64-darwin-21
data/README.md CHANGED
@@ -27,19 +27,19 @@ end
27
27
  will be transformed into
28
28
 
29
29
  ```ruby
30
-
30
+ #
31
31
 
32
32
  class HelloWorld
33
-
33
+ extend T::Sig
34
34
 
35
35
 
36
36
  def hello
37
- "World!"
37
+ ("World!" )
38
38
  end
39
39
  end
40
40
  ```
41
41
 
42
- Notice that the `extend T::Sig` and `sig` constructs have been removed from your source code. Notice also that all line and column information has been preserved 1:1, so that stack traces and tracepoints will still be accurate.
42
+ The `sig` method calls have been removed from your source code. `T::Sig` has been left in place, but is shimmed with an empty module to ensure any reflection is consistent. All line and column information has been preserved 1:1, so that stack traces and tracepoints will still be accurate.
43
43
 
44
44
  ## Installation
45
45
 
@@ -95,30 +95,30 @@ If you used any runtime structures like `T::Struct` or `T::Enum` you'll need a r
95
95
 
96
96
  Below is a table of the status of each `sorbet-runtime` construct and its current support status.
97
97
 
98
- | Construct | Status | Replacement |
99
- | --------------------------------------------------- | ------ | ----------------- |
100
- | `# typed: foo` | ✅ | `# typed: ignore` |
101
- | `extend T::*` | ✅ | Shimmed |
102
- | `abstract!`, `final!`, `interface!`, `sealed!` | ✅ | Shimmed |
103
- | `mixes_in_class_methods(*)`, `requires_ancestor(*)` | ✅ | Shimmed |
104
- | `type_member(*)`, `type_template(*)` | ✅ | Shimmed |
105
- | `class Foo < T::Enum` | ✅ | Shimmed |
106
- | `class Foo < T::InexactStruct` | 🛠 | Shimmed |
107
- | `class Foo < T::Struct` | 🛠 | Shimmed |
108
- | `class Foo < T::ImmutableStruct` | 🛠 | Shimmed |
109
- | `include T::Props` | 🛠 | Shimmed |
110
- | `include T::Props::Serializable` | 🛠 | Shimmed |
111
- | `include T::Props::Constructor` | 🛠 | Shimmed |
112
- | `sig` | ✅ | Removed |
113
- | `T.absurd(foo)` | ✅ | Shimmed |
114
- | `T.assert_type!(foo, bar)` | ✅ | `foo` |
115
- | `T.bind(self, foo)` | ✅ | `self` |
116
- | `T.cast(foo, bar)` | ✅ | `foo` |
117
- | `T.let(foo, bar)` | ✅ | `foo` |
118
- | `T.must(foo)` | ✅ | `foo` |
119
- | `T.reveal_type(foo)` | ✅ | `foo` |
120
- | `T.type_alias { foo }` | ✅ | Shimmed |
121
- | `T.unsafe(foo)` | ✅ | `foo` |
98
+ | Construct | Status | Replacement |
99
+ | --------------------------------------------------- | ------ | ----------- |
100
+ | `# typed: foo` | ✅ | `#` |
101
+ | `extend T::*` | ✅ | Shimmed |
102
+ | `abstract!`, `final!`, `interface!`, `sealed!` | ✅ | Shimmed |
103
+ | `mixes_in_class_methods(*)`, `requires_ancestor(*)` | ✅ | Shimmed |
104
+ | `type_member(*)`, `type_template(*)` | ✅ | Shimmed |
105
+ | `class Foo < T::Enum` | ✅ | Shimmed |
106
+ | `class Foo < T::InexactStruct` | 🛠 | Shimmed |
107
+ | `class Foo < T::Struct` | 🛠 | Shimmed |
108
+ | `class Foo < T::ImmutableStruct` | 🛠 | Shimmed |
109
+ | `include T::Props` | 🛠 | Shimmed |
110
+ | `include T::Props::Serializable` | 🛠 | Shimmed |
111
+ | `include T::Props::Constructor` | 🛠 | Shimmed |
112
+ | `sig` | ✅ | Removed |
113
+ | `T.absurd(foo)` | ✅ | Shimmed |
114
+ | `T.assert_type!(foo, bar)` | ✅ | `foo` |
115
+ | `T.bind(self, foo)` | ✅ | `self` |
116
+ | `T.cast(foo, bar)` | ✅ | `foo` |
117
+ | `T.let(foo, bar)` | ✅ | `foo` |
118
+ | `T.must(foo)` | ✅ | `foo` |
119
+ | `T.reveal_type(foo)` | ✅ | `foo` |
120
+ | `T.type_alias { foo }` | ✅ | Shimmed |
121
+ | `T.unsafe(foo)` | ✅ | `foo` |
122
122
 
123
123
  In the above table, for `Status`:
124
124
 
data/exe/sorbet-eraser CHANGED
@@ -4,4 +4,11 @@ $:.unshift(File.expand_path("../lib", __dir__))
4
4
  require "sorbet/eraser"
5
5
  require "sorbet/eraser/cli"
6
6
 
7
- Sorbet::Eraser::CLI.start(ARGV)
7
+ if ENV["BENCHMARK"]
8
+ require "benchmark"
9
+ Benchmark.bm do |x|
10
+ x.report { Sorbet::Eraser::CLI.start(ARGV) }
11
+ end
12
+ else
13
+ Sorbet::Eraser::CLI.start(ARGV)
14
+ end
@@ -2,21 +2,21 @@
2
2
 
3
3
  require "sorbet/eraser"
4
4
 
5
- # Hook into bootsnap so that before the source is compiled through
6
- # RubyVM::InstructionSequence it gets erased through the eraser.
7
- if RubyVM::InstructionSequence.method_defined?(:load_iseq)
8
- load_iseq, = RubyVM::InstructionSequence.method(:load_iseq).source_location
9
-
10
- if load_iseq.include?("/bootsnap/")
11
- module Sorbet::Eraser::Patch
12
- def input_to_storage(contents, filepath)
13
- erased = Sorbet::Eraser.erase(contents)
14
- RubyVM::InstructionSequence.compile(erased, filepath, filepath).to_binary
15
- rescue SyntaxError
16
- raise ::Bootsnap::CompileCache::Uncompilable, "syntax error"
17
- end
5
+ if RubyVM::InstructionSequence.method_defined?(:load_iseq) &&
6
+ RubyVM::InstructionSequence.method(:load_iseq).source_location[0].include?("/bootsnap/")
7
+ # If the load_iseq method is defined by bootsnap, then we need to override it.
8
+ module Sorbet::Eraser::Patch
9
+ def input_to_storage(contents, filepath)
10
+ super(Sorbet::Eraser.erase(contents), filepath)
18
11
  end
12
+ end
19
13
 
20
- Bootsnap::CompileCache::ISeq.singleton_class.prepend(Sorbet::Eraser::Patch)
14
+ Bootsnap::CompileCache::ISeq.singleton_class.prepend(Sorbet::Eraser::Patch)
15
+ else
16
+ # Otherwise if the method isn't defined by bootsnap, then we'll define it
17
+ # ourselves.
18
+ def (RubyVM::InstructionSequence).load_iseq(filepath)
19
+ erased = Sorbet::Eraser.erase_filepath(filepath)
20
+ RubyVM::InstructionSequence.compile(erased, filepath, filepath)
21
21
  end
22
22
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ # This is a polyfill for the String#bytesplice method that didn't exist before
5
+ # Ruby 3.2.0, and didn't return the receiver until 3.2.1.
6
+ def bytesplice(range, value)
7
+ previous_encoding = encoding
8
+
9
+ begin
10
+ force_encoding(Encoding::ASCII_8BIT)
11
+ self[range] = value
12
+ ensure
13
+ force_encoding(previous_encoding)
14
+ end
15
+
16
+ self
17
+ end
18
+ end
@@ -7,9 +7,10 @@ module Sorbet
7
7
  class CLI
8
8
  POOL_SIZE = 4
9
9
 
10
- attr_reader :filepaths
10
+ attr_reader :verify, :filepaths
11
11
 
12
- def initialize(filepaths)
12
+ def initialize(verify, filepaths)
13
+ @verify = verify
13
14
  @filepaths = filepaths
14
15
  end
15
16
 
@@ -28,22 +29,36 @@ module Sorbet
28
29
  break if filepath == :eoq
29
30
  process(filepath)
30
31
  end
31
- end
32
+ end.tap { |thread| thread.abort_on_exception = true }
32
33
  end
33
34
 
34
35
  workers.each(&:join)
35
36
  end
36
37
 
37
38
  def self.start(argv)
38
- new(argv.flat_map { |pattern| Dir.glob(pattern) }).start
39
+ verify = false
40
+
41
+ if argv.first == "--verify"
42
+ verify = true
43
+ argv.shift
44
+ end
45
+
46
+ filepaths = []
47
+ argv.each { |pattern| filepaths.concat(Dir.glob(pattern)) }
48
+
49
+ new(verify, filepaths).start
39
50
  end
40
51
 
41
52
  private
42
53
 
43
54
  def process(filepath)
44
- File.write(filepath, Eraser.erase(File.read(filepath)))
45
- rescue Parser::ParsingError => error
46
- warn("Could not parse #{filepath}: #{error}")
55
+ contents = Eraser.erase_file(filepath)
56
+
57
+ if verify && Ripper.sexp_raw(contents).nil?
58
+ warn("Internal error while parsing #{filepath}")
59
+ else
60
+ File.write(filepath, contents)
61
+ end
47
62
  rescue => error
48
63
  warn("Could not parse #{filepath}: #{error}")
49
64
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sorbet
4
4
  module Eraser
5
- VERSION = "0.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/sorbet/eraser.rb CHANGED
@@ -1,23 +1,169 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ripper"
3
+ require "yarp"
4
4
 
5
- require "sorbet/eraser/parser"
6
- require "sorbet/eraser/patterns"
7
5
  require "sorbet/eraser/version"
8
- require "t"
6
+ require "sorbet/eraser/t"
7
+
8
+ # Check if String#bytesplice is supported, and otherwise define it. If it is
9
+ # already defined but doesn't return the receiver, override it.
10
+ if !("".respond_to?(:bytesplice))
11
+ require "sorbet/eraser/bytesplice"
12
+ elsif (+"aa").bytesplice(0, 1, "z") != "za"
13
+ class String
14
+ undef bytesplice
15
+ end
16
+
17
+ require "sorbet/eraser/bytesplice"
18
+ end
9
19
 
10
20
  module Sorbet
21
+ # This module contains the logic for erasing Sorbet type annotations from
22
+ # Ruby source code.
11
23
  module Eraser
12
- # Hook the patterns into the parser so that the correct methods get
13
- # overridden and will trigger replacements.
14
- Parser.prepend(Patterns)
15
-
16
- # The entrypoint method to this overall module. This should be called with a
17
- # string that represents Ruby source, and it will return the modified Ruby
18
- # source.
19
- def self.erase(source)
20
- Parser.erase(source)
24
+ # This class is a YARP visitor that finds the ranges of bytes that should be
25
+ # erased from the source code.
26
+ class Ranges < YARP::Visitor
27
+ attr_reader :ranges
28
+
29
+ def initialize(ranges)
30
+ @ranges = ranges
31
+ end
32
+
33
+ def visit_call_node(node)
34
+ case node.name
35
+ when "abstract!", "final!", "interface!"
36
+ # abstract!
37
+ # abstract!()
38
+ # final!
39
+ # final!()
40
+ # interface!
41
+ # interface!()
42
+ if !node.receiver && !node.arguments && !node.block
43
+ ranges << (node.location.start_offset...node.location.end_offset)
44
+ end
45
+ when "assert_type!", "bind", "cast", "let"
46
+ # T.assert_type! foo, String
47
+ # T.assert_type!(foo, String)
48
+ # T.bind self, String
49
+ # T.bind(self, String)
50
+ # T.cast foo, String
51
+ # T.cast(foo, String)
52
+ # T.let foo, String
53
+ # T.let(foo, String)
54
+ if node.receiver.is_a?(YARP::ConstantReadNode) && node.receiver.name == :T && node.arguments && !node.block && node.arguments.arguments.length == 2
55
+ arguments = node.arguments.arguments
56
+
57
+ if node.opening_loc
58
+ ranges << (node.location.start_offset...node.opening_loc.start_offset)
59
+ ranges << (arguments.first.location.end_offset...node.closing_loc.start_offset)
60
+ else
61
+ ranges << (node.location.start_offset...arguments.first.location.start_offset)
62
+ ranges << (arguments.last.location.end_offset...node.location.end_offset)
63
+ end
64
+ end
65
+ when "const", "prop"
66
+ # const :foo, String
67
+ # const :foo, String, required: true
68
+ # const(:foo, String)
69
+ # const(:foo, String, required: true)
70
+ # prop :foo, String
71
+ # prop :foo, String, required: true
72
+ # prop(:foo, String)
73
+ # prop(:foo, String, required: true)
74
+ if !node.receiver && node.arguments && !node.block
75
+ arguments = node.arguments.arguments
76
+
77
+ case arguments.length
78
+ when 2
79
+ ranges << (arguments[0].location.end_offset...arguments[1].location.end_offset)
80
+ when 3
81
+ ranges << (arguments[1].location.start_offset...arguments[2].location.start_offset)
82
+ end
83
+ end
84
+ when "mixes_in_class_methods"
85
+ # mixes_in_class_methods Foo
86
+ # mixes_in_class_methods(Foo)
87
+ if !node.receiver && node.arguments && !node.block && node.arguments.arguments.length == 1
88
+ ranges << (node.location.start_offset...node.location.end_offset)
89
+ end
90
+ when "must", "reveal_type", "unsafe"
91
+ # T.must foo
92
+ # T.must(foo)
93
+ # T.reveal_type foo
94
+ # T.reveal_type(foo)
95
+ # T.unsafe foo
96
+ # T.unsafe(foo)
97
+ if node.receiver.is_a?(YARP::ConstantReadNode) && node.receiver.name == :T && node.arguments && !node.block && node.arguments.arguments.length == 1
98
+ argument = node.arguments.arguments.first
99
+
100
+ if node.opening_loc
101
+ ranges << (node.location.start_offset...node.opening_loc.start_offset)
102
+ ranges << (argument.location.end_offset...node.closing_loc.start_offset)
103
+ else
104
+ ranges << (node.location.start_offset...argument.location.start_offset)
105
+ ranges << (argument.location.end_offset...node.location.end_offset)
106
+ end
107
+ end
108
+ when "sig"
109
+ # sig { ... }
110
+ # sig do ... end
111
+ if !node.receiver && !node.arguments && node.block
112
+ ranges << (node.location.start_offset...node.location.end_offset)
113
+ end
114
+ end
115
+
116
+ super
117
+ end
118
+ end
119
+
120
+ class << self
121
+ # The is one of the two entrypoints to the module. This should be called
122
+ # with a string that contains Ruby source. It returns the modified Ruby
123
+ # source.
124
+ def erase(source)
125
+ erase_result(YARP.parse(source), source)
126
+ end
127
+
128
+ # This is one of the two entrypoints to the module. This should be called
129
+ # with a filepath that points to a file that contains Ruby source. It
130
+ # returns the modified Ruby source.
131
+ def erase_file(filepath)
132
+ erase_result(YARP.parse_file(filepath), File.read(filepath))
133
+ end
134
+
135
+ private
136
+
137
+ # Accept a YARP::ParseResult and return a list of ranges that should be
138
+ # erased from comments that contain typed sigils.
139
+ def comment_ranges(result, ranges)
140
+ first = result.value.statements.body.first
141
+ minimum = first ? first.location.start_line : result.source.offsets.length
142
+
143
+ result.comments.each do |comment|
144
+ # Implicitly assuming that comments are in order.
145
+ break if comment.location.start_line >= minimum
146
+
147
+ if comment.type == :inline && comment.location.slice.match?(/\A#\s*typed:\s*(?:ignore|false|true|strict|strong)\s*\z/)
148
+ ranges << ((comment.location.start_offset + 1)...comment.location.end_offset)
149
+ end
150
+ end
151
+ end
152
+
153
+ # Accept a YARP::ParseResult and return the modified Ruby source.
154
+ def erase_result(result, source)
155
+ ranges = []
156
+
157
+ result.value.accept(Ranges.new(ranges))
158
+ comment_ranges(result, ranges)
159
+
160
+ ranges.inject(source) do |current, range|
161
+ # This is deceptive in that it hides that it actually replaces
162
+ # everything with spaces _except_ newline characters, which is keeps
163
+ # in place.
164
+ source.bytesplice(range, source.byteslice(range).gsub(/./, " "))
165
+ end
166
+ end
21
167
  end
22
168
  end
23
169
  end
@@ -30,6 +30,8 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ["lib"]
32
32
 
33
+ spec.add_dependency "yarp"
34
+
33
35
  spec.add_development_dependency "bundler"
34
36
  spec.add_development_dependency "rake"
35
37
  spec.add_development_dependency "minitest"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-eraser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.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: 2023-07-03 00:00:00.000000000 Z
11
+ date: 2023-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: yarp
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -61,6 +75,7 @@ extensions: []
61
75
  extra_rdoc_files: []
62
76
  files:
63
77
  - ".github/dependabot.yml"
78
+ - ".github/workflows/auto-merge.yml"
64
79
  - ".github/workflows/main.yml"
65
80
  - ".gitignore"
66
81
  - CHANGELOG.md
@@ -74,9 +89,8 @@ files:
74
89
  - exe/sorbet-eraser
75
90
  - lib/sorbet/eraser.rb
76
91
  - lib/sorbet/eraser/autoload.rb
92
+ - lib/sorbet/eraser/bytesplice.rb
77
93
  - lib/sorbet/eraser/cli.rb
78
- - lib/sorbet/eraser/parser.rb
79
- - lib/sorbet/eraser/patterns.rb
80
94
  - lib/sorbet/eraser/t.rb
81
95
  - lib/sorbet/eraser/t/enum.rb
82
96
  - lib/sorbet/eraser/t/props.rb
@@ -89,7 +103,7 @@ licenses:
89
103
  - MIT
90
104
  metadata:
91
105
  bug_tracker_uri: https://github.com/kddnewton/sorbet-eraser/issues
92
- changelog_uri: https://github.com/kddnewton/sorbet-eraser/blob/v0.4.0/CHANGELOG.md
106
+ changelog_uri: https://github.com/kddnewton/sorbet-eraser/blob/v0.6.0/CHANGELOG.md
93
107
  source_code_uri: https://github.com/kddnewton/sorbet-eraser
94
108
  rubygems_mfa_required: 'true'
95
109
  post_install_message:
@@ -1,293 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sorbet
4
- module Eraser
5
- # A Ripper parser that will replace usage of Sorbet patterns with whitespace
6
- # so that location information is maintained but Sorbet methods aren't
7
- # called.
8
- class Parser < Ripper
9
- # Represents a line in the source. If this class is being used, it means
10
- # that every character in the string is 1 byte in length, so we can just
11
- # return the start of the line + the index.
12
- class SingleByteString
13
- def initialize(start)
14
- @start = start
15
- end
16
-
17
- def [](byteindex)
18
- @start + byteindex
19
- end
20
- end
21
-
22
- # Represents a line in the source. If this class is being used, it means
23
- # that there are characters in the string that are multi-byte, so we will
24
- # build up an array of indices, such that array[byteindex] will be equal
25
- # to the index of the character within the string.
26
- class MultiByteString
27
- def initialize(start, line)
28
- @indices = []
29
-
30
- line
31
- .each_char
32
- .with_index(start) do |char, index|
33
- char.bytesize.times { @indices << index }
34
- end
35
- end
36
-
37
- def [](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
42
- end
43
- end
44
-
45
- # Represents a node in the AST. Keeps track of the event that generated
46
- # it, any child nodes that descend from it, and the location in the
47
- # source.
48
- class Node
49
- attr_reader :event, :body, :range
50
-
51
- def initialize(event, body, range)
52
- @event = event
53
- @body = body
54
- @range = range
55
- end
56
-
57
- def match?(pattern)
58
- to_s.match?(pattern)
59
- end
60
-
61
- def to_s
62
- @repr ||= begin
63
- children = body.map { |child| child.is_a?(Array) ? child.map(&:to_s) : child }
64
- "<#{event} #{children.join(" ")}>"
65
- end
66
- end
67
- end
68
-
69
- # Raised in the case that source can't be parsed.
70
- class ParsingError < StandardError
71
- end
72
-
73
- attr_reader :source, :line_counts, :errors, :patterns, :heredocs
74
-
75
- def initialize(source)
76
- super(source)
77
-
78
- @source = source
79
- @line_counts = []
80
- last_index = 0
81
-
82
- source.lines.each do |line|
83
- if line.size == line.bytesize
84
- @line_counts << SingleByteString.new(last_index)
85
- else
86
- @line_counts << MultiByteString.new(last_index, line)
87
- end
88
-
89
- last_index += line.bytesize
90
- end
91
-
92
- @errors = []
93
- @patterns = []
94
- @heredocs = []
95
- end
96
-
97
- def self.erase(source)
98
- parser = new(source)
99
-
100
- if parser.parse.nil? || parser.error?
101
- raise ParsingError, parser.errors.join("\n")
102
- else
103
- parser.patterns.inject(source) do |current, pattern|
104
- pattern.erase(current)
105
- end
106
- end
107
- end
108
-
109
- private
110
-
111
- def loc
112
- line_counts[lineno - 1][column]
113
- end
114
-
115
- def find_loc(args)
116
- ranges = []
117
-
118
- args.each do |arg|
119
- case arg
120
- when Node
121
- ranges << arg.range if arg.range
122
- when Array
123
- ranges << find_loc(arg)
124
- end
125
- end
126
-
127
- case ranges.length
128
- when 0
129
- nil
130
- when 1
131
- ranges.first
132
- else
133
- ranges.first.begin...ranges.last.end
134
- end
135
- end
136
-
137
- # Better location information for aref.
138
- def on_aref(recv, arg)
139
- rend = arg.range.end + source[arg.range.end..].index("]") + 1
140
- Node.new(:aref, [recv, arg], recv.range.begin...rend)
141
- end
142
-
143
- # Better location information for arg_paren.
144
- def on_arg_paren(arg)
145
- if arg
146
- rbegin = source[..arg.range.begin].rindex("(")
147
- rend = arg.range.end + source[arg.range.end..].index(")") + 1
148
- Node.new(:arg_paren, [arg], rbegin...rend)
149
- else
150
- segment = source[..loc]
151
- Node.new(:arg_paren, [arg], segment.rindex("(")...(segment.rindex(")") + 1))
152
- end
153
- end
154
-
155
- LISTS = { qsymbols: "%i", qwords: "%w", symbols: "%I", words: "%W" }.freeze
156
- TERMINATORS = { "[" => "]", "{" => "}", "(" => ")", "<" => ">" }.freeze
157
-
158
- # Better location information for array.
159
- def on_array(arg)
160
- case arg&.event
161
- when nil
162
- segment = source[..loc]
163
- Node.new(:array, [arg], segment.rindex("[")...(segment.rindex("]") + 1))
164
- when :qsymbols, :qwords, :symbols, :words
165
- rbegin = source[...arg.range.begin].rindex(LISTS.fetch(arg.event))
166
- rend = source[arg.range.end..].index(TERMINATORS.fetch(source[rbegin + 2]) { source[rbegin + 2] }) + arg.range.end + 1
167
- Node.new(:array, [arg], rbegin...rend)
168
- else
169
- Node.new(:array, [arg], arg.range)
170
- end
171
- end
172
-
173
- # Better location information for brace_block.
174
- def on_brace_block(params, body)
175
- if params || body.range
176
- rbegin = source[...(params || body).range.begin].rindex("{")
177
-
178
- rend = body.range&.end || params.range.end
179
- rend = rend + source[rend..].index("}") + 1
180
-
181
- Node.new(:brace_block, [params, body], rbegin...rend)
182
- else
183
- segment = source[..loc]
184
- Node.new(:brace_block, [params, body], segment.rindex("{")...(segment.rindex("}") + 1))
185
- end
186
- end
187
-
188
- # Better location information for do_block.
189
- def on_do_block(params, body)
190
- if params || body.range
191
- rbegin = source[...(params || body).range.begin].rindex("do")
192
-
193
- rend = body.range&.end || params.range.end
194
- rend = rend + source[rend..].index("end") + 3
195
-
196
- Node.new(:do_block, [params, body], rbegin...rend)
197
- else
198
- segment = source[..loc]
199
- Node.new(:do_block, [params, body], segment.rindex("do")...(segment.rindex("end") + 3))
200
- end
201
- end
202
-
203
- # Better location information for hash.
204
- def on_hash(arg)
205
- if arg
206
- Node.new(:hash, [arg], arg.range)
207
- else
208
- segment = source[..loc]
209
- Node.new(:hash, [arg], segment.rindex("{")...(segment.rindex("}") + 1))
210
- end
211
- end
212
-
213
- # Track the open heredocs so we can replace the string literal ranges with
214
- # the range of their declarations.
215
- def on_heredoc_beg(value)
216
- range = loc.then { |start| start...(start + value.bytesize) }
217
- heredocs << [range, value, nil]
218
-
219
- Node.new(:@heredoc_beg, [value], range)
220
- end
221
-
222
- # If a heredoc ends, then the next string literal event will be the
223
- # heredoc.
224
- def on_heredoc_end(value)
225
- range = loc.then { |start| start...(start + value.bytesize) }
226
- heredocs.find { |(_, beg_arg, end_arg)| beg_arg.include?(value.strip) && end_arg.nil? }[2] = value
227
-
228
- Node.new(:@heredoc_end, [value], range)
229
- end
230
-
231
- # Track the parsing errors for nicer error messages.
232
- def on_parse_error(error)
233
- errors << "line #{lineno}: #{error}"
234
- end
235
-
236
- # Better location information for string_literal taking into account
237
- # heredocs.
238
- def on_string_literal(arg)
239
- if heredoc = heredocs.find { |(_, _, end_arg)| end_arg }
240
- Node.new(:string_literal, [arg], heredocs.delete(heredoc)[0])
241
- else
242
- Node.new(:string_literal, [arg], arg.range)
243
- end
244
- end
245
-
246
- handled = private_instance_methods(false)
247
-
248
- # Loop through all of the scanner events and define a basic method that
249
- # wraps everything into a node class.
250
- SCANNER_EVENTS.each do |event|
251
- next if handled.include?(:"on_#{event}")
252
-
253
- define_method(:"on_#{event}") do |value|
254
- range = loc.then { |start| start...(start + (value&.bytesize || 0)) }
255
- Node.new(:"@#{event}", [value], range)
256
- end
257
- end
258
-
259
- # Loop through the parser events and generate a method for each event. If
260
- # it's one of the _new methods, then use arrays like SexpBuilderPP. If
261
- # it's an _add method then just append to the array. If it's a normal
262
- # method, then create a new node and determine its bounds.
263
- PARSER_EVENT_TABLE.each do |event, arity|
264
- next if handled.include?(:"on_#{event}")
265
-
266
- if event =~ /\A(.+)_new\z/ && event != :assoc_new
267
- prefix = $1.to_sym
268
-
269
- define_method(:"on_#{event}") do
270
- Node.new(prefix, [], nil)
271
- end
272
- elsif event =~ /_add\z/
273
- define_method(:"on_#{event}") do |node, value|
274
- range =
275
- if node.body.empty?
276
- value.range
277
- elsif node.range && value.range
278
- (node.range.begin...value.range.end)
279
- else
280
- node.range || value.range
281
- end
282
-
283
- node.class.new(node.event, node.body + [value], range)
284
- end
285
- else
286
- define_method(:"on_#{event}") do |*args|
287
- Node.new(event, args, find_loc(args))
288
- end
289
- end
290
- end
291
- end
292
- end
293
- end
@@ -1,250 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sorbet
4
- module Eraser
5
- module Patterns
6
- # A pattern in code that represents a call to a special Sorbet method.
7
- class Pattern
8
- attr_reader :range, :metadata
9
-
10
- def initialize(range, **metadata)
11
- @range = range
12
- @metadata = metadata
13
- end
14
-
15
- def erase(source)
16
- original = source[range]
17
- replaced = replace(original)
18
-
19
- # puts "Replacing #{original} (len=#{original.length}) " \
20
- # "with #{replaced} (len=#{replaced.length})"
21
-
22
- source[range] = replaced
23
- source
24
- end
25
-
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(/./, " ")
31
- end
32
-
33
- def replace(segment)
34
- segment
35
- end
36
- end
37
-
38
- # T.must(foo) => foo
39
- # T.reveal_type(foo) => foo
40
- # T.unsafe(foo) => foo
41
- class TOneArgMethodCallParensPattern < Pattern
42
- def replace(segment)
43
- segment.gsub(/(T\s*\.(?:must|reveal_type|unsafe)\(\s*)(.+)(\s*\))(.*)/m) do
44
- "#{blank($1)}#{$2}#{blank($3)}#{$4}"
45
- end
46
- end
47
- end
48
-
49
- # T.assert_type!(foo, bar) => foo
50
- # T.bind(self, foo) => self
51
- # T.cast(foo, bar) => foo
52
- # T.let(foo, bar) => let
53
- class TTwoArgMethodCallParensPattern < Pattern
54
- def replace(segment)
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}"
79
- end
80
- end
81
- end
82
-
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
89
- end
90
- end
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
- # typed: ignore
104
- # typed: false
105
- # typed: true
106
- # typed: strong
107
- class TypedCommentPattern < Pattern
108
- def replace(segment)
109
- segment.gsub(/(\A#\s*typed:\s*)(?:ignore|false|true|strong)(\s*)\z/) do
110
- "#{$1}ignore#{$2}"
111
- end
112
- end
113
- end
114
-
115
- def on_comment(comment)
116
- super.tap do |node|
117
- if lineno == 1 && comment.match?(/\A#\s*typed:\s*(?:ignore|false|true|strong)\s*\z/)
118
- # typed: ignore
119
- # typed: false
120
- # typed: true
121
- # typed: strong
122
- patterns << TypedCommentPattern.new(node.range)
123
- end
124
- end
125
- end
126
-
127
- def on_method_add_arg(call, arg_paren)
128
- if call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:must|reveal_type|unsafe)>>\z/) && arg_paren.match?(/\A<arg_paren <args_add_block <args .+> false>>\z/)
129
- # T.must(foo)
130
- # T.reveal_type(foo)
131
- # T.unsafe(foo)
132
- patterns << TOneArgMethodCallParensPattern.new(call.range.begin...arg_paren.range.end)
133
- elsif call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident (?:assert_type!|cast|let)>>\z/) && arg_paren.match?(/\A<arg_paren <args_add_block <args .+> false>>\z/)
134
- # T.assert_type!(foo, bar)
135
- # T.cast(foo, bar)
136
- # T.let(foo, bar)
137
- patterns << TTwoArgMethodCallParensPattern.new(
138
- call.range.begin...arg_paren.range.end,
139
- comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
140
- )
141
- elsif call.match?(/\A<call <var_ref <@const T>> <@period \.> <@ident bind>>\z/) && arg_paren.match?(/\A<arg_paren <args_add_block <args <var_ref <@kw self>> .+> false>>\z/)
142
- # T.bind(self, foo)
143
- patterns << TTwoArgMethodCallParensPattern.new(
144
- call.range.begin...arg_paren.range.end,
145
- comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
146
- )
147
- elsif call.match?(/\A<fcall <@ident (?:abstract|final|interface)!>>\z/) && arg_paren.match?("<args >")
148
- # abstract!
149
- # final!
150
- # interface!
151
- patterns << DeclarationPattern.new(call.range)
152
- elsif call.match?("<fcall <@ident mixes_in_class_methods>>") && arg_paren.match?(/\A<arg_paren <args_add_block <args <.+>>> false>>\z/)
153
- # mixes_in_class_methods(foo)
154
- patterns << MixesInClassMethodsPattern.new(call.range.begin...arg_paren.range.end)
155
- end
156
-
157
- super
158
- end
159
-
160
- # prop :foo, String => prop :foo
161
- # const :foo, String => const :foo
162
- class PropWithoutOptionsPattern < Pattern
163
- def replace(segment)
164
- segment.dup.tap do |replacement|
165
- range = metadata.fetch(:comma)..-1
166
- replacement[range] = blank(replacement[range])
167
- end
168
- end
169
- end
170
-
171
- # prop :foo, String, default: "" => prop :foo, default: ""
172
- # const :foo, String, default: "" => const :foo, default: ""
173
- class PropWithOptionsPattern < Pattern
174
- def replace(segment)
175
- segment.dup.tap do |replacement|
176
- first_comma = metadata.fetch(:first_comma)
177
- second_comma = metadata.fetch(:second_comma)
178
-
179
- range = (first_comma + 1)..second_comma
180
- replacement[range] = blank(replacement[range])
181
- end
182
- end
183
- end
184
-
185
- def on_command(ident, args_add_block)
186
- if ident.match?(/\A<@ident (?:const|prop)>\z/)
187
- if args_add_block.match?(/\A<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> <bare_assoc_hash .+> false>\z/)
188
- # prop :foo, String, default: ""
189
- # const :foo, String, default: ""
190
- patterns << PropWithOptionsPattern.new(
191
- ident.range.begin..args_add_block.range.end,
192
- first_comma: args_add_block.body[0].body[0].range.end - ident.range.begin,
193
- second_comma: args_add_block.body[0].body[1].range.end - ident.range.begin
194
- )
195
- elsif args_add_block.match?(/\A<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> false>\z/)
196
- # prop :foo, String
197
- # const :foo, String
198
- patterns << PropWithoutOptionsPattern.new(
199
- ident.range.begin..args_add_block.range.end,
200
- comma: args_add_block.body[0].body[0].range.end - ident.range.begin
201
- )
202
- end
203
- end
204
-
205
- super
206
- end
207
-
208
- def on_command_call(var_ref, period, ident, args_add_block)
209
- if var_ref.match?("<var_ref <@const T>>") && period.match?("<@period .>") && ident.match?(/\A<@ident (?:must|reveal_type|unsafe)>\z/) && args_add_block.match?(/\A<args_add_block <args <.+>> false>\z/) && args_add_block.body[0].body.length == 1
210
- # T.must foo
211
- # T.reveal_type foo
212
- # T.unsafe foo
213
- patterns << TMustNoParensPattern.new(var_ref.range.begin..args_add_block.range.end)
214
- end
215
-
216
- super
217
- end
218
-
219
- # sig { foo } =>
220
- class SigBracesPattern < Pattern
221
- def replace(segment)
222
- segment.gsub(/(sig\s*\{.+\})(.*)/m) do
223
- "#{blank($1)}#{$2}"
224
- end
225
- end
226
- end
227
-
228
- # sig do foo end =>
229
- class SigBlockPattern < Pattern
230
- def replace(segment)
231
- segment.gsub(/(sig\s*do.+end)(.*)/m) do
232
- "#{blank($1)}#{$2}"
233
- end
234
- end
235
- end
236
-
237
- def on_stmts_add(node, value)
238
- if value.match?(/\A<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <brace_block <stmts .+>>>\z/)
239
- # sig { foo }
240
- patterns << SigBracesPattern.new(value.range)
241
- elsif value.match?(/\A<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <do_block <bodystmt .+>>>\z/)
242
- # sig do foo end
243
- patterns << SigBlockPattern.new(value.range)
244
- end
245
-
246
- super
247
- end
248
- end
249
- end
250
- end