tapioca 0.3.1 → 0.4.4
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 +4 -4
- data/Gemfile +25 -1
- data/README.md +23 -2
- data/Rakefile +15 -4
- data/lib/tapioca.rb +8 -2
- data/lib/tapioca/cli.rb +32 -3
- data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
- data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +267 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +393 -0
- data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
- data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
- data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
- data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
- data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
- data/lib/tapioca/compilers/dsl/base.rb +165 -0
- data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
- data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
- data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
- data/lib/tapioca/compilers/dsl/url_helpers.rb +92 -0
- data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
- data/lib/tapioca/compilers/requires_compiler.rb +67 -0
- data/lib/tapioca/compilers/sorbet.rb +34 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +171 -26
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -20
- data/lib/tapioca/compilers/todos_compiler.rb +32 -0
- data/lib/tapioca/config.rb +14 -6
- data/lib/tapioca/config_builder.rb +22 -9
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +32 -9
- data/lib/tapioca/generator.rb +231 -23
- data/lib/tapioca/loader.rb +30 -9
- data/lib/tapioca/version.rb +1 -1
- metadata +32 -39
@@ -2,16 +2,12 @@
|
|
2
2
|
# typed: true
|
3
3
|
|
4
4
|
require 'json'
|
5
|
-
require 'pathname'
|
6
5
|
require 'tempfile'
|
7
|
-
require 'shellwords'
|
8
6
|
|
9
7
|
module Tapioca
|
10
8
|
module Compilers
|
11
9
|
module SymbolTable
|
12
10
|
module SymbolLoader
|
13
|
-
SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
|
14
|
-
|
15
11
|
class << self
|
16
12
|
extend(T::Sig)
|
17
13
|
|
@@ -53,22 +49,7 @@ module Tapioca
|
|
53
49
|
end
|
54
50
|
|
55
51
|
def symbol_table_json_from(input, table_type: "symbol-table-json")
|
56
|
-
|
57
|
-
[
|
58
|
-
sorbet_path,
|
59
|
-
# We don't want to pick up any sorbet/config files in cwd
|
60
|
-
"--no-config",
|
61
|
-
"--print=#{table_type}",
|
62
|
-
"--quiet",
|
63
|
-
input,
|
64
|
-
].join(' '),
|
65
|
-
err: "/dev/null"
|
66
|
-
).read
|
67
|
-
end
|
68
|
-
|
69
|
-
sig { returns(String) }
|
70
|
-
def sorbet_path
|
71
|
-
SORBET.to_s.shellescape
|
52
|
+
Tapioca::Compilers::Sorbet.run("--no-config", "--print=#{table_type}", input)
|
72
53
|
end
|
73
54
|
end
|
74
55
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strong
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Compilers
|
6
|
+
# Taken from https://github.com/sorbet/sorbet/blob/master/gems/sorbet/lib/todo-rbi.rb
|
7
|
+
class TodosCompiler
|
8
|
+
extend(T::Sig)
|
9
|
+
|
10
|
+
sig do
|
11
|
+
returns(String)
|
12
|
+
end
|
13
|
+
def compile
|
14
|
+
list_todos.each_line.map do |line|
|
15
|
+
next if line.include?("<") || line.include?("class_of")
|
16
|
+
"module #{line.strip.gsub('T.untyped::', '')}; end"
|
17
|
+
end.compact.join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
sig { returns(String) }
|
23
|
+
def list_todos
|
24
|
+
Tapioca::Compilers::Sorbet.run(
|
25
|
+
"--print=missing-constants",
|
26
|
+
"--stdout-hup-hack",
|
27
|
+
"--no-error-count"
|
28
|
+
).strip
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/tapioca/config.rb
CHANGED
@@ -11,20 +11,28 @@ module Tapioca
|
|
11
11
|
const(:generate_command, String)
|
12
12
|
const(:exclude, T::Array[String])
|
13
13
|
const(:typed_overrides, T::Hash[String, String])
|
14
|
+
const(:todos_path, String)
|
15
|
+
const(:generators, T::Array[String])
|
14
16
|
|
15
17
|
sig { returns(Pathname) }
|
16
18
|
def outpath
|
17
|
-
@outpath
|
18
|
-
|
19
|
+
@outpath = T.let(@outpath, T.nilable(Pathname))
|
20
|
+
@outpath ||= Pathname.new(outdir)
|
19
21
|
end
|
20
22
|
|
21
23
|
private_class_method :new
|
22
24
|
|
23
|
-
|
24
|
-
SORBET_CONFIG = "
|
25
|
+
SORBET_PATH = T.let("sorbet", String)
|
26
|
+
SORBET_CONFIG = T.let("#{SORBET_PATH}/config", String)
|
27
|
+
TAPIOCA_PATH = T.let("#{SORBET_PATH}/tapioca", String)
|
28
|
+
TAPIOCA_CONFIG = T.let("#{TAPIOCA_PATH}/config.yml", String)
|
29
|
+
|
30
|
+
DEFAULT_POSTREQUIRE = T.let("#{TAPIOCA_PATH}/require.rb", String)
|
31
|
+
DEFAULT_RBIDIR = T.let("#{SORBET_PATH}/rbi", String)
|
32
|
+
DEFAULT_DSLDIR = T.let("#{DEFAULT_RBIDIR}/dsl", String)
|
33
|
+
DEFAULT_GEMDIR = T.let("#{DEFAULT_RBIDIR}/gems", String)
|
34
|
+
DEFAULT_TODOSPATH = T.let("#{DEFAULT_RBIDIR}/todo.rbi", String)
|
25
35
|
|
26
|
-
DEFAULT_POSTREQUIRE = "sorbet/tapioca/require.rb"
|
27
|
-
DEFAULT_OUTDIR = "sorbet/rbi/gems"
|
28
36
|
DEFAULT_OVERRIDES = T.let({
|
29
37
|
# ActiveSupport overrides some core methods with different signatures
|
30
38
|
# so we generate a typed: false RBI for it to suppress errors
|
@@ -1,15 +1,17 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require 'yaml'
|
5
|
+
|
4
6
|
module Tapioca
|
5
7
|
class ConfigBuilder
|
6
8
|
class << self
|
7
9
|
extend(T::Sig)
|
8
10
|
|
9
|
-
sig { params(options: T::Hash[String, T.untyped]).returns(Config) }
|
10
|
-
def from_options(options)
|
11
|
+
sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
|
12
|
+
def from_options(command, options)
|
11
13
|
Config.from_hash(
|
12
|
-
merge_options(default_options, config_options, options)
|
14
|
+
merge_options(default_options(command), config_options, options)
|
13
15
|
)
|
14
16
|
end
|
15
17
|
|
@@ -17,16 +19,25 @@ module Tapioca
|
|
17
19
|
|
18
20
|
sig { returns(T::Hash[String, T.untyped]) }
|
19
21
|
def config_options
|
20
|
-
if File.exist?(Config::
|
21
|
-
YAML.load_file(Config::
|
22
|
+
if File.exist?(Config::TAPIOCA_CONFIG)
|
23
|
+
YAML.load_file(Config::TAPIOCA_CONFIG, fallback: {})
|
22
24
|
else
|
23
25
|
{}
|
24
26
|
end
|
25
27
|
end
|
26
28
|
|
27
|
-
sig { returns(T::Hash[String, T.untyped]) }
|
28
|
-
def default_options
|
29
|
-
|
29
|
+
sig { params(command: Symbol).returns(T::Hash[String, T.untyped]) }
|
30
|
+
def default_options(command)
|
31
|
+
default_outdir = case command
|
32
|
+
when :sync, :generate
|
33
|
+
Config::DEFAULT_GEMDIR
|
34
|
+
when :dsl
|
35
|
+
Config::DEFAULT_DSLDIR
|
36
|
+
else
|
37
|
+
Config::SORBET_PATH
|
38
|
+
end
|
39
|
+
|
40
|
+
DEFAULT_OPTIONS.merge("outdir" => default_outdir)
|
30
41
|
end
|
31
42
|
|
32
43
|
sig { returns(String) }
|
@@ -53,10 +64,12 @@ module Tapioca
|
|
53
64
|
|
54
65
|
DEFAULT_OPTIONS = T.let({
|
55
66
|
"postrequire" => Config::DEFAULT_POSTREQUIRE,
|
56
|
-
"outdir" =>
|
67
|
+
"outdir" => nil,
|
57
68
|
"generate_command" => default_command,
|
58
69
|
"exclude" => [],
|
59
70
|
"typed_overrides" => Config::DEFAULT_OVERRIDES,
|
71
|
+
"todos_path" => Config::DEFAULT_TODOSPATH,
|
72
|
+
"generators" => [],
|
60
73
|
}.freeze, T::Hash[String, T.untyped])
|
61
74
|
end
|
62
75
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Class
|
5
|
+
# Returns an array with all classes that are < than its receiver.
|
6
|
+
#
|
7
|
+
# class C; end
|
8
|
+
# C.descendants # => []
|
9
|
+
#
|
10
|
+
# class B < C; end
|
11
|
+
# C.descendants # => [B]
|
12
|
+
#
|
13
|
+
# class A < B; end
|
14
|
+
# C.descendants # => [B, A]
|
15
|
+
#
|
16
|
+
# class D < C; end
|
17
|
+
# C.descendants # => [B, A, D]
|
18
|
+
def descendants
|
19
|
+
ObjectSpace.each_object(singleton_class).reject do |k|
|
20
|
+
k.singleton_class? || k == self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/tapioca/gemfile.rb
CHANGED
@@ -28,7 +28,7 @@ module Tapioca
|
|
28
28
|
sig { returns(T::Array[Gem]) }
|
29
29
|
def dependencies
|
30
30
|
@dependencies ||= begin
|
31
|
-
specs = definition.specs.to_a
|
31
|
+
specs = definition.locked_gems.specs.to_a
|
32
32
|
|
33
33
|
definition
|
34
34
|
.resolve
|
@@ -79,17 +79,18 @@ module Tapioca
|
|
79
79
|
extend(T::Sig)
|
80
80
|
|
81
81
|
IGNORED_GEMS = T.let(%w{
|
82
|
-
sorbet sorbet-static sorbet-runtime
|
82
|
+
sorbet sorbet-static sorbet-runtime
|
83
83
|
}.freeze, T::Array[String])
|
84
84
|
|
85
85
|
sig { returns(String) }
|
86
|
-
attr_reader :full_gem_path
|
86
|
+
attr_reader :full_gem_path, :version
|
87
87
|
|
88
88
|
sig { params(spec: Spec).void }
|
89
89
|
def initialize(spec)
|
90
90
|
@spec = T.let(spec, Tapioca::Gemfile::Spec)
|
91
91
|
real_gem_path = to_realpath(@spec.full_gem_path)
|
92
92
|
@full_gem_path = T.let(real_gem_path, String)
|
93
|
+
@version = T.let(version_string, String)
|
93
94
|
end
|
94
95
|
|
95
96
|
sig { params(gemfile_dir: String).returns(T::Boolean) }
|
@@ -109,11 +110,6 @@ module Tapioca
|
|
109
110
|
@spec.name
|
110
111
|
end
|
111
112
|
|
112
|
-
sig { returns(::Gem::Version) }
|
113
|
-
def version
|
114
|
-
@spec.version
|
115
|
-
end
|
116
|
-
|
117
113
|
sig { returns(String) }
|
118
114
|
def rbi_file_name
|
119
115
|
"#{name}@#{version}.rbi"
|
@@ -121,11 +117,38 @@ module Tapioca
|
|
121
117
|
|
122
118
|
sig { params(path: String).returns(T::Boolean) }
|
123
119
|
def contains_path?(path)
|
124
|
-
to_realpath(path).start_with?(full_gem_path)
|
120
|
+
to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
|
125
121
|
end
|
126
122
|
|
127
123
|
private
|
128
124
|
|
125
|
+
sig { returns(String) }
|
126
|
+
def version_string
|
127
|
+
version = @spec.version.to_s
|
128
|
+
version += "-#{@spec.source.revision}" if Bundler::Source::Git === @spec.source
|
129
|
+
version
|
130
|
+
end
|
131
|
+
|
132
|
+
sig { params(path: String).returns(T::Boolean) }
|
133
|
+
def has_parent_gemspec?(path)
|
134
|
+
# For some Git installed gems the location of the loaded file can
|
135
|
+
# be different from the gem path as indicated by the spec file
|
136
|
+
#
|
137
|
+
# To compensate for these cases, we walk up the directory hierarchy
|
138
|
+
# from the given file and try to match a <gem-name.gemspec> file in
|
139
|
+
# one of those folders to see if the path really belongs in the given gem
|
140
|
+
# or not.
|
141
|
+
return false unless Bundler::Source::Git === @spec.source
|
142
|
+
parent = Pathname.new(path)
|
143
|
+
|
144
|
+
until parent.root?
|
145
|
+
parent = parent.parent.expand_path
|
146
|
+
return true if parent.join("#{name}.gemspec").file?
|
147
|
+
end
|
148
|
+
|
149
|
+
false
|
150
|
+
end
|
151
|
+
|
129
152
|
sig { params(path: T.any(String, Pathname)).returns(String) }
|
130
153
|
def to_realpath(path)
|
131
154
|
path_string = path.to_s
|
data/lib/tapioca/generator.rb
CHANGED
@@ -35,7 +35,7 @@ module Tapioca
|
|
35
35
|
.each do |gem|
|
36
36
|
say("Processing '#{gem.name}' gem:", :green)
|
37
37
|
indent do
|
38
|
-
|
38
|
+
compile_gem_rbi(gem)
|
39
39
|
puts
|
40
40
|
end
|
41
41
|
end
|
@@ -44,6 +44,105 @@ module Tapioca
|
|
44
44
|
say("Please review changes and commit them.", [:green, :bold])
|
45
45
|
end
|
46
46
|
|
47
|
+
sig { void }
|
48
|
+
def build_requires
|
49
|
+
requires_path = Config::DEFAULT_POSTREQUIRE
|
50
|
+
compiler = Compilers::RequiresCompiler.new(Config::SORBET_CONFIG)
|
51
|
+
name = set_color(requires_path, :yellow, :bold)
|
52
|
+
say("Compiling #{name}, this may take a few seconds... ")
|
53
|
+
|
54
|
+
rb_string = compiler.compile
|
55
|
+
if rb_string.empty?
|
56
|
+
say("Nothing to do", :green)
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
# Clean all existing requires before regenerating the list so we update
|
61
|
+
# it with the new one found in the client code and remove the old ones.
|
62
|
+
File.delete(requires_path) if File.exist?(requires_path)
|
63
|
+
|
64
|
+
content = String.new
|
65
|
+
content << rbi_header(
|
66
|
+
config.generate_command,
|
67
|
+
reason: "explicit gem requires",
|
68
|
+
strictness: "false"
|
69
|
+
)
|
70
|
+
content << rb_string
|
71
|
+
|
72
|
+
outdir = File.dirname(requires_path)
|
73
|
+
FileUtils.mkdir_p(outdir)
|
74
|
+
File.write(requires_path, content)
|
75
|
+
|
76
|
+
say("Done", :green)
|
77
|
+
|
78
|
+
say("All requires from this application have been written to #{name}.", [:green, :bold])
|
79
|
+
cmd = set_color("tapioca sync", :yellow, :bold)
|
80
|
+
say("Please review changes and commit them, then run #{cmd}.", [:green, :bold])
|
81
|
+
end
|
82
|
+
|
83
|
+
sig { void }
|
84
|
+
def build_todos
|
85
|
+
todos_path = config.todos_path
|
86
|
+
compiler = Compilers::TodosCompiler.new
|
87
|
+
name = set_color(todos_path, :yellow, :bold)
|
88
|
+
say("Compiling #{name}, this may take a few seconds... ")
|
89
|
+
|
90
|
+
# Clean all existing unresolved constants before regenerating the list
|
91
|
+
# so Sorbet won't grab them as already resolved.
|
92
|
+
File.delete(todos_path) if File.exist?(todos_path)
|
93
|
+
|
94
|
+
rbi_string = compiler.compile
|
95
|
+
if rbi_string.empty?
|
96
|
+
say("Nothing to do", :green)
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
100
|
+
content = String.new
|
101
|
+
content << rbi_header(
|
102
|
+
config.generate_command,
|
103
|
+
reason: "unresolved constants",
|
104
|
+
strictness: "false"
|
105
|
+
)
|
106
|
+
content << rbi_string
|
107
|
+
content << "\n"
|
108
|
+
|
109
|
+
outdir = File.dirname(todos_path)
|
110
|
+
FileUtils.mkdir_p(outdir)
|
111
|
+
File.write(todos_path, content)
|
112
|
+
|
113
|
+
say("Done", :green)
|
114
|
+
|
115
|
+
say("All unresolved constants have been written to #{name}.", [:green, :bold])
|
116
|
+
say("Please review changes and commit them.", [:green, :bold])
|
117
|
+
end
|
118
|
+
|
119
|
+
sig { params(requested_constants: T::Array[String]).void }
|
120
|
+
def build_dsl(requested_constants)
|
121
|
+
load_application(eager_load: requested_constants.empty?)
|
122
|
+
load_dsl_generators
|
123
|
+
|
124
|
+
say("Compiling DSL RBI files...")
|
125
|
+
say("")
|
126
|
+
|
127
|
+
compiler = Compilers::DslCompiler.new(
|
128
|
+
requested_constants: constantize(requested_constants),
|
129
|
+
requested_generators: config.generators,
|
130
|
+
error_handler: ->(error) {
|
131
|
+
say_error(error, :bold, :red)
|
132
|
+
}
|
133
|
+
)
|
134
|
+
|
135
|
+
compiler.run do |constant, contents|
|
136
|
+
compile_dsl_rbi(constant, contents)
|
137
|
+
end
|
138
|
+
|
139
|
+
say("")
|
140
|
+
say("Done", :green)
|
141
|
+
|
142
|
+
say("All operations performed in working directory.", [:green, :bold])
|
143
|
+
say("Please review changes and commit them.", [:green, :bold])
|
144
|
+
end
|
145
|
+
|
47
146
|
sig { void }
|
48
147
|
def sync_rbis_with_gemfile
|
49
148
|
anything_done = [
|
@@ -63,6 +162,11 @@ module Tapioca
|
|
63
162
|
|
64
163
|
private
|
65
164
|
|
165
|
+
EMPTY_RBI_COMMENT = <<~CONTENT
|
166
|
+
# THIS IS AN EMPTY RBI FILE.
|
167
|
+
# see https://github.com/Shopify/tapioca/blob/master/README.md#manual-gem-requires
|
168
|
+
CONTENT
|
169
|
+
|
66
170
|
sig { returns(Gemfile) }
|
67
171
|
def bundle
|
68
172
|
@bundle ||= Gemfile.new
|
@@ -81,15 +185,83 @@ module Tapioca
|
|
81
185
|
sig { void }
|
82
186
|
def require_gem_file
|
83
187
|
say("Requiring all gems to prepare for compiling... ")
|
84
|
-
|
188
|
+
begin
|
189
|
+
loader.load_bundle(config.prerequire, config.postrequire)
|
190
|
+
rescue LoadError => e
|
191
|
+
explain_failed_require(config.postrequire, e)
|
192
|
+
exit(1)
|
193
|
+
end
|
85
194
|
say(" Done", :green)
|
86
195
|
puts
|
87
196
|
end
|
88
197
|
|
198
|
+
sig { params(file: String, error: LoadError).void }
|
199
|
+
def explain_failed_require(file, error)
|
200
|
+
say_error("\n\nLoadError: #{error}", :bold, :red)
|
201
|
+
say_error("\nTapioca could not load all the gems required by your application.", :yellow)
|
202
|
+
say_error("If you populated ", :yellow)
|
203
|
+
say_error("#{file} ", :bold, :blue)
|
204
|
+
say_error("with ", :yellow)
|
205
|
+
say_error("tapioca require", :bold, :blue)
|
206
|
+
say_error("you should probably review it and remove the faulty line.", :yellow)
|
207
|
+
end
|
208
|
+
|
209
|
+
sig do
|
210
|
+
params(
|
211
|
+
message: String,
|
212
|
+
color: T.any(Symbol, T::Array[Symbol]),
|
213
|
+
).void
|
214
|
+
end
|
215
|
+
def say_error(message = "", *color)
|
216
|
+
force_new_line = (message.to_s !~ /( |\t)\Z/)
|
217
|
+
buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
|
218
|
+
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
|
219
|
+
|
220
|
+
stderr.print(buffer)
|
221
|
+
stderr.flush
|
222
|
+
end
|
223
|
+
|
224
|
+
sig { params(eager_load: T::Boolean).void }
|
225
|
+
def load_application(eager_load:)
|
226
|
+
say("Loading Rails application... ")
|
227
|
+
|
228
|
+
loader.load_rails(
|
229
|
+
environment_load: true,
|
230
|
+
eager_load: eager_load
|
231
|
+
)
|
232
|
+
|
233
|
+
say("Done", :green)
|
234
|
+
end
|
235
|
+
|
236
|
+
sig { void }
|
237
|
+
def load_dsl_generators
|
238
|
+
say("Loading DSL generator classes... ")
|
239
|
+
|
240
|
+
Dir.glob([
|
241
|
+
"#{__dir__}/compilers/dsl/*.rb",
|
242
|
+
"#{Config::TAPIOCA_PATH}/generators/**/*.rb",
|
243
|
+
]).each do |generator|
|
244
|
+
require File.expand_path(generator)
|
245
|
+
end
|
246
|
+
|
247
|
+
say("Done", :green)
|
248
|
+
end
|
249
|
+
|
250
|
+
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
251
|
+
def constantize(constant_names)
|
252
|
+
constant_names.map do |name|
|
253
|
+
begin
|
254
|
+
name.constantize
|
255
|
+
rescue NameError
|
256
|
+
nil
|
257
|
+
end
|
258
|
+
end.compact
|
259
|
+
end
|
260
|
+
|
89
261
|
sig { returns(T::Hash[String, String]) }
|
90
262
|
def existing_rbis
|
91
263
|
@existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
|
92
|
-
.map { |f| f.basename(".*").to_s.split('@') }
|
264
|
+
.map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
|
93
265
|
.to_h
|
94
266
|
end
|
95
267
|
|
@@ -102,22 +274,22 @@ module Tapioca
|
|
102
274
|
end
|
103
275
|
|
104
276
|
sig { params(gem_name: String, version: String).returns(Pathname) }
|
105
|
-
def
|
277
|
+
def gem_rbi_filename(gem_name, version)
|
106
278
|
config.outpath / "#{gem_name}@#{version}.rbi"
|
107
279
|
end
|
108
280
|
|
109
281
|
sig { params(gem_name: String).returns(Pathname) }
|
110
282
|
def existing_rbi(gem_name)
|
111
|
-
|
283
|
+
gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
112
284
|
end
|
113
285
|
|
114
286
|
sig { params(gem_name: String).returns(Pathname) }
|
115
287
|
def expected_rbi(gem_name)
|
116
|
-
|
288
|
+
gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
117
289
|
end
|
118
290
|
|
119
291
|
sig { params(gem_name: String).returns(T::Boolean) }
|
120
|
-
def
|
292
|
+
def gem_rbi_exists?(gem_name)
|
121
293
|
existing_rbis.key?(gem_name)
|
122
294
|
end
|
123
295
|
|
@@ -195,13 +367,13 @@ module Tapioca
|
|
195
367
|
gems.each do |gem_name|
|
196
368
|
filename = expected_rbi(gem_name)
|
197
369
|
|
198
|
-
if
|
370
|
+
if gem_rbi_exists?(gem_name)
|
199
371
|
old_filename = existing_rbi(gem_name)
|
200
372
|
move(old_filename, filename) unless old_filename == filename
|
201
373
|
end
|
202
374
|
|
203
375
|
gem = T.must(bundle.gem(gem_name))
|
204
|
-
|
376
|
+
compile_gem_rbi(gem)
|
205
377
|
add(filename)
|
206
378
|
|
207
379
|
puts
|
@@ -233,37 +405,73 @@ module Tapioca
|
|
233
405
|
end
|
234
406
|
end
|
235
407
|
|
236
|
-
sig { params(command: String,
|
237
|
-
def rbi_header(command,
|
238
|
-
<<~HEAD
|
239
|
-
#
|
240
|
-
#
|
408
|
+
sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
|
409
|
+
def rbi_header(command, reason: nil, strictness: nil)
|
410
|
+
statement = <<~HEAD
|
411
|
+
# DO NOT EDIT MANUALLY
|
412
|
+
# This is an autogenerated file for #{reason}.
|
413
|
+
# Please instead update this file by running `#{command}`.
|
414
|
+
HEAD
|
241
415
|
|
242
|
-
|
416
|
+
sigil = <<~SIGIL if strictness
|
417
|
+
# typed: #{strictness}
|
418
|
+
SIGIL
|
243
419
|
|
244
|
-
|
420
|
+
[statement, sigil].compact.join("\n").strip.concat("\n\n")
|
245
421
|
end
|
246
422
|
|
247
423
|
sig { params(gem: Gemfile::Gem).void }
|
248
|
-
def
|
424
|
+
def compile_gem_rbi(gem)
|
249
425
|
compiler = Compilers::SymbolTableCompiler.new
|
250
426
|
gem_name = set_color(gem.name, :yellow, :bold)
|
251
427
|
say("Compiling #{gem_name}, this may take a few seconds... ")
|
252
428
|
|
253
|
-
|
254
|
-
|
255
|
-
content =
|
256
|
-
content
|
429
|
+
strictness = config.typed_overrides[gem.name] || "true"
|
430
|
+
rbi_body_content = compiler.compile(gem)
|
431
|
+
content = String.new
|
432
|
+
content << rbi_header(
|
433
|
+
config.generate_command,
|
434
|
+
reason: "types exported from the `#{gem.name}` gem",
|
435
|
+
strictness: strictness
|
436
|
+
)
|
257
437
|
|
258
438
|
FileUtils.mkdir_p(config.outdir)
|
259
439
|
filename = config.outpath / gem.rbi_file_name
|
260
|
-
File.write(filename.to_s, content)
|
261
440
|
|
262
|
-
|
441
|
+
if rbi_body_content.strip.empty?
|
442
|
+
content << EMPTY_RBI_COMMENT
|
443
|
+
say("Done (empty output)", :yellow)
|
444
|
+
else
|
445
|
+
content << rbi_body_content
|
446
|
+
say("Done", :green)
|
447
|
+
end
|
448
|
+
File.write(filename.to_s, content)
|
263
449
|
|
264
450
|
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
265
451
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
266
452
|
end
|
267
453
|
end
|
454
|
+
|
455
|
+
sig { params(constant: Module, contents: String).void }
|
456
|
+
def compile_dsl_rbi(constant, contents)
|
457
|
+
return if contents.nil?
|
458
|
+
|
459
|
+
command = format(config.generate_command, constant.name)
|
460
|
+
constant_name = Module.instance_method(:name).bind(constant).call
|
461
|
+
rbi_name = constant_name.underscore + ".rbi"
|
462
|
+
filename = config.outpath / rbi_name
|
463
|
+
|
464
|
+
out = String.new
|
465
|
+
out << rbi_header(
|
466
|
+
command,
|
467
|
+
reason: "dynamic methods in `#{constant.name}`"
|
468
|
+
)
|
469
|
+
out << contents
|
470
|
+
|
471
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
472
|
+
File.write(filename, out)
|
473
|
+
say("Wrote: ", [:green])
|
474
|
+
say(filename)
|
475
|
+
end
|
268
476
|
end
|
269
477
|
end
|