curdle 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '03619ba9f0f656e0ecb014f9e363d30489dcdf5e0ba644ca6cfcd97d07ce117f'
4
+ data.tar.gz: f77085d060179a1d04ab0c1c7c7d33452d999dc076eba557a782ab16955ba339
5
+ SHA512:
6
+ metadata.gz: 822585de86c1001268f0124c8b9fb88f8b6fc7b1fcbc73ed36196fef1bdc3fe7caa1cd1ce2c5d6bc4767d3e1f47a030c55bd9e153e593891b735ce08c101252a
7
+ data.tar.gz: e5f2be2f8e08e34a495965df1359f6fa25a5b9bd5c5c53866192fc6a65218bcc7a11fc1c9846cdce876c3b035f1ce6c3db6ec2485b2f6426aeca5b5186b80a19
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'pry-byebug'
7
+ end
8
+
9
+ group :test do
10
+ gem 'rspec'
11
+ end
12
+
13
+ group :development, :test do
14
+ gem 'rake'
15
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Cameron Dutro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubygems/package_task'
4
+
5
+ require 'curdle'
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run specs'
12
+ RSpec::Core::RakeTask.new do |t|
13
+ t.pattern = './spec/**/*_spec.rb'
14
+ end
data/bin/curdle ADDED
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'curdle'
4
+
5
+ ARGV.each do |file|
6
+ Curdle.process_file(file)
7
+ puts "Wrote #{file}"
8
+ end
data/curdle.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $:.unshift File.expand_path('lib', __dir__)
2
+ require 'curdle/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'curdle'
6
+ s.version = ::Curdle::VERSION
7
+ s.authors = ['Cameron Dutro']
8
+ s.email = ['camertron@gmail.com']
9
+ s.homepage = 'http://github.com/camertron/curdle'
10
+
11
+ s.description = s.summary = 'Programmatically remove Sorbet type annotations from Ruby code.'
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+
15
+ s.add_dependency 'parser', '~> 3.1'
16
+
17
+ s.add_development_dependency 'rspec'
18
+
19
+ s.require_path = 'lib'
20
+ s.executables << 'curdle'
21
+
22
+ s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'LICENSE', 'CHANGELOG.md', 'README.md', 'Rakefile', 'curdle.gemspec']
23
+ end
@@ -0,0 +1,209 @@
1
+ require 'parser/current'
2
+
3
+ module Curdle
4
+ class RemoveSorbet < ::Parser::TreeRewriter
5
+ def on_send(node)
6
+ remove_extend(node, 'T::Sig') ||
7
+ remove_extend(node, 'T::Generic') ||
8
+ remove_extend(node, 'T::Helpers') ||
9
+ remove_t_send(node) ||
10
+ remove_abstract_bang(node)
11
+
12
+ super
13
+ end
14
+
15
+ def on_block(node)
16
+ remove_sig(node)
17
+ super
18
+ end
19
+
20
+ def on_ivasgn(node)
21
+ remove_let(node)
22
+ super
23
+ end
24
+
25
+ def on_casgn(node)
26
+ remove_let(node) ||
27
+ remove_type_member(node) ||
28
+ remove_type_alias(node)
29
+
30
+ super
31
+ end
32
+
33
+ private
34
+
35
+ def remove_extend(send_node, const_str)
36
+ receiver, name, *args = *send_node
37
+ return false unless receiver.nil? && name == :extend
38
+ return false unless args.size == 1
39
+ return false unless args.first.location.expression.is?(const_str)
40
+
41
+ replace(
42
+ send_node.location.expression,
43
+ "# #{send_node.location.expression.source}"
44
+ )
45
+
46
+ true
47
+ end
48
+
49
+ def remove_sig(block_node)
50
+ send_node, = *block_node
51
+ receiver, name, *args = *send_node
52
+ return false unless receiver.nil? && name == :sig && args.empty?
53
+
54
+ comment_out_node(block_node)
55
+
56
+ true
57
+ end
58
+
59
+ def remove_t_send(send_node)
60
+ receiver, name = *send_node
61
+ return false unless receiver && receiver.location.expression.is?('T')
62
+
63
+ remove_cast(send_node) ||
64
+ remove_must(send_node) ||
65
+ remove_unsafe(send_node)
66
+
67
+ true
68
+ end
69
+
70
+ def remove_abstract_bang(send_node)
71
+ receiver, name = *send_node
72
+ return false unless receiver.nil? && name == :abstract!
73
+
74
+ comment_out_node(send_node)
75
+
76
+ true
77
+ end
78
+
79
+ def remove_cast(send_node)
80
+ _, name, *args = *send_node
81
+ return false unless name == :cast
82
+
83
+ remove_method_call(send_node)
84
+ remove_all_but_first_arg(send_node)
85
+
86
+ true
87
+ end
88
+
89
+ def remove_must(send_node)
90
+ _, name, *args = *send_node
91
+ return false unless name == :must
92
+
93
+ remove_method_call(send_node)
94
+
95
+ true
96
+ end
97
+
98
+ def remove_unsafe(send_node)
99
+ _, name, *args = *send_node
100
+ return false unless name == :unsafe
101
+
102
+ remove_method_call(send_node)
103
+
104
+ true
105
+ end
106
+
107
+ def remove_let(asgn_node)
108
+ case asgn_node.type
109
+ when :ivasgn
110
+ lhs_var, rhs = *asgn_node
111
+ when :casgn
112
+ _, lhs_var, rhs = *asgn_node
113
+ else
114
+ raise "Unexpected node type '#{asgn_node.type}'"
115
+ end
116
+
117
+ return false unless rhs
118
+ return false unless rhs.type == :send
119
+
120
+ receiver, name, *args = *rhs
121
+ return false unless name == :let && receiver.location.expression.is?('T')
122
+
123
+ if args.first.type == :ivar || args.first.type == :cvar
124
+ rhs_var, = *args.first
125
+
126
+ # Indicates the @ivar = T.let(@ivar, ...) pattern. The entire ivar
127
+ # assignment statement can be removed
128
+ if lhs_var == rhs_var
129
+ comment_out_node(asgn_node)
130
+ end
131
+ else
132
+ # indicates assigning the ivar to an actual value, in which case we
133
+ # can just remove the let and all args except the first
134
+ remove_method_call(rhs)
135
+ remove_all_but_first_arg(rhs)
136
+ end
137
+
138
+ true
139
+ end
140
+
141
+ def remove_type_member(casgn_node)
142
+ _, _const_name, value_node = *casgn_node
143
+ return false unless value_node.type == :send
144
+
145
+ receiver, name = *value_node
146
+ return false unless receiver.nil? && name == :type_member
147
+
148
+ comment_out_node(casgn_node)
149
+
150
+ true
151
+ end
152
+
153
+ def remove_type_alias(casgn_node)
154
+ _, _const_name, value_node = *casgn_node
155
+ return false unless value_node.type == :block
156
+
157
+ send_node, = *value_node
158
+ receiver, name = *send_node
159
+ return false unless receiver && receiver.location.expression.is?('T') && name == :type_alias
160
+
161
+ comment_out_node(casgn_node)
162
+
163
+ true
164
+ end
165
+
166
+ def remove_method_call(send_node)
167
+ receiver, name = *send_node
168
+
169
+ # remove receiver, dot, and method name
170
+ remove(receiver.location.expression) if receiver
171
+ remove(send_node.location.dot) if send_node.location.dot
172
+ remove(send_node.location.selector)
173
+
174
+ # remove enclosing parens
175
+ remove(send_node.location.begin) if send_node.location.begin
176
+ remove(send_node.location.end) if send_node.location.end
177
+ end
178
+
179
+ def remove_all_but_first_arg(send_node)
180
+ _, _, *args = *send_node
181
+ first_arg_loc = args[0].location.expression
182
+ remove(args[1].location.expression.with(begin_pos: first_arg_loc.end_pos))
183
+ end
184
+
185
+ def comment_out_node(node)
186
+ location = include_leading_whitespace(node.location.expression)
187
+ lines = location.source.split(/\r?\n/)
188
+ indent = lines.map { |line| line.index(/[\S]/) }.min
189
+ new_str = lines.map { |line| line.insert(indent, '# ') }.join("\n")
190
+ replace(location, new_str)
191
+ end
192
+
193
+ def include_leading_whitespace(location)
194
+ source = location.source_buffer.source
195
+ start = source.rindex(/[^ \t]/, location.begin_pos - 1) + 1
196
+ location.with(begin_pos: start)
197
+ end
198
+
199
+ # this isn't used anywhere but it was so hard to write I'm keeping it in case
200
+ # I ever need to use it again
201
+ def include_trailing_whitespace(location)
202
+ if m = source.match(/[ \t]*(?:\r?\n)*/, location.end_pos)
203
+ location = location.with(end_pos: location.end_pos + m[0].size)
204
+ end
205
+
206
+ location
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,46 @@
1
+ module Curdle
2
+ module Tasks
3
+ class << self
4
+ include Rake::DSL
5
+
6
+ def install(gemspec_file = find_gemspec)
7
+ task :build do
8
+ require 'tmpdir'
9
+ require 'fileutils'
10
+
11
+ Dir.mktmpdir do |build_dir|
12
+ spec = Bundler.load_gemspec(gemspec_file)
13
+
14
+ spec.files.each do |source_path|
15
+ next if File.directory?(source_path)
16
+
17
+ dest_path = File.join(build_dir, source_path)
18
+ FileUtils.mkdir_p(File.dirname(dest_path))
19
+
20
+ if File.extname(source_path) == '.rb'
21
+ File.write(dest_path, Curdle.process(File.read(source_path)))
22
+ else
23
+ FileUtils.cp(source_path, dest_path)
24
+ end
25
+ end
26
+
27
+ system("gem build --silent -C #{build_dir}")
28
+ artifact = Dir.glob(File.join(build_dir, "#{spec.name}*.gem")).first
29
+
30
+ FileUtils.mkdir_p('pkg')
31
+ artifact_dest = File.join('pkg', File.basename(artifact))
32
+ FileUtils.cp(artifact, artifact_dest)
33
+
34
+ puts "#{spec.name} #{spec.version} built to #{artifact_dest}."
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def find_gemspec
42
+ Dir.glob(File.join(Dir.getwd, '*.gemspec')).first
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,3 @@
1
+ module Curdle
2
+ VERSION = '1.0.0'
3
+ end
data/lib/curdle.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'parser/current'
2
+
3
+ module Curdle
4
+ autoload :RemoveSorbet, 'curdle/remove_sorbet'
5
+ autoload :Tasks, 'curdle/tasks'
6
+
7
+ def self.process_file(file)
8
+ code = File.read(file)
9
+ File.write(file, process(code, file))
10
+ end
11
+
12
+ def self.process(code, filename = '(curdle)')
13
+ ast = Parser::CurrentRuby.parse(code)
14
+ buffer = Parser::Source::Buffer.new(filename, source: code)
15
+ rewriter = Curdle::RemoveSorbet.new
16
+ rewriter.rewrite(buffer, ast)
17
+ end
18
+ end
@@ -0,0 +1,155 @@
1
+ require 'spec_helper'
2
+
3
+ describe Curdle do
4
+ def verify(input, expected)
5
+ expect(described_class.process(input)).to eq(expected)
6
+ end
7
+
8
+ it 'removes extend T::Sig' do
9
+ verify('extend T::Sig', '# extend T::Sig')
10
+ end
11
+
12
+ it 'removes extend T::Generic' do
13
+ verify('extend T::Generic', '# extend T::Generic')
14
+ end
15
+
16
+ it 'removes extend T::Helpers' do
17
+ verify('extend T::Helpers', '# extend T::Helpers')
18
+ end
19
+
20
+ it 'removes casts' do
21
+ verify('foo = T.cast(bar, String)', 'foo = bar')
22
+ end
23
+
24
+ it 'removes musts' do
25
+ verify('foo = T.must(bar)', 'foo = bar')
26
+ end
27
+
28
+ it 'removes unsafes' do
29
+ verify('foo = T.unsafe(bar)', 'foo = bar')
30
+ end
31
+
32
+ it 'removes abstract! calls' do
33
+ verify(<<~END1, <<~END2)
34
+ class Foo
35
+ abstract!
36
+ end
37
+ END1
38
+ class Foo
39
+ # abstract!
40
+ end
41
+ END2
42
+ end
43
+
44
+ it 'removes single-line sigs' do
45
+ verify(<<~END1, <<~END2)
46
+ class Foo
47
+ sig { returns(String) }
48
+ def foo
49
+ end
50
+ end
51
+ END1
52
+ class Foo
53
+ # sig { returns(String) }
54
+ def foo
55
+ end
56
+ end
57
+ END2
58
+ end
59
+
60
+ it 'removes multi-line sigs' do
61
+ verify(<<~END1, <<~END2)
62
+ class Foo
63
+ sig {
64
+ params(bar: String, baz: Integer).returns(String)
65
+ }
66
+ def foo(bar, baz)
67
+ end
68
+ end
69
+ END1
70
+ class Foo
71
+ # sig {
72
+ # params(bar: String, baz: Integer).returns(String)
73
+ # }
74
+ def foo(bar, baz)
75
+ end
76
+ end
77
+ END2
78
+ end
79
+
80
+ it 'removes T.let assigned to an instance variable' do
81
+ verify(<<~END1, <<~END2)
82
+ class Foo
83
+ def initialize
84
+ @version = T.let(@version, T.nilable(String))
85
+ end
86
+ end
87
+ END1
88
+ class Foo
89
+ def initialize
90
+ # @version = T.let(@version, T.nilable(String))
91
+ end
92
+ end
93
+ END2
94
+ end
95
+
96
+ it 'removes T.let ivar with an actual value' do
97
+ verify(<<~END1, <<~END2)
98
+ class Foo
99
+ def initialize
100
+ @version = T.let(version, T.nilable(String))
101
+ end
102
+ end
103
+ END1
104
+ class Foo
105
+ def initialize
106
+ @version = version
107
+ end
108
+ end
109
+ END2
110
+ end
111
+
112
+ it 'removes T.let assigned to a constant' do
113
+ verify(<<~END1, <<~END2)
114
+ class Foo
115
+ FOO = T.let('foo'.freeze, String)
116
+ end
117
+ END1
118
+ class Foo
119
+ FOO = 'foo'.freeze
120
+ end
121
+ END2
122
+ end
123
+
124
+ it 'removes type_members' do
125
+ verify(<<~END1, <<~END2)
126
+ class Foo
127
+ extend T::Generic
128
+
129
+ Elem = type_member
130
+ end
131
+ END1
132
+ class Foo
133
+ # extend T::Generic
134
+
135
+ # Elem = type_member
136
+ end
137
+ END2
138
+ end
139
+
140
+ it 'removes type_aliases' do
141
+ verify(<<~END1, <<~END2)
142
+ class Foo
143
+ AfterCallback = T.type_alias do
144
+ T.proc.params(cmd: T::Array[String], last_status: T.nilable(Process::Status)).void
145
+ end
146
+ end
147
+ END1
148
+ class Foo
149
+ # AfterCallback = T.type_alias do
150
+ # T.proc.params(cmd: T::Array[String], last_status: T.nilable(Process::Status)).void
151
+ # end
152
+ end
153
+ END2
154
+ end
155
+ end
@@ -0,0 +1,8 @@
1
+ $:.push(__dir__)
2
+
3
+ require 'rspec'
4
+ require 'curdle'
5
+ require 'pry-byebug'
6
+
7
+ RSpec.configure do |config|
8
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: curdle
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Programmatically remove Sorbet type annotations from Ruby code.
42
+ email:
43
+ - camertron@gmail.com
44
+ executables:
45
+ - curdle
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - Gemfile
50
+ - LICENSE
51
+ - Rakefile
52
+ - bin/curdle
53
+ - curdle.gemspec
54
+ - lib/curdle.rb
55
+ - lib/curdle/remove_sorbet.rb
56
+ - lib/curdle/tasks.rb
57
+ - lib/curdle/version.rb
58
+ - spec/curdle_spec.rb
59
+ - spec/spec_helper.rb
60
+ homepage: http://github.com/camertron/curdle
61
+ licenses: []
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.2.32
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Programmatically remove Sorbet type annotations from Ruby code.
82
+ test_files: []