tapioca 0.0.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.com/Shopify/tapioca.svg?token=AuiMGLmuYDrK2mb81pyq&branch=master)](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
|