tapioca 0.2.8 → 0.4.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 +4 -4
- data/Gemfile +25 -1
- data/README.md +23 -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 +387 -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 +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 +210 -50
- 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 +44 -9
- data/lib/tapioca/generator.rb +248 -70
- data/lib/tapioca/loader.rb +20 -9
- data/lib/tapioca/sorbet_config_parser.rb +77 -0
- data/lib/tapioca/version.rb +1 -1
- metadata +33 -51
|
@@ -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
|
@@ -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
|
-
real_gem_path =
|
|
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,18 +110,52 @@ 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"
|
|
120
116
|
end
|
|
121
117
|
|
|
118
|
+
sig { params(path: String).returns(T::Boolean) }
|
|
119
|
+
def contains_path?(path)
|
|
120
|
+
to_realpath(path).start_with?(full_gem_path) || has_parent_gemspec?(path)
|
|
121
|
+
end
|
|
122
|
+
|
|
122
123
|
private
|
|
123
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
|
+
|
|
152
|
+
sig { params(path: T.any(String, Pathname)).returns(String) }
|
|
153
|
+
def to_realpath(path)
|
|
154
|
+
path_string = path.to_s
|
|
155
|
+
path_string = File.realpath(path_string) if File.exist?(path_string)
|
|
156
|
+
path_string
|
|
157
|
+
end
|
|
158
|
+
|
|
124
159
|
sig { returns(T::Boolean) }
|
|
125
160
|
def gem_ignored?
|
|
126
161
|
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
|
|
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)
|
|
64
137
|
end
|
|
65
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,13 +162,10 @@ module Tapioca
|
|
|
86
162
|
|
|
87
163
|
private
|
|
88
164
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"#{command} #{args}"
|
|
95
|
-
end
|
|
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
|
|
96
169
|
|
|
97
170
|
sig { returns(Gemfile) }
|
|
98
171
|
def bundle
|
|
@@ -112,42 +185,111 @@ module Tapioca
|
|
|
112
185
|
sig { void }
|
|
113
186
|
def require_gem_file
|
|
114
187
|
say("Requiring all gems to prepare for compiling... ")
|
|
115
|
-
|
|
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
|
|
116
194
|
say(" Done", :green)
|
|
117
195
|
puts
|
|
118
196
|
end
|
|
119
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
|
+
|
|
120
261
|
sig { returns(T::Hash[String, String]) }
|
|
121
262
|
def existing_rbis
|
|
122
|
-
@existing_rbis ||= Pathname.glob((
|
|
123
|
-
.map { |f| f.basename(".*").to_s.split('@') }
|
|
263
|
+
@existing_rbis ||= Pathname.glob((config.outpath / "*@*.rbi").to_s)
|
|
264
|
+
.map { |f| T.cast(f.basename(".*").to_s.split('@', 2), [String, String]) }
|
|
124
265
|
.to_h
|
|
125
266
|
end
|
|
126
267
|
|
|
127
268
|
sig { returns(T::Hash[String, String]) }
|
|
128
269
|
def expected_rbis
|
|
129
270
|
@expected_rbis ||= bundle.dependencies
|
|
271
|
+
.reject { |gem| config.exclude.include?(gem.name) }
|
|
130
272
|
.map { |gem| [gem.name, gem.version.to_s] }
|
|
131
273
|
.to_h
|
|
132
274
|
end
|
|
133
275
|
|
|
134
276
|
sig { params(gem_name: String, version: String).returns(Pathname) }
|
|
135
|
-
def
|
|
136
|
-
|
|
277
|
+
def gem_rbi_filename(gem_name, version)
|
|
278
|
+
config.outpath / "#{gem_name}@#{version}.rbi"
|
|
137
279
|
end
|
|
138
280
|
|
|
139
281
|
sig { params(gem_name: String).returns(Pathname) }
|
|
140
282
|
def existing_rbi(gem_name)
|
|
141
|
-
|
|
283
|
+
gem_rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
|
142
284
|
end
|
|
143
285
|
|
|
144
286
|
sig { params(gem_name: String).returns(Pathname) }
|
|
145
287
|
def expected_rbi(gem_name)
|
|
146
|
-
|
|
288
|
+
gem_rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
|
147
289
|
end
|
|
148
290
|
|
|
149
291
|
sig { params(gem_name: String).returns(T::Boolean) }
|
|
150
|
-
def
|
|
292
|
+
def gem_rbi_exists?(gem_name)
|
|
151
293
|
existing_rbis.key?(gem_name)
|
|
152
294
|
end
|
|
153
295
|
|
|
@@ -225,13 +367,13 @@ module Tapioca
|
|
|
225
367
|
gems.each do |gem_name|
|
|
226
368
|
filename = expected_rbi(gem_name)
|
|
227
369
|
|
|
228
|
-
if
|
|
370
|
+
if gem_rbi_exists?(gem_name)
|
|
229
371
|
old_filename = existing_rbi(gem_name)
|
|
230
372
|
move(old_filename, filename) unless old_filename == filename
|
|
231
373
|
end
|
|
232
374
|
|
|
233
375
|
gem = T.must(bundle.gem(gem_name))
|
|
234
|
-
|
|
376
|
+
compile_gem_rbi(gem)
|
|
235
377
|
add(filename)
|
|
236
378
|
|
|
237
379
|
puts
|
|
@@ -263,37 +405,73 @@ module Tapioca
|
|
|
263
405
|
end
|
|
264
406
|
end
|
|
265
407
|
|
|
266
|
-
sig { params(command: String,
|
|
267
|
-
def rbi_header(command,
|
|
268
|
-
<<~HEAD
|
|
269
|
-
#
|
|
270
|
-
#
|
|
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
|
|
271
415
|
|
|
272
|
-
|
|
416
|
+
sigil = <<~SIGIL if strictness
|
|
417
|
+
# typed: #{strictness}
|
|
418
|
+
SIGIL
|
|
273
419
|
|
|
274
|
-
|
|
420
|
+
[statement, sigil].compact.join("\n").strip.concat("\n\n")
|
|
275
421
|
end
|
|
276
422
|
|
|
277
423
|
sig { params(gem: Gemfile::Gem).void }
|
|
278
|
-
def
|
|
424
|
+
def compile_gem_rbi(gem)
|
|
279
425
|
compiler = Compilers::SymbolTableCompiler.new
|
|
280
426
|
gem_name = set_color(gem.name, :yellow, :bold)
|
|
281
427
|
say("Compiling #{gem_name}, this may take a few seconds... ")
|
|
282
428
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
content =
|
|
286
|
-
content
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
+
)
|
|
437
|
+
|
|
438
|
+
FileUtils.mkdir_p(config.outdir)
|
|
439
|
+
filename = config.outpath / gem.rbi_file_name
|
|
440
|
+
|
|
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
|
|
290
448
|
File.write(filename.to_s, content)
|
|
291
449
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
Pathname.glob((outdir / "#{gem.name}@*.rbi").to_s) do |file|
|
|
450
|
+
Pathname.glob((config.outpath / "#{gem.name}@*.rbi").to_s) do |file|
|
|
295
451
|
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
|
296
452
|
end
|
|
297
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
|
|
298
476
|
end
|
|
299
477
|
end
|