sorbet-eraser 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/auto-merge.yml +22 -0
- data/.github/workflows/main.yml +10 -18
- data/CHANGELOG.md +9 -2
- data/Gemfile.lock +4 -2
- data/README.md +5 -5
- data/exe/sorbet-eraser +8 -1
- data/lib/sorbet/eraser/autoload.rb +1 -2
- data/lib/sorbet/eraser/bytesplice.rb +18 -0
- data/lib/sorbet/eraser/cli.rb +1 -3
- data/lib/sorbet/eraser/version.rb +1 -1
- data/lib/sorbet/eraser.rb +159 -13
- data/sorbet-eraser.gemspec +2 -0
- metadata +19 -5
- data/lib/sorbet/eraser/parser.rb +0 -293
- data/lib/sorbet/eraser/patterns.rb +0 -249
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb18fb992ca140f69b3971850f1aa3da26091b6d01ba133656c13d6c2427a80a
|
4
|
+
data.tar.gz: 7e00f8f81e9b90b23bec44a9e943322eddbf51d75f5ae6491787a7674ce3ca6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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}}
|
data/.github/workflows/main.yml
CHANGED
@@ -1,32 +1,24 @@
|
|
1
1
|
name: Main
|
2
|
+
|
2
3
|
on:
|
3
4
|
- push
|
4
|
-
-
|
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:
|
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/CHANGELOG.md
CHANGED
@@ -6,6 +6,12 @@ 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
|
+
|
9
15
|
## [0.5.0] - 2023-07-13
|
10
16
|
|
11
17
|
### Added
|
@@ -50,8 +56,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
|
|
50
56
|
|
51
57
|
- Require MFA for releasing.
|
52
58
|
|
53
|
-
[unreleased]: https://github.com/kddnewton/sorbet-eraser/compare/v0.
|
54
|
-
[0.
|
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
|
55
62
|
[0.4.0]: https://github.com/kddnewton/sorbet-eraser/compare/v0.3.1...v0.4.0
|
56
63
|
[0.3.1]: https://github.com/kddnewton/sorbet-eraser/compare/v0.3.0...v0.3.1
|
57
64
|
[0.3.0]: https://github.com/kddnewton/sorbet-eraser/compare/v0.2.0...v0.3.0
|
data/Gemfile.lock
CHANGED
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
|
-
|
37
|
+
("World!" )
|
38
38
|
end
|
39
39
|
end
|
40
40
|
```
|
41
41
|
|
42
|
-
|
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
|
|
@@ -97,7 +97,7 @@ Below is a table of the status of each `sorbet-runtime` construct and its curren
|
|
97
97
|
|
98
98
|
| Construct | Status | Replacement |
|
99
99
|
| --------------------------------------------------- | ------ | ----------- |
|
100
|
-
| `# typed: foo` | ✅ |
|
100
|
+
| `# typed: foo` | ✅ | `#` |
|
101
101
|
| `extend T::*` | ✅ | Shimmed |
|
102
102
|
| `abstract!`, `final!`, `interface!`, `sealed!` | ✅ | Shimmed |
|
103
103
|
| `mixes_in_class_methods(*)`, `requires_ancestor(*)` | ✅ | Shimmed |
|
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
|
-
|
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
|
@@ -16,8 +16,7 @@ else
|
|
16
16
|
# Otherwise if the method isn't defined by bootsnap, then we'll define it
|
17
17
|
# ourselves.
|
18
18
|
def (RubyVM::InstructionSequence).load_iseq(filepath)
|
19
|
-
|
20
|
-
erased = Sorbet::Eraser.erase(contents)
|
19
|
+
erased = Sorbet::Eraser.erase_filepath(filepath)
|
21
20
|
RubyVM::InstructionSequence.compile(erased, filepath, filepath)
|
22
21
|
end
|
23
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
|
data/lib/sorbet/eraser/cli.rb
CHANGED
@@ -52,15 +52,13 @@ module Sorbet
|
|
52
52
|
private
|
53
53
|
|
54
54
|
def process(filepath)
|
55
|
-
contents = Eraser.
|
55
|
+
contents = Eraser.erase_file(filepath)
|
56
56
|
|
57
57
|
if verify && Ripper.sexp_raw(contents).nil?
|
58
58
|
warn("Internal error while parsing #{filepath}")
|
59
59
|
else
|
60
60
|
File.write(filepath, contents)
|
61
61
|
end
|
62
|
-
rescue Parser::ParsingError => error
|
63
|
-
warn("Could not parse #{filepath}: #{error}")
|
64
62
|
rescue => error
|
65
63
|
warn("Could not parse #{filepath}: #{error}")
|
66
64
|
end
|
data/lib/sorbet/eraser.rb
CHANGED
@@ -1,23 +1,169 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
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
|
-
#
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
data/sorbet-eraser.gemspec
CHANGED
@@ -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
|
+
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-
|
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.
|
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:
|
data/lib/sorbet/eraser/parser.rb
DELETED
@@ -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.byteslice(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.byteslice(..arg.range.begin).rindex("(")
|
147
|
-
rend = arg.range.end + source.byteslice(arg.range.end..).index(")") + 1
|
148
|
-
Node.new(:arg_paren, [arg], rbegin...rend)
|
149
|
-
else
|
150
|
-
segment = source.byteslice(..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.byteslice(..loc)
|
163
|
-
Node.new(:array, [arg], segment.rindex("[")...(segment.rindex("]") + 1))
|
164
|
-
when :qsymbols, :qwords, :symbols, :words
|
165
|
-
rbegin = source.byteslice(...arg.range.begin).rindex(LISTS.fetch(arg.event))
|
166
|
-
rend = source.byteslice(arg.range.end..).index(TERMINATORS.fetch(source.byteslice(rbegin + 2)) { source.byteslice(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.byteslice(...(params || body).range.begin).rindex("{")
|
177
|
-
|
178
|
-
rend = body.range&.end || params.range.end
|
179
|
-
rend = rend + source.byteslice(rend..).index("}") + 1
|
180
|
-
|
181
|
-
Node.new(:brace_block, [params, body], rbegin...rend)
|
182
|
-
else
|
183
|
-
segment = source.byteslice(..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.byteslice(...(params || body).range.begin).rindex("do")
|
192
|
-
|
193
|
-
rend = body.range&.end || params.range.end
|
194
|
-
rend = rend + source.byteslice(rend..).index("end") + 3
|
195
|
-
|
196
|
-
Node.new(:do_block, [params, body], rbegin...rend)
|
197
|
-
else
|
198
|
-
segment = source.byteslice(..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.byteslice(..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.begin - 1)...(arg.range.end + 1))
|
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,249 +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
|
-
encoding = source.encoding
|
17
|
-
source.force_encoding(Encoding::ASCII_8BIT)
|
18
|
-
|
19
|
-
source[range] = replace(source[range])
|
20
|
-
source.force_encoding(encoding)
|
21
|
-
end
|
22
|
-
|
23
|
-
def blank(segment)
|
24
|
-
# This is deceptive in that it hides that it actually replaces
|
25
|
-
# everything with spaces _except_ newline characters, which is keeps
|
26
|
-
# in place.
|
27
|
-
segment.gsub(/./, " ")
|
28
|
-
end
|
29
|
-
|
30
|
-
def replace(segment)
|
31
|
-
segment
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
# T.must(foo) => foo
|
36
|
-
# T.reveal_type(foo) => foo
|
37
|
-
# T.unsafe(foo) => foo
|
38
|
-
class TOneArgMethodCallParensPattern < Pattern
|
39
|
-
def replace(segment)
|
40
|
-
segment.gsub(/(T\s*\.(?:must|reveal_type|unsafe)\(\s*)(.+)(\s*\))(.*)/m) do
|
41
|
-
"#{blank($1)}#{$2}#{blank($3)}#{$4}"
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# T.assert_type!(foo, bar) => foo
|
47
|
-
# T.bind(self, foo) => self
|
48
|
-
# T.cast(foo, bar) => foo
|
49
|
-
# T.let(foo, bar) => let
|
50
|
-
class TTwoArgMethodCallParensPattern < Pattern
|
51
|
-
def replace(segment)
|
52
|
-
replacement = segment.dup
|
53
|
-
|
54
|
-
# We can't really rely on regex here because commas have semantic
|
55
|
-
# meaning and you might have some in the value of the first argument.
|
56
|
-
comma = metadata.fetch(:comma)
|
57
|
-
pre, post = 0...comma, comma..-1
|
58
|
-
|
59
|
-
replacement[pre] =
|
60
|
-
replacement[pre].gsub(/(T\s*\.(?:assert_type!|bind|cast|let)\(\s*)(.+)/m) do
|
61
|
-
"#{blank($1)}#{$2}"
|
62
|
-
end
|
63
|
-
|
64
|
-
replacement[post] = blank(replacement[post])
|
65
|
-
replacement
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# abstract! =>
|
70
|
-
# final! =>
|
71
|
-
# interface! =>
|
72
|
-
class DeclarationPattern < Pattern
|
73
|
-
def replace(segment)
|
74
|
-
segment.gsub(/((?:abstract|final|interface)!(?:\(\s*\))?)(.*)/) do
|
75
|
-
"#{blank($1)}#{$2}"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
# mixes_in_class_methods(foo) => foo
|
81
|
-
class MixesInClassMethodsPattern < Pattern
|
82
|
-
def replace(segment)
|
83
|
-
segment.gsub(/(mixes_in_class_methods\(\s*)(.+)(\s*\))(.*)/m) do
|
84
|
-
"#{blank($1)}#{$2}#{blank($3)}#{$4}"
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# T.must foo => foo
|
90
|
-
# T.reveal_type foo => foo
|
91
|
-
# T.unsafe foo => foo
|
92
|
-
class TMustNoParensPattern < Pattern
|
93
|
-
def replace(segment)
|
94
|
-
segment.gsub(/(T\s*\.(?:must|reveal_type|unsafe)\s*)(.+)/) do
|
95
|
-
"#{blank($1)}#{$2}"
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# typed: ignore => #
|
101
|
-
# typed: false => #
|
102
|
-
# typed: true => #
|
103
|
-
# typed: strict => #
|
104
|
-
# typed: strong => #
|
105
|
-
class TypedCommentPattern < Pattern
|
106
|
-
def replace(segment)
|
107
|
-
segment.gsub(/\A#(\s*typed:\s*(?:ignore|false|true|strict|strong)(\s*))\z/) do
|
108
|
-
"##{blank($1)}"
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def on_comment(comment)
|
114
|
-
super.tap do |node|
|
115
|
-
if lineno == 1 && comment.match?(/\A#\s*typed:\s*(?:ignore|false|true|strict|strong)\s*\z/)
|
116
|
-
# typed: ignore
|
117
|
-
# typed: false
|
118
|
-
# typed: true
|
119
|
-
# typed: strict
|
120
|
-
# typed: strong
|
121
|
-
patterns << TypedCommentPattern.new(node.range)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def on_method_add_arg(call, arg_paren)
|
127
|
-
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/)
|
128
|
-
# T.must(foo)
|
129
|
-
# T.reveal_type(foo)
|
130
|
-
# T.unsafe(foo)
|
131
|
-
patterns << TOneArgMethodCallParensPattern.new(call.range.begin...arg_paren.range.end)
|
132
|
-
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/)
|
133
|
-
# T.assert_type!(foo, bar)
|
134
|
-
# T.cast(foo, bar)
|
135
|
-
# T.let(foo, bar)
|
136
|
-
patterns << TTwoArgMethodCallParensPattern.new(
|
137
|
-
call.range.begin...arg_paren.range.end,
|
138
|
-
comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
|
139
|
-
)
|
140
|
-
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/)
|
141
|
-
# T.bind(self, foo)
|
142
|
-
patterns << TTwoArgMethodCallParensPattern.new(
|
143
|
-
call.range.begin...arg_paren.range.end,
|
144
|
-
comma: arg_paren.body[0].body[0].body[0].range.end - call.range.begin
|
145
|
-
)
|
146
|
-
elsif call.match?(/\A<fcall <@ident (?:abstract|final|interface)!>>\z/) && arg_paren.match?("<args >")
|
147
|
-
# abstract!
|
148
|
-
# final!
|
149
|
-
# interface!
|
150
|
-
patterns << DeclarationPattern.new(call.range)
|
151
|
-
elsif call.match?("<fcall <@ident mixes_in_class_methods>>") && arg_paren.match?(/\A<arg_paren <args_add_block <args <.+>>> false>>\z/)
|
152
|
-
# mixes_in_class_methods(foo)
|
153
|
-
patterns << MixesInClassMethodsPattern.new(call.range.begin...arg_paren.range.end)
|
154
|
-
end
|
155
|
-
|
156
|
-
super
|
157
|
-
end
|
158
|
-
|
159
|
-
# prop :foo, String => prop :foo
|
160
|
-
# const :foo, String => const :foo
|
161
|
-
class PropWithoutOptionsPattern < Pattern
|
162
|
-
def replace(segment)
|
163
|
-
segment.dup.tap do |replacement|
|
164
|
-
range = metadata.fetch(:comma)..-1
|
165
|
-
replacement[range] = blank(replacement[range])
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
# prop :foo, String, default: "" => prop :foo, default: ""
|
171
|
-
# const :foo, String, default: "" => const :foo, default: ""
|
172
|
-
class PropWithOptionsPattern < Pattern
|
173
|
-
def replace(segment)
|
174
|
-
segment.dup.tap do |replacement|
|
175
|
-
first_comma = metadata.fetch(:first_comma)
|
176
|
-
second_comma = metadata.fetch(:second_comma)
|
177
|
-
|
178
|
-
range = (first_comma + 1)..second_comma
|
179
|
-
replacement[range] = blank(replacement[range])
|
180
|
-
end
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def on_command(ident, args_add_block)
|
185
|
-
if ident.match?(/\A<@ident (?:const|prop)>\z/)
|
186
|
-
if args_add_block.match?(/\A<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> <bare_assoc_hash .+> false>\z/)
|
187
|
-
# prop :foo, String, default: ""
|
188
|
-
# const :foo, String, default: ""
|
189
|
-
patterns << PropWithOptionsPattern.new(
|
190
|
-
ident.range.begin..args_add_block.range.end,
|
191
|
-
first_comma: args_add_block.body[0].body[0].range.end - ident.range.begin,
|
192
|
-
second_comma: args_add_block.body[0].body[1].range.end - ident.range.begin
|
193
|
-
)
|
194
|
-
elsif args_add_block.match?(/\A<args_add_block <args <symbol_literal <symbol <@ident .+?>>> <.+> false>\z/)
|
195
|
-
# prop :foo, String
|
196
|
-
# const :foo, String
|
197
|
-
patterns << PropWithoutOptionsPattern.new(
|
198
|
-
ident.range.begin..args_add_block.range.end,
|
199
|
-
comma: args_add_block.body[0].body[0].range.end - ident.range.begin
|
200
|
-
)
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
super
|
205
|
-
end
|
206
|
-
|
207
|
-
def on_command_call(var_ref, period, ident, args_add_block)
|
208
|
-
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
|
209
|
-
# T.must foo
|
210
|
-
# T.reveal_type foo
|
211
|
-
# T.unsafe foo
|
212
|
-
patterns << TMustNoParensPattern.new(var_ref.range.begin..args_add_block.range.end)
|
213
|
-
end
|
214
|
-
|
215
|
-
super
|
216
|
-
end
|
217
|
-
|
218
|
-
# sig { foo } =>
|
219
|
-
class SigBracesPattern < Pattern
|
220
|
-
def replace(segment)
|
221
|
-
segment.gsub(/(sig\s*\{.+\})(.*)/m) do
|
222
|
-
"#{blank($1)}#{$2}"
|
223
|
-
end
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
# sig do foo end =>
|
228
|
-
class SigBlockPattern < Pattern
|
229
|
-
def replace(segment)
|
230
|
-
segment.gsub(/(sig\s*do.+end)(.*)/m) do
|
231
|
-
"#{blank($1)}#{$2}"
|
232
|
-
end
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
def on_stmts_add(node, value)
|
237
|
-
if value.match?(/\A<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <brace_block <stmts .+>>>\z/)
|
238
|
-
# sig { foo }
|
239
|
-
patterns << SigBracesPattern.new(value.range)
|
240
|
-
elsif value.match?(/\A<method_add_block <method_add_arg <fcall <@ident sig>> <args >> <do_block <bodystmt .+>>>\z/)
|
241
|
-
# sig do foo end
|
242
|
-
patterns << SigBlockPattern.new(value.range)
|
243
|
-
end
|
244
|
-
|
245
|
-
super
|
246
|
-
end
|
247
|
-
end
|
248
|
-
end
|
249
|
-
end
|