tapioca 0.2.7 → 0.4.1
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 +27 -1
- data/README.md +21 -2
- data/Rakefile +15 -4
- data/lib/tapioca.rb +15 -9
- data/lib/tapioca/cli.rb +41 -12
- 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 +285 -0
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -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 +163 -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 +83 -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 +209 -49
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
- data/lib/tapioca/compilers/todos_compiler.rb +32 -0
- data/lib/tapioca/config.rb +42 -0
- data/lib/tapioca/config_builder.rb +75 -0
- data/lib/tapioca/constant_locator.rb +1 -0
- data/lib/tapioca/core_ext/class.rb +23 -0
- data/lib/tapioca/gemfile.rb +14 -1
- data/lib/tapioca/generator.rb +235 -67
- data/lib/tapioca/loader.rb +20 -9
- data/lib/tapioca/sorbet_config_parser.rb +77 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +35 -66
@@ -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
|
|
@@ -33,7 +29,7 @@ module Tapioca
|
|
33
29
|
file.write(Array(paths).join("\n"))
|
34
30
|
file.flush
|
35
31
|
|
36
|
-
symbol_table_json_from("@#{file.path}")
|
32
|
+
symbol_table_json_from("@#{file.path.shellescape}")
|
37
33
|
end, T.nilable(String))
|
38
34
|
|
39
35
|
return Set.new if output.nil? || output.empty?
|
@@ -44,7 +40,7 @@ module Tapioca
|
|
44
40
|
|
45
41
|
def ignored_symbols
|
46
42
|
unless @ignored_symbols
|
47
|
-
output = symbol_table_json_from("''", table_type: "symbol-table-full-json")
|
43
|
+
output = symbol_table_json_from("-e ''", table_type: "symbol-table-full-json")
|
48
44
|
json = JSON.parse(output)
|
49
45
|
@ignored_symbols = SymbolTableParser.parse(json)
|
50
46
|
end
|
@@ -53,17 +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,
|
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
|
-
].shelljoin,
|
65
|
-
err: "/dev/null"
|
66
|
-
).read
|
52
|
+
Tapioca::Compilers::Sorbet.run("--no-config", "--print=#{table_type}", input)
|
67
53
|
end
|
68
54
|
end
|
69
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
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
class Config < T::Struct
|
6
|
+
extend(T::Sig)
|
7
|
+
|
8
|
+
const(:outdir, String)
|
9
|
+
const(:prerequire, T.nilable(String))
|
10
|
+
const(:postrequire, String)
|
11
|
+
const(:generate_command, String)
|
12
|
+
const(:exclude, T::Array[String])
|
13
|
+
const(:typed_overrides, T::Hash[String, String])
|
14
|
+
const(:todos_path, String)
|
15
|
+
const(:generators, T::Array[String])
|
16
|
+
|
17
|
+
sig { returns(Pathname) }
|
18
|
+
def outpath
|
19
|
+
@outpath = T.let(@outpath, T.nilable(Pathname))
|
20
|
+
@outpath ||= Pathname.new(outdir)
|
21
|
+
end
|
22
|
+
|
23
|
+
private_class_method :new
|
24
|
+
|
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)
|
35
|
+
|
36
|
+
DEFAULT_OVERRIDES = T.let({
|
37
|
+
# ActiveSupport overrides some core methods with different signatures
|
38
|
+
# so we generate a typed: false RBI for it to suppress errors
|
39
|
+
"activesupport" => "false",
|
40
|
+
}.freeze, T::Hash[String, String])
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
class ConfigBuilder
|
8
|
+
class << self
|
9
|
+
extend(T::Sig)
|
10
|
+
|
11
|
+
sig { params(command: Symbol, options: T::Hash[String, T.untyped]).returns(Config) }
|
12
|
+
def from_options(command, options)
|
13
|
+
Config.from_hash(
|
14
|
+
merge_options(default_options(command), config_options, options)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
21
|
+
def config_options
|
22
|
+
if File.exist?(Config::TAPIOCA_CONFIG)
|
23
|
+
YAML.load_file(Config::TAPIOCA_CONFIG, fallback: {})
|
24
|
+
else
|
25
|
+
{}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
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)
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { returns(String) }
|
44
|
+
def default_command
|
45
|
+
command = File.basename($PROGRAM_NAME)
|
46
|
+
args = ARGV.join(" ")
|
47
|
+
|
48
|
+
"#{command} #{args}".strip
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { params(options: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
|
52
|
+
def merge_options(*options)
|
53
|
+
options.each_with_object({}) do |option, result|
|
54
|
+
result.merge!(option) do |_, this_val, other_val|
|
55
|
+
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
|
56
|
+
this_val.merge(other_val)
|
57
|
+
else
|
58
|
+
other_val
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
DEFAULT_OPTIONS = T.let({
|
66
|
+
"postrequire" => Config::DEFAULT_POSTREQUIRE,
|
67
|
+
"outdir" => nil,
|
68
|
+
"generate_command" => default_command,
|
69
|
+
"exclude" => [],
|
70
|
+
"typed_overrides" => Config::DEFAULT_OVERRIDES,
|
71
|
+
"todos_path" => Config::DEFAULT_TODOSPATH,
|
72
|
+
"generators" => [],
|
73
|
+
}.freeze, T::Hash[String, T.untyped])
|
74
|
+
end
|
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
@@ -88,7 +88,8 @@ module Tapioca
|
|
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
|
+
@full_gem_path = T.let(real_gem_path, String)
|
92
93
|
end
|
93
94
|
|
94
95
|
sig { params(gemfile_dir: String).returns(T::Boolean) }
|
@@ -118,8 +119,20 @@ module Tapioca
|
|
118
119
|
"#{name}@#{version}.rbi"
|
119
120
|
end
|
120
121
|
|
122
|
+
sig { params(path: String).returns(T::Boolean) }
|
123
|
+
def contains_path?(path)
|
124
|
+
to_realpath(path).start_with?(full_gem_path)
|
125
|
+
end
|
126
|
+
|
121
127
|
private
|
122
128
|
|
129
|
+
sig { params(path: T.any(String, Pathname)).returns(String) }
|
130
|
+
def to_realpath(path)
|
131
|
+
path_string = path.to_s
|
132
|
+
path_string = File.realpath(path_string) if File.exist?(path_string)
|
133
|
+
path_string
|
134
|
+
end
|
135
|
+
|
123
136
|
sig { returns(T::Boolean) }
|
124
137
|
def gem_ignored?
|
125
138
|
IGNORED_GEMS.include?(name)
|
data/lib/tapioca/generator.rb
CHANGED
@@ -8,41 +8,16 @@ module Tapioca
|
|
8
8
|
class Generator < ::Thor::Shell::Color
|
9
9
|
extend(T::Sig)
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
DEFAULT_OUTDIR = "sorbet/rbi/gems"
|
14
|
-
DEFAULT_OVERRIDES = T.let({
|
15
|
-
# ActiveSupport overrides some core methods with different signatures
|
16
|
-
# so we generate a typed: false RBI for it to suppress errors
|
17
|
-
"activesupport" => "false",
|
18
|
-
}.freeze, T::Hash[String, String])
|
19
|
-
|
20
|
-
sig { returns(Pathname) }
|
21
|
-
attr_reader :outdir
|
22
|
-
sig { returns(T.nilable(String)) }
|
23
|
-
attr_reader :prerequire
|
24
|
-
sig { returns(T.nilable(String)) }
|
25
|
-
attr_reader :postrequire
|
26
|
-
sig { returns(String) }
|
27
|
-
attr_reader :command
|
28
|
-
sig { returns(T::Hash[String, String]) }
|
29
|
-
attr_reader :typed_overrides
|
11
|
+
sig { returns(Config) }
|
12
|
+
attr_reader :config
|
30
13
|
|
31
14
|
sig do
|
32
15
|
params(
|
33
|
-
|
34
|
-
prerequire: T.nilable(String),
|
35
|
-
postrequire: T.nilable(String),
|
36
|
-
command: T.nilable(String),
|
37
|
-
typed_overrides: T.nilable(T::Hash[String, String])
|
16
|
+
config: Config
|
38
17
|
).void
|
39
18
|
end
|
40
|
-
def initialize(
|
41
|
-
@
|
42
|
-
@prerequire = T.let(prerequire, T.nilable(String))
|
43
|
-
@postrequire = T.let(postrequire || DEFAULT_POSTREQUIRE, T.nilable(String))
|
44
|
-
@command = T.let(command || default_command, String)
|
45
|
-
@typed_overrides = T.let(typed_overrides || {}, T::Hash[String, String])
|
19
|
+
def initialize(config)
|
20
|
+
@config = config
|
46
21
|
@bundle = T.let(nil, T.nilable(Gemfile))
|
47
22
|
@loader = T.let(nil, T.nilable(Loader))
|
48
23
|
@compiler = T.let(nil, T.nilable(Compilers::SymbolTableCompiler))
|
@@ -55,14 +30,115 @@ module Tapioca
|
|
55
30
|
def build_gem_rbis(gem_names)
|
56
31
|
require_gem_file
|
57
32
|
|
58
|
-
gems_to_generate(gem_names)
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
33
|
+
gems_to_generate(gem_names)
|
34
|
+
.reject { |gem| config.exclude.include?(gem.name) }
|
35
|
+
.each do |gem|
|
36
|
+
say("Processing '#{gem.name}' gem:", :green)
|
37
|
+
indent do
|
38
|
+
compile_gem_rbi(gem)
|
39
|
+
puts
|
40
|
+
end
|
63
41
|
end
|
42
|
+
|
43
|
+
say("All operations performed in working directory.", [:green, :bold])
|
44
|
+
say("Please review changes and commit them.", [:green, :bold])
|
45
|
+
end
|
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
|
64
98
|
end
|
65
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
|
+
|
66
142
|
say("All operations performed in working directory.", [:green, :bold])
|
67
143
|
say("Please review changes and commit them.", [:green, :bold])
|
68
144
|
end
|
@@ -86,14 +162,6 @@ module Tapioca
|
|
86
162
|
|
87
163
|
private
|
88
164
|
|
89
|
-
sig { returns(String) }
|
90
|
-
def default_command
|
91
|
-
command = File.basename($PROGRAM_NAME)
|
92
|
-
args = ARGV.join(" ")
|
93
|
-
|
94
|
-
"#{command} #{args}"
|
95
|
-
end
|
96
|
-
|
97
165
|
sig { returns(Gemfile) }
|
98
166
|
def bundle
|
99
167
|
@bundle ||= Gemfile.new
|
@@ -112,42 +180,111 @@ module Tapioca
|
|
112
180
|
sig { void }
|
113
181
|
def require_gem_file
|
114
182
|
say("Requiring all gems to prepare for compiling... ")
|
115
|
-
|
183
|
+
begin
|
184
|
+
loader.load_bundle(config.prerequire, config.postrequire)
|
185
|
+
rescue LoadError => e
|
186
|
+
explain_failed_require(config.postrequire, e)
|
187
|
+
exit(1)
|
188
|
+
end
|
116
189
|
say(" Done", :green)
|
117
190
|
puts
|
118
191
|
end
|
119
192
|
|
193
|
+
sig { params(file: String, error: LoadError).void }
|
194
|
+
def explain_failed_require(file, error)
|
195
|
+
say_error("\n\nLoadError: #{error}", :bold, :red)
|
196
|
+
say_error("\nTapioca could not load all the gems required by your application.", :yellow)
|
197
|
+
say_error("If you populated ", :yellow)
|
198
|
+
say_error("#{file} ", :bold, :blue)
|
199
|
+
say_error("with ", :yellow)
|
200
|
+
say_error("tapioca require", :bold, :blue)
|
201
|
+
say_error("you should probably review it and remove the faulty line.", :yellow)
|
202
|
+
end
|
203
|
+
|
204
|
+
sig do
|
205
|
+
params(
|
206
|
+
message: String,
|
207
|
+
color: T.any(Symbol, T::Array[Symbol]),
|
208
|
+
).void
|
209
|
+
end
|
210
|
+
def say_error(message = "", *color)
|
211
|
+
force_new_line = (message.to_s !~ /( |\t)\Z/)
|
212
|
+
buffer = prepare_message(*T.unsafe([message, *T.unsafe(color)]))
|
213
|
+
buffer << "\n" if force_new_line && !message.to_s.end_with?("\n")
|
214
|
+
|
215
|
+
stderr.print(buffer)
|
216
|
+
stderr.flush
|
217
|
+
end
|
218
|
+
|
219
|
+
sig { params(eager_load: T::Boolean).void }
|
220
|
+
def load_application(eager_load:)
|
221
|
+
say("Loading Rails application... ")
|
222
|
+
|
223
|
+
loader.load_rails(
|
224
|
+
environment_load: true,
|
225
|
+
eager_load: eager_load
|
226
|
+
)
|
227
|
+
|
228
|
+
say("Done", :green)
|
229
|
+
end
|
230
|
+
|
231
|
+
sig { void }
|
232
|
+
def load_dsl_generators
|
233
|
+
say("Loading DSL generator classes... ")
|
234
|
+
|
235
|
+
Dir.glob([
|
236
|
+
"#{__dir__}/compilers/dsl/*.rb",
|
237
|
+
"#{Config::TAPIOCA_PATH}/generators/**/*.rb",
|
238
|
+
]).each do |generator|
|
239
|
+
require File.expand_path(generator)
|
240
|
+
end
|
241
|
+
|
242
|
+
say("Done", :green)
|
243
|
+
end
|
244
|
+
|
245
|
+
sig { params(constant_names: T::Array[String]).returns(T::Array[Module]) }
|
246
|
+
def constantize(constant_names)
|
247
|
+
constant_names.map do |name|
|
248
|
+
begin
|
249
|
+
name.constantize
|
250
|
+
rescue NameError
|
251
|
+
nil
|
252
|
+
end
|
253
|
+
end.compact
|
254
|
+
end
|
255
|
+
|
120
256
|
sig { returns(T::Hash[String, String]) }
|
121
257
|
def existing_rbis
|
122
|
-
@existing_rbis ||= Pathname.glob((
|
123
|
-
.map { |f| f.basename(".*").to_s.split('@') }
|
258
|
+
@existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
|
259
|
+
.map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
|
124
260
|
.to_h
|
125
261
|
end
|
126
262
|
|
127
263
|
sig { returns(T::Hash[String, String]) }
|
128
264
|
def expected_rbis
|
129
265
|
@expected_rbis ||= bundle.dependencies
|
266
|
+
.reject { |gem| config.exclude.include?(gem.name) }
|
130
267
|
.map { |gem| [gem.name, gem.version.to_s] }
|
131
268
|
.to_h
|
132
269
|
end
|
133
270
|
|
134
271
|
sig { params(gem_name: String, version: String).returns(Pathname) }
|
135
|
-
def
|
136
|
-
|
272
|
+
def gem_rbi_filename(gem_name, version)
|
273
|
+
config.outpath / "#{gem_name}@#{version}.rbi"
|
137
274
|
end
|
138
275
|
|
139
276
|
sig { params(gem_name: String).returns(Pathname) }
|
140
277
|
def existing_rbi(gem_name)
|
141
|
-
|
278
|
+
gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
142
279
|
end
|
143
280
|
|
144
281
|
sig { params(gem_name: String).returns(Pathname) }
|
145
282
|
def expected_rbi(gem_name)
|
146
|
-
|
283
|
+
gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
147
284
|
end
|
148
285
|
|
149
286
|
sig { params(gem_name: String).returns(T::Boolean) }
|
150
|
-
def
|
287
|
+
def gem_rbi_exists?(gem_name)
|
151
288
|
existing_rbis.key?(gem_name)
|
152
289
|
end
|
153
290
|
|
@@ -225,13 +362,13 @@ module Tapioca
|
|
225
362
|
gems.each do |gem_name|
|
226
363
|
filename = expected_rbi(gem_name)
|
227
364
|
|
228
|
-
if
|
365
|
+
if gem_rbi_exists?(gem_name)
|
229
366
|
old_filename = existing_rbi(gem_name)
|
230
367
|
move(old_filename, filename) unless old_filename == filename
|
231
368
|
end
|
232
369
|
|
233
370
|
gem = T.must(bundle.gem(gem_name))
|
234
|
-
|
371
|
+
compile_gem_rbi(gem)
|
235
372
|
add(filename)
|
236
373
|
|
237
374
|
puts
|
@@ -263,37 +400,68 @@ module Tapioca
|
|
263
400
|
end
|
264
401
|
end
|
265
402
|
|
266
|
-
sig { params(command: String,
|
267
|
-
def rbi_header(command,
|
268
|
-
<<~HEAD
|
269
|
-
#
|
270
|
-
#
|
403
|
+
sig { params(command: String, reason: T.nilable(String), strictness: T.nilable(String)).returns(String) }
|
404
|
+
def rbi_header(command, reason: nil, strictness: nil)
|
405
|
+
statement = <<~HEAD
|
406
|
+
# DO NOT EDIT MANUALLY
|
407
|
+
# This is an autogenerated file for #{reason}.
|
408
|
+
# Please instead update this file by running `#{command}`.
|
409
|
+
HEAD
|
271
410
|
|
272
|
-
|
411
|
+
sigil = <<~SIGIL if strictness
|
412
|
+
# typed: #{strictness}
|
413
|
+
SIGIL
|
273
414
|
|
274
|
-
|
415
|
+
[statement, sigil].compact.join("\n").strip.concat("\n\n")
|
275
416
|
end
|
276
417
|
|
277
418
|
sig { params(gem: Gemfile::Gem).void }
|
278
|
-
def
|
419
|
+
def compile_gem_rbi(gem)
|
279
420
|
compiler = Compilers::SymbolTableCompiler.new
|
280
421
|
gem_name = set_color(gem.name, :yellow, :bold)
|
281
422
|
say("Compiling #{gem_name}, this may take a few seconds... ")
|
282
423
|
|
283
|
-
|
424
|
+
strictness = config.typed_overrides[gem.name] || "true"
|
284
425
|
|
285
|
-
content =
|
286
|
-
content
|
426
|
+
content = String.new
|
427
|
+
content << rbi_header(
|
428
|
+
config.generate_command,
|
429
|
+
reason: "types exported from the `#{gem.name}` gem",
|
430
|
+
strictness: strictness
|
431
|
+
)
|
432
|
+
content << compiler.compile(gem)
|
287
433
|
|
288
|
-
FileUtils.mkdir_p(outdir)
|
289
|
-
filename =
|
434
|
+
FileUtils.mkdir_p(config.outdir)
|
435
|
+
filename = config.outpath / gem.rbi_file_name
|
290
436
|
File.write(filename.to_s, content)
|
291
437
|
|
292
438
|
say("Done", :green)
|
293
439
|
|
294
|
-
Pathname.glob((
|
440
|
+
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
295
441
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
296
442
|
end
|
297
443
|
end
|
444
|
+
|
445
|
+
sig { params(constant: Module, contents: String).void }
|
446
|
+
def compile_dsl_rbi(constant, contents)
|
447
|
+
return if contents.nil?
|
448
|
+
|
449
|
+
command = format(config.generate_command, constant.name)
|
450
|
+
constant_name = Module.instance_method(:name).bind(constant).call
|
451
|
+
rbi_name = constant_name.underscore + ".rbi"
|
452
|
+
filename = config.outpath / rbi_name
|
453
|
+
|
454
|
+
out = String.new
|
455
|
+
out << rbi_header(
|
456
|
+
command,
|
457
|
+
reason: "dynamic methods in `#{constant.name}`"
|
458
|
+
)
|
459
|
+
out << contents
|
460
|
+
|
461
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
462
|
+
File.write(filename, out)
|
463
|
+
say("Wrote: ", [:green])
|
464
|
+
say(filename)
|
465
|
+
end
|
298
466
|
end
|
299
467
|
end
|