tapioca 0.0.1 → 0.1.2
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 +7 -0
- data/Gemfile +11 -2
- data/README.md +51 -7
- data/Rakefile +11 -1
- data/exe/tapioca +6 -0
- data/lib/t.rb +50 -0
- data/lib/tapioca.rb +18 -1
- data/lib/tapioca/cli.rb +58 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +571 -0
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +100 -0
- data/lib/tapioca/compilers/symbol_table_compiler.rb +23 -0
- data/lib/tapioca/gemfile.rb +136 -0
- data/lib/tapioca/generator.rb +286 -0
- data/lib/tapioca/version.rb +4 -1
- metadata +170 -21
- data/.gitignore +0 -17
- data/LICENSE +0 -22
- data/tapioca.gemspec +0 -17
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 30b2f07033fc6b668d5f04c1a9dc33f6d06f776610b4e7051d89f8916695cb3f
|
4
|
+
data.tar.gz: 4b06d393137b287fd2b4c0a68955542ad9d9a3ab6be094c144ccc433b787c938
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 886c53802ecd357a27d13b045cae53ac2c14dd90e3003d0142f33e410d911f25fc882587e8e19fd44b3d80e03a08eaaa665657943936c6c154c49e44edb4e822
|
7
|
+
data.tar.gz: 3183720c7eaa9e89fdda702cbdaa1872eb7ae4ddf0b9b6ba329844a0db6ff29df7adaf2da33c2a92be73605b74eec7c639fa1dbf529f15a86d95d6ae26053bbf
|
data/Gemfile
CHANGED
@@ -1,4 +1,13 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source("https://rubygems.org")
|
2
4
|
|
3
|
-
# Specify your gem's dependencies in tapioca.gemspec
|
4
5
|
gemspec
|
6
|
+
|
7
|
+
group(:deployment) do
|
8
|
+
gem("package_cloud", "~> 0.2.33")
|
9
|
+
end
|
10
|
+
|
11
|
+
group(:deployment, :development) do
|
12
|
+
gem("rake", "~> 12.3")
|
13
|
+
end
|
data/README.md
CHANGED
@@ -1,17 +1,61 @@
|
|
1
1
|
# Tapioca
|
2
2
|
|
3
|
-
|
3
|
+
[](https://travis-ci.com/Shopify/tapioca)
|
4
|
+
|
5
|
+
Tapioca is a library used to generate RBI (Ruby interface) files for use with [Sorbet](https://sorbet.org). RBI files provide the structure (classes, modules, methods, parameters) of the gem/library to Sorbet to assist with typechecking.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
7
|
-
Add this line to your application's Gemfile
|
9
|
+
Add this line to your application's `Gemfile`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'tapioca', '~> 0.1.2', require: false
|
13
|
+
```
|
14
|
+
|
15
|
+
and do not forget to execute `tapioca` using `bundler`:
|
16
|
+
|
17
|
+
```shell
|
18
|
+
$ bundle exec tapioca
|
19
|
+
Commands:
|
20
|
+
tapioca bundle # sync RBIs to Gemfile
|
21
|
+
tapioca generate [gem...] # generate RBIs from gems
|
22
|
+
tapioca help [COMMAND] # Describe available commands or one specific command
|
23
|
+
|
24
|
+
Options:
|
25
|
+
--pre, -b, [--prerequire=file] # A file to be required before Bundler.require is called
|
26
|
+
--post, -a, [--postrequire=file] # A file to be required after Bundler.require is called
|
27
|
+
--out, -o, [--outdir=directory] # The output directory for generated RBI files
|
28
|
+
# Default: sorbet/rbi/gems
|
29
|
+
--cmd, -c, [--generate-command=command] # The command to run to regenerate RBI files
|
30
|
+
--typed, -t, [--typed-overrides=gem:level] # Overrides for typed sigils for generated gem RBIs
|
31
|
+
```
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
### Generate for gems
|
36
|
+
|
37
|
+
Command: `tapioca generate [gems...]`
|
38
|
+
|
39
|
+
This will generate RBIs for the specified gems and place them in the RBI directory.
|
40
|
+
|
41
|
+
### Generate for all gems in Gemfile
|
42
|
+
|
43
|
+
Command: `tapioca bundle`
|
44
|
+
|
45
|
+
This will sync the RBIs with the gems in the Gemfile and will add, update, and remove RBIs as necessary.
|
46
|
+
|
47
|
+
### Flags
|
8
48
|
|
9
|
-
|
49
|
+
- `--prerequire [file]`: A file to be required before `Bundler.require` is called.
|
50
|
+
- `--postrequire [file]`: A file to be required after `Bundler.require` is called.
|
51
|
+
- `--out [directory]`: The output directory for generated RBI files, default to `sorbet/rbi/gems`.
|
52
|
+
- `--generate_command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
|
53
|
+
- `--typed_overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
|
10
54
|
|
11
|
-
|
55
|
+
## Contributing
|
12
56
|
|
13
|
-
|
57
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
14
58
|
|
15
|
-
|
59
|
+
## License
|
16
60
|
|
17
|
-
|
61
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,2 +1,12 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
9
|
+
rescue LoadError # rubocop:disable Lint/HandleExceptions
|
10
|
+
end
|
11
|
+
|
12
|
+
task(default: :spec)
|
data/exe/tapioca
ADDED
data/lib/t.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
unless defined?(T)
|
2
|
+
module T
|
3
|
+
def self.any(type_a, type_b, *types); end
|
4
|
+
def self.nilable(type); end
|
5
|
+
def self.untyped; end
|
6
|
+
def self.noreturn; end
|
7
|
+
def self.all(type_a, type_b, *types); end
|
8
|
+
def self.enum(values); end
|
9
|
+
def self.proc; end
|
10
|
+
def self.self_type; end
|
11
|
+
def self.class_of(klass); end
|
12
|
+
def self.type_alias(type); end
|
13
|
+
def self.type_parameter(name); end
|
14
|
+
|
15
|
+
def self.cast(value, type, checked: true); value; end
|
16
|
+
def self.let(value, type, checked: true); value; end
|
17
|
+
def self.assert_type!(value, type, checked: true); value; end
|
18
|
+
def self.unsafe(value); value; end
|
19
|
+
def self.must(arg, msg=nil); arg; end
|
20
|
+
def self.reveal_type(value); value; end
|
21
|
+
end
|
22
|
+
|
23
|
+
module T::Sig
|
24
|
+
def sig(&blk); end
|
25
|
+
end
|
26
|
+
|
27
|
+
module T::Array
|
28
|
+
def self.[](type); end
|
29
|
+
end
|
30
|
+
|
31
|
+
module T::Hash
|
32
|
+
def self.[](keys, values); end
|
33
|
+
end
|
34
|
+
|
35
|
+
module T::Enumerable
|
36
|
+
def self.[](type); end
|
37
|
+
end
|
38
|
+
|
39
|
+
module T::Range
|
40
|
+
def self.[](type); end
|
41
|
+
end
|
42
|
+
|
43
|
+
module T::Set
|
44
|
+
def self.[](type); end
|
45
|
+
end
|
46
|
+
|
47
|
+
module T::Boolean
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/lib/tapioca.rb
CHANGED
@@ -1,4 +1,21 @@
|
|
1
|
-
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "zeitwerk"
|
5
|
+
require_relative "./t"
|
6
|
+
|
7
|
+
loader = Zeitwerk::Loader.for_gem
|
8
|
+
loader.setup
|
9
|
+
loader.eager_load
|
2
10
|
|
3
11
|
module Tapioca
|
12
|
+
def self.silence_warnings
|
13
|
+
original_verbosity = $VERBOSE
|
14
|
+
$VERBOSE = nil
|
15
|
+
yield
|
16
|
+
ensure
|
17
|
+
$VERBOSE = original_verbosity
|
18
|
+
end
|
19
|
+
|
20
|
+
class Error < StandardError; end
|
4
21
|
end
|
data/lib/tapioca/cli.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
class Cli < Thor
|
8
|
+
class_option :prerequire,
|
9
|
+
aliases: ["--pre", "-b"],
|
10
|
+
banner: "file",
|
11
|
+
desc: "A file to be required before Bundler.require is called"
|
12
|
+
class_option :postrequire,
|
13
|
+
aliases: ["--post", "-a"],
|
14
|
+
banner: "file",
|
15
|
+
desc: "A file to be required after Bundler.require is called"
|
16
|
+
class_option :outdir,
|
17
|
+
aliases: ["--out", "-o"],
|
18
|
+
default: Generator::DEFAULT_OUTDIR,
|
19
|
+
banner: "directory",
|
20
|
+
desc: "The output directory for generated RBI files"
|
21
|
+
class_option :generate_command,
|
22
|
+
aliases: ["--cmd", "-c"],
|
23
|
+
banner: "command",
|
24
|
+
desc: "The command to run to regenerate RBI files"
|
25
|
+
class_option :typed_overrides,
|
26
|
+
aliases: ["--typed", "-t"],
|
27
|
+
type: :hash,
|
28
|
+
default: {},
|
29
|
+
banner: "gem:level",
|
30
|
+
desc: "Overrides for typed sigils for generated gem RBIs"
|
31
|
+
|
32
|
+
desc "generate [gem...]", "generate RBIs from gems"
|
33
|
+
def generate(*gems)
|
34
|
+
Tapioca.silence_warnings do
|
35
|
+
generator.build_gem_rbis(gems)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "bundle", "sync RBIs to Gemfile"
|
40
|
+
def bundle
|
41
|
+
Tapioca.silence_warnings do
|
42
|
+
generator.sync_rbis_with_gemfile
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
no_commands do
|
47
|
+
def generator
|
48
|
+
@generator ||= Generator.new(
|
49
|
+
outdir: options[:outdir],
|
50
|
+
prerequire: options[:prerequire],
|
51
|
+
postrequire: options[:postrequire],
|
52
|
+
command: options[:generate_command],
|
53
|
+
typed_overrides: options[:typed_overrides]
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,571 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
module Compilers
|
8
|
+
module SymbolTable
|
9
|
+
class SymbolGenerator
|
10
|
+
extend(T::Sig)
|
11
|
+
|
12
|
+
IGNORED_SYMBOLS = %w{
|
13
|
+
YAML
|
14
|
+
MiniTest
|
15
|
+
Mutex
|
16
|
+
}
|
17
|
+
|
18
|
+
attr_reader(:gem, :indent)
|
19
|
+
|
20
|
+
sig { params(gem: Gemfile::Gem, indent: Integer).void }
|
21
|
+
def initialize(gem, indent = 0)
|
22
|
+
@gem = gem
|
23
|
+
@indent = indent
|
24
|
+
@seen = Set.new
|
25
|
+
@alias_namespace ||= Set.new
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(String) }
|
29
|
+
def generate
|
30
|
+
symbols
|
31
|
+
.sort
|
32
|
+
.map(&method(:generate_from_symbol))
|
33
|
+
.compact
|
34
|
+
.join("\n\n")
|
35
|
+
.concat("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
sig { returns(T::Set[String]) }
|
41
|
+
def symbols
|
42
|
+
symbols = Tapioca::Compilers::SymbolTable::SymbolLoader.list_from_paths(gem.files)
|
43
|
+
symbols.union(engine_symbols(symbols))
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(symbols: T::Set[String]).returns(T::Set[String]) }
|
47
|
+
def engine_symbols(symbols)
|
48
|
+
return Set.new unless Object.const_defined?("Rails::Engine")
|
49
|
+
|
50
|
+
engine = Object.const_get("Rails::Engine")
|
51
|
+
.descendants.reject(&:abstract_railtie?)
|
52
|
+
.find do |klass|
|
53
|
+
name = name_of(klass)
|
54
|
+
!name.nil? && symbols.include?(name)
|
55
|
+
end
|
56
|
+
|
57
|
+
return Set.new unless engine
|
58
|
+
|
59
|
+
paths = engine.config.eager_load_paths.flat_map do |load_path|
|
60
|
+
Pathname.glob("#{load_path}/**/*.rb")
|
61
|
+
end
|
62
|
+
|
63
|
+
Tapioca::Compilers::SymbolTable::SymbolLoader.list_from_paths(paths)
|
64
|
+
rescue
|
65
|
+
Set.new
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { params(symbol: String).returns(T.nilable(String)) }
|
69
|
+
def generate_from_symbol(symbol)
|
70
|
+
constant = resolve_constant(symbol)
|
71
|
+
|
72
|
+
return unless constant
|
73
|
+
|
74
|
+
compile(symbol, constant)
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { params(symbol: String).returns(BasicObject) }
|
78
|
+
def resolve_constant(symbol)
|
79
|
+
Object.const_get(symbol, false)
|
80
|
+
rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { params(name: T.nilable(String), constant: BasicObject).returns(T.nilable(String)) }
|
85
|
+
def compile(name, constant)
|
86
|
+
return unless constant
|
87
|
+
return unless name
|
88
|
+
return if name.strip.empty?
|
89
|
+
return if name.start_with?('#<')
|
90
|
+
return if name.downcase == name
|
91
|
+
return if alias_namespaced?(name)
|
92
|
+
return if seen?(name)
|
93
|
+
return unless parent_declares_constant?(name)
|
94
|
+
|
95
|
+
mark_seen(name)
|
96
|
+
compile_constant(name, constant)
|
97
|
+
end
|
98
|
+
|
99
|
+
sig { params(name: String, constant: BasicObject).returns(T.nilable(String)) }
|
100
|
+
def compile_constant(name, constant)
|
101
|
+
case constant
|
102
|
+
when Module
|
103
|
+
if name_of(constant) != name
|
104
|
+
compile_alias(name, constant)
|
105
|
+
else
|
106
|
+
compile_module(name, constant)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
compile_object(name, constant)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
114
|
+
def compile_alias(name, constant)
|
115
|
+
return if symbol_ignored?(name)
|
116
|
+
|
117
|
+
constant_name = name_of(constant)
|
118
|
+
add_to_alias_namespace(name)
|
119
|
+
|
120
|
+
return if IGNORED_SYMBOLS.include?(name)
|
121
|
+
|
122
|
+
indented("#{name} = #{constant_name}")
|
123
|
+
end
|
124
|
+
|
125
|
+
sig { params(name: String, value: BasicObject).returns(T.nilable(String)) }
|
126
|
+
def compile_object(name, value)
|
127
|
+
indented("#{name} = T.let(T.unsafe(nil), #{type_name_of(value)})")
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { params(value: BasicObject).returns(String) }
|
131
|
+
def type_name_of(value)
|
132
|
+
klass = class_of(value)
|
133
|
+
|
134
|
+
type_name = public_module?(klass) && name_of(klass) || "T.untyped"
|
135
|
+
# Range needs to be processed separately to be put in the T::Range[] form
|
136
|
+
type_name = "T::Range[#{type_name_of(T.cast(value, T::Range[T.untyped]).first)}]" if klass == Range
|
137
|
+
|
138
|
+
type_name
|
139
|
+
end
|
140
|
+
|
141
|
+
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
142
|
+
def compile_module(name, constant)
|
143
|
+
return unless public_module?(constant)
|
144
|
+
return unless defined_in_gem?(constant, strict: false)
|
145
|
+
|
146
|
+
header =
|
147
|
+
if constant.is_a?(Class)
|
148
|
+
indented("class #{name}#{compile_superclass(constant)}")
|
149
|
+
else
|
150
|
+
indented("module #{name}")
|
151
|
+
end
|
152
|
+
|
153
|
+
body = compile_body(name, constant)
|
154
|
+
|
155
|
+
return if symbol_ignored?(name) && body.nil?
|
156
|
+
|
157
|
+
[
|
158
|
+
header,
|
159
|
+
body,
|
160
|
+
indented("end"),
|
161
|
+
compile_subconstants(name, constant),
|
162
|
+
].select { |b| !b.nil? && b.strip != "" }.join("\n")
|
163
|
+
end
|
164
|
+
|
165
|
+
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
166
|
+
def compile_body(name, constant)
|
167
|
+
with_indentation do
|
168
|
+
methods = compile_methods(name, constant)
|
169
|
+
|
170
|
+
return if symbol_ignored?(name) && methods.nil?
|
171
|
+
|
172
|
+
[
|
173
|
+
compile_mixins(constant),
|
174
|
+
methods,
|
175
|
+
].select { |b| b != "" }.join("\n\n")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
180
|
+
def compile_subconstants(name, constant)
|
181
|
+
output = constants_of(constant).sort.uniq.map do |constant_name|
|
182
|
+
symbol = (name == "Object" ? "" : name) + "::#{constant_name}"
|
183
|
+
subconstant = resolve_constant(symbol)
|
184
|
+
|
185
|
+
# Don't compile modules of Object because Object::Foo == Foo
|
186
|
+
# Don't compile modules of BasicObject because BasicObject::BasicObject == BasicObject
|
187
|
+
next if (Object == constant || BasicObject == constant) && Module === subconstant
|
188
|
+
next unless subconstant
|
189
|
+
|
190
|
+
compile(symbol, subconstant)
|
191
|
+
end.compact
|
192
|
+
|
193
|
+
return "" if output.empty?
|
194
|
+
|
195
|
+
"\n" + output.join("\n\n")
|
196
|
+
end
|
197
|
+
|
198
|
+
sig { params(constant: Class).returns(String) }
|
199
|
+
def compile_superclass(constant)
|
200
|
+
superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
|
201
|
+
|
202
|
+
while (superclass = superclass_of(constant))
|
203
|
+
constant_name = name_of(constant)
|
204
|
+
constant = superclass
|
205
|
+
|
206
|
+
# Some classes have superclasses that are private constants
|
207
|
+
# so if we generate code with that superclass, the output
|
208
|
+
# will not be compilable (since private constants are not
|
209
|
+
# publicly visible).
|
210
|
+
#
|
211
|
+
# So we skip superclasses that are not public and walk up the
|
212
|
+
# chain.
|
213
|
+
next unless public_module?(superclass)
|
214
|
+
|
215
|
+
# Some types have "themselves" as their superclass
|
216
|
+
# which can happen via:
|
217
|
+
#
|
218
|
+
# class A < Numeric; end
|
219
|
+
# A = Class.new(A)
|
220
|
+
# A.superclass #=> A
|
221
|
+
#
|
222
|
+
# We compare names here to make sure we skip those
|
223
|
+
# superclass instances and walk up the chain.
|
224
|
+
#
|
225
|
+
# The name comparison is against the name of the constant
|
226
|
+
# resolved from the name of the superclass, since
|
227
|
+
# this is also possible:
|
228
|
+
#
|
229
|
+
# B = Class.new
|
230
|
+
# class A < B; end
|
231
|
+
# B = A
|
232
|
+
# A.superclass.name #=> "B"
|
233
|
+
# B #=> A
|
234
|
+
superclass_name = T.must(name_of(superclass))
|
235
|
+
resolved_superclass = resolve_constant(superclass_name)
|
236
|
+
next unless Module === resolved_superclass
|
237
|
+
next if name_of(resolved_superclass) == constant_name
|
238
|
+
|
239
|
+
# We found a suitable superclass
|
240
|
+
break
|
241
|
+
end
|
242
|
+
|
243
|
+
return "" if superclass == ::Object || superclass == ::Delegator
|
244
|
+
return "" if superclass.nil?
|
245
|
+
|
246
|
+
name = name_of(superclass)
|
247
|
+
return "" if name.nil? || name.empty?
|
248
|
+
|
249
|
+
" < ::#{name}"
|
250
|
+
end
|
251
|
+
|
252
|
+
sig { params(constant: Module).returns(String) }
|
253
|
+
def compile_mixins(constant)
|
254
|
+
ignorable_ancestors =
|
255
|
+
if constant.is_a?(Class)
|
256
|
+
ancestors = constant.superclass&.ancestors || Object.ancestors
|
257
|
+
Set.new(ancestors)
|
258
|
+
else
|
259
|
+
Module.ancestors
|
260
|
+
end
|
261
|
+
|
262
|
+
inherited_singleton_class_ancestors =
|
263
|
+
if constant.is_a?(Class)
|
264
|
+
Set.new(constant.superclass.singleton_class.ancestors)
|
265
|
+
else
|
266
|
+
Module.ancestors
|
267
|
+
end
|
268
|
+
|
269
|
+
interesting_ancestors =
|
270
|
+
constant.ancestors.reject { |mod| ignorable_ancestors.include?(mod) }
|
271
|
+
|
272
|
+
prepend = interesting_ancestors.take_while { |c| !are_equal?(constant, c) }
|
273
|
+
include = interesting_ancestors.drop(prepend.size + 1)
|
274
|
+
extend = constant.singleton_class.ancestors
|
275
|
+
.reject do |mod|
|
276
|
+
mod == constant.singleton_class ||
|
277
|
+
inherited_singleton_class_ancestors.include?(mod) ||
|
278
|
+
!public_module?(mod) ||
|
279
|
+
Module != class_of(mod)
|
280
|
+
end
|
281
|
+
|
282
|
+
prepends = prepend
|
283
|
+
.select(&method(:name_of))
|
284
|
+
.select(&method(:public_module?))
|
285
|
+
.map do |mod|
|
286
|
+
# TODO: Sorbet currently does not handle prepend
|
287
|
+
# properly for method resolution, so we generate an
|
288
|
+
# include statement instead
|
289
|
+
indented("include(#{qualified_name_of(mod)})")
|
290
|
+
end
|
291
|
+
|
292
|
+
includes = include
|
293
|
+
.select(&method(:name_of))
|
294
|
+
.select(&method(:public_module?))
|
295
|
+
.map do |mod|
|
296
|
+
indented("include(#{qualified_name_of(mod)})")
|
297
|
+
end
|
298
|
+
|
299
|
+
extends = extend
|
300
|
+
.select(&method(:name_of))
|
301
|
+
.select(&method(:public_module?))
|
302
|
+
.map do |mod|
|
303
|
+
indented("extend(#{qualified_name_of(mod)})")
|
304
|
+
end
|
305
|
+
|
306
|
+
mixes_class_methods = extend
|
307
|
+
.select do |mod|
|
308
|
+
qualified_name_of(mod) == "::ActiveSupport::Concern" &&
|
309
|
+
Module === resolve_constant("#{name_of(constant)}::ClassMethods")
|
310
|
+
end
|
311
|
+
.first(1)
|
312
|
+
.flat_map do
|
313
|
+
["", indented("mixes_in_class_methods(ClassMethods)")]
|
314
|
+
end
|
315
|
+
|
316
|
+
(prepends + includes + extends + mixes_class_methods).join("\n")
|
317
|
+
end
|
318
|
+
|
319
|
+
sig { params(name: String, constant: Module).returns(T.nilable(String)) }
|
320
|
+
def compile_methods(name, constant)
|
321
|
+
initialize_method = compile_method(
|
322
|
+
name,
|
323
|
+
constant,
|
324
|
+
initialize_method_for(constant)
|
325
|
+
)
|
326
|
+
|
327
|
+
instance_methods = compile_directly_owned_methods(name, constant)
|
328
|
+
singleton_methods = compile_directly_owned_methods(name, constant.singleton_class, [:public])
|
329
|
+
|
330
|
+
return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
|
331
|
+
|
332
|
+
[
|
333
|
+
initialize_method || "",
|
334
|
+
instance_methods,
|
335
|
+
singleton_methods,
|
336
|
+
].select { |b| b.strip != "" }.join("\n\n")
|
337
|
+
end
|
338
|
+
|
339
|
+
sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
|
340
|
+
def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
|
341
|
+
method_names_by_visibility(mod)
|
342
|
+
.delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
|
343
|
+
.flat_map do |visibility, method_list|
|
344
|
+
compiled = method_list.sort!.map do |name|
|
345
|
+
next if name == :initialize
|
346
|
+
compile_method(module_name, mod, mod.instance_method(name))
|
347
|
+
end
|
348
|
+
compiled.compact!
|
349
|
+
|
350
|
+
unless compiled.empty? || visibility == :public
|
351
|
+
# add visibility badge
|
352
|
+
compiled.unshift('', indented(visibility.to_s), '')
|
353
|
+
end
|
354
|
+
|
355
|
+
compiled
|
356
|
+
end
|
357
|
+
.compact
|
358
|
+
.join("\n")
|
359
|
+
end
|
360
|
+
|
361
|
+
sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
|
362
|
+
def method_names_by_visibility(mod)
|
363
|
+
{
|
364
|
+
public: Module.instance_method(:public_instance_methods).bind(mod).call,
|
365
|
+
protected: Module.instance_method(:protected_instance_methods).bind(mod).call,
|
366
|
+
private: Module.instance_method(:private_instance_methods).bind(mod).call,
|
367
|
+
}
|
368
|
+
end
|
369
|
+
|
370
|
+
sig do
|
371
|
+
params(
|
372
|
+
symbol_name: String,
|
373
|
+
constant: Module,
|
374
|
+
method: T.nilable(UnboundMethod)
|
375
|
+
).returns(T.nilable(String))
|
376
|
+
end
|
377
|
+
def compile_method(symbol_name, constant, method)
|
378
|
+
return unless method
|
379
|
+
return unless method.owner == constant
|
380
|
+
return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
|
381
|
+
|
382
|
+
method_name = method.name.to_s
|
383
|
+
return unless valid_method_name?(method_name)
|
384
|
+
|
385
|
+
params = T.let(method.parameters, T::Array[T::Array[Symbol]])
|
386
|
+
parameters = params.map do |(type, name)|
|
387
|
+
name ||= :_
|
388
|
+
|
389
|
+
case type
|
390
|
+
when :req
|
391
|
+
name.to_s
|
392
|
+
when :opt
|
393
|
+
"#{name} = _"
|
394
|
+
when :rest
|
395
|
+
"*#{name}"
|
396
|
+
when :keyreq
|
397
|
+
"#{name}:"
|
398
|
+
when :key
|
399
|
+
"#{name}: _"
|
400
|
+
when :keyrest
|
401
|
+
"**#{name}"
|
402
|
+
when :block
|
403
|
+
"&#{name}"
|
404
|
+
end
|
405
|
+
end.join(', ')
|
406
|
+
|
407
|
+
method_name.prepend(constant.singleton_class? ? 'self.' : '')
|
408
|
+
parameters = "(#{parameters})" if parameters != ""
|
409
|
+
|
410
|
+
indented("def #{method_name}#{parameters}; end")
|
411
|
+
end
|
412
|
+
|
413
|
+
sig { params(symbol_name: String).returns(T::Boolean) }
|
414
|
+
def symbol_ignored?(symbol_name)
|
415
|
+
SymbolLoader.ignore_symbol?(symbol_name)
|
416
|
+
end
|
417
|
+
|
418
|
+
sig { params(path: String).returns(T::Boolean) }
|
419
|
+
def path_in_gem?(path)
|
420
|
+
path.start_with?(gem.full_gem_path)
|
421
|
+
end
|
422
|
+
|
423
|
+
SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]
|
424
|
+
|
425
|
+
sig { params(name: String).returns(T::Boolean) }
|
426
|
+
def valid_method_name?(name)
|
427
|
+
return true if SPECIAL_METHOD_NAMES.include?(name)
|
428
|
+
!!name.match(/^[[:word:]]+[?!=]?$/)
|
429
|
+
end
|
430
|
+
|
431
|
+
sig do
|
432
|
+
type_parameters(:U)
|
433
|
+
.params(
|
434
|
+
_blk: T.proc
|
435
|
+
.returns(T.type_parameter(:U))
|
436
|
+
)
|
437
|
+
.returns(T.type_parameter(:U))
|
438
|
+
end
|
439
|
+
def with_indentation(&_blk)
|
440
|
+
@indent += 2
|
441
|
+
yield
|
442
|
+
ensure
|
443
|
+
@indent -= 2
|
444
|
+
end
|
445
|
+
|
446
|
+
sig { params(str: String).returns(String) }
|
447
|
+
def indented(str)
|
448
|
+
" " * @indent + str
|
449
|
+
end
|
450
|
+
|
451
|
+
sig { params(method: UnboundMethod).returns(T::Boolean) }
|
452
|
+
def method_in_gem?(method)
|
453
|
+
source_location = method.source_location&.first
|
454
|
+
return false if source_location.nil?
|
455
|
+
|
456
|
+
path_in_gem?(source_location)
|
457
|
+
end
|
458
|
+
|
459
|
+
sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
|
460
|
+
def defined_in_gem?(constant, strict: true)
|
461
|
+
files = get_file_candidates(constant)
|
462
|
+
|
463
|
+
return !strict if files.empty?
|
464
|
+
|
465
|
+
files.any? do |file|
|
466
|
+
path_in_gem?(file)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
sig { params(constant: Module).returns(T::Array[String]) }
|
471
|
+
def get_file_candidates(constant)
|
472
|
+
wrapped_module = Pry::WrappedModule.new(constant)
|
473
|
+
|
474
|
+
wrapped_module.candidates.map(&:file).to_a.compact
|
475
|
+
rescue ArgumentError, NameError
|
476
|
+
[]
|
477
|
+
end
|
478
|
+
|
479
|
+
sig { params(name: String).void }
|
480
|
+
def add_to_alias_namespace(name)
|
481
|
+
@alias_namespace.add("#{name}::")
|
482
|
+
end
|
483
|
+
|
484
|
+
sig { params(name: String).returns(T::Boolean) }
|
485
|
+
def alias_namespaced?(name)
|
486
|
+
@alias_namespace.any? do |namespace|
|
487
|
+
name.start_with?(namespace)
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
sig { params(name: String).void }
|
492
|
+
def mark_seen(name)
|
493
|
+
@seen.add(name)
|
494
|
+
end
|
495
|
+
|
496
|
+
sig { params(name: String).returns(T::Boolean) }
|
497
|
+
def seen?(name)
|
498
|
+
@seen.include?(name)
|
499
|
+
end
|
500
|
+
|
501
|
+
def initialize_method_for(constant)
|
502
|
+
constant.instance_method(:initialize)
|
503
|
+
rescue
|
504
|
+
nil
|
505
|
+
end
|
506
|
+
|
507
|
+
def parent_declares_constant?(name)
|
508
|
+
name_parts = name.split("::")
|
509
|
+
|
510
|
+
parent_name = name_parts[0...-1].join("::").delete_prefix("::")
|
511
|
+
parent_name = 'Object' if parent_name == ""
|
512
|
+
parent = T.cast(resolve_constant(parent_name), T.nilable(Module))
|
513
|
+
|
514
|
+
return false unless parent
|
515
|
+
|
516
|
+
constants_of(parent).include?(name_parts.last.to_sym)
|
517
|
+
end
|
518
|
+
|
519
|
+
sig { params(constant: Module).returns(T::Boolean) }
|
520
|
+
def public_module?(constant)
|
521
|
+
constant_name = name_of(constant)
|
522
|
+
return false unless constant_name
|
523
|
+
|
524
|
+
begin
|
525
|
+
# can't use !! here because the constant might override ! and mess with us
|
526
|
+
Module === eval(constant_name) # rubocop:disable Security/Eval
|
527
|
+
rescue NameError
|
528
|
+
false
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
sig { params(constant: BasicObject).returns(Class) }
|
533
|
+
def class_of(constant)
|
534
|
+
Kernel.instance_method(:class).bind(constant).call
|
535
|
+
end
|
536
|
+
|
537
|
+
sig { params(constant: Module).returns(T::Array[Symbol]) }
|
538
|
+
def constants_of(constant)
|
539
|
+
Module.instance_method(:constants).bind(constant).call(false)
|
540
|
+
end
|
541
|
+
|
542
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
543
|
+
def name_of(constant)
|
544
|
+
name = Module.instance_method(:name).bind(constant).call
|
545
|
+
return if name.nil?
|
546
|
+
return unless are_equal?(constant, resolve_constant(name))
|
547
|
+
name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
|
548
|
+
name
|
549
|
+
end
|
550
|
+
|
551
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
552
|
+
def qualified_name_of(constant)
|
553
|
+
name = name_of(constant)
|
554
|
+
return if name.nil?
|
555
|
+
name.prepend("::") unless name.start_with?("::")
|
556
|
+
name
|
557
|
+
end
|
558
|
+
|
559
|
+
sig { params(constant: Class).returns(T.nilable(Class)) }
|
560
|
+
def superclass_of(constant)
|
561
|
+
Class.instance_method(:superclass).bind(constant).call
|
562
|
+
end
|
563
|
+
|
564
|
+
sig { params(constant: Module, other: BasicObject).returns(T::Boolean) }
|
565
|
+
def are_equal?(constant, other)
|
566
|
+
BasicObject.instance_method(:equal?).bind(constant).call(other)
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|