tapioca 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +11 -2
- data/README.md +51 -7
- data/Rakefile +11 -1
- data/exe/tapioca +6 -0
- data/lib/t.rb +50 -0
- data/lib/tapioca.rb +18 -1
- data/lib/tapioca/cli.rb +58 -0
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +571 -0
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +100 -0
- data/lib/tapioca/compilers/symbol_table_compiler.rb +23 -0
- data/lib/tapioca/gemfile.rb +136 -0
- data/lib/tapioca/generator.rb +286 -0
- data/lib/tapioca/version.rb +4 -1
- metadata +170 -21
- data/.gitignore +0 -17
- data/LICENSE +0 -22
- data/tapioca.gemspec +0 -17
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require 'json'
|
5
|
+
require 'pathname'
|
6
|
+
require 'tempfile'
|
7
|
+
require 'shellwords'
|
8
|
+
|
9
|
+
module Tapioca
|
10
|
+
module Compilers
|
11
|
+
module SymbolTable
|
12
|
+
module SymbolLoader
|
13
|
+
SORBET = Pathname.new(Gem::Specification.find_by_name("sorbet-static").full_gem_path) / "libexec" / "sorbet"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
extend(T::Sig)
|
17
|
+
|
18
|
+
sig { params(paths: T::Array[Pathname]).returns(T::Set[String]) }
|
19
|
+
def list_from_paths(paths)
|
20
|
+
load_symbols(paths.map(&:to_s))
|
21
|
+
end
|
22
|
+
|
23
|
+
def ignore_symbol?(symbol)
|
24
|
+
ignored_symbols.include?(symbol)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
sig { params(paths: T::Array[String]).returns(T::Set[String]) }
|
30
|
+
def load_symbols(paths)
|
31
|
+
output = T.cast(Tempfile.create('sorbet') do |file|
|
32
|
+
file.write(Array(paths).join("\n"))
|
33
|
+
file.flush
|
34
|
+
|
35
|
+
symbol_table_json_from("@#{file.path}")
|
36
|
+
end, T.nilable(String))
|
37
|
+
|
38
|
+
return Set.new if output.nil? || output.empty?
|
39
|
+
|
40
|
+
json = JSON.parse(output)
|
41
|
+
SymbolTableParser.parse(json)
|
42
|
+
end
|
43
|
+
|
44
|
+
def ignored_symbols
|
45
|
+
unless @ignored_symbols
|
46
|
+
output = symbol_table_json_from("''", table_type: "symbol-table-full-json")
|
47
|
+
json = JSON.parse(output)
|
48
|
+
@ignored_symbols = SymbolTableParser.parse(json)
|
49
|
+
end
|
50
|
+
|
51
|
+
@ignored_symbols
|
52
|
+
end
|
53
|
+
|
54
|
+
def symbol_table_json_from(input, table_type: "symbol-table-json")
|
55
|
+
IO.popen(
|
56
|
+
[
|
57
|
+
SORBET,
|
58
|
+
# We don't want to pick up any sorbet/config files in cwd
|
59
|
+
"--no-config",
|
60
|
+
"--print=#{table_type}",
|
61
|
+
"--quiet",
|
62
|
+
input,
|
63
|
+
].shelljoin,
|
64
|
+
err: "/dev/null"
|
65
|
+
).read
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class SymbolTableParser
|
70
|
+
def self.parse(object, parents = [])
|
71
|
+
symbols = Set.new
|
72
|
+
|
73
|
+
children = object.fetch("children", [])
|
74
|
+
|
75
|
+
children.each do |child|
|
76
|
+
kind = child.fetch("kind")
|
77
|
+
name = child.fetch("name")
|
78
|
+
name = name.fetch("name") if name.is_a?(Hash)
|
79
|
+
|
80
|
+
next if kind.nil? || name.nil?
|
81
|
+
|
82
|
+
next unless %w[CLASS STATIC_FIELD].include?(kind)
|
83
|
+
next if name =~ /[<>()$]/
|
84
|
+
next if name =~ /^[0-9]+$/
|
85
|
+
next if name == "T::Helpers"
|
86
|
+
|
87
|
+
parents << name
|
88
|
+
|
89
|
+
symbols.add(parents.join("::"))
|
90
|
+
symbols.merge(parse(child, parents))
|
91
|
+
|
92
|
+
parents.pop
|
93
|
+
end
|
94
|
+
symbols
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strong
|
3
|
+
|
4
|
+
module Tapioca
|
5
|
+
module Compilers
|
6
|
+
class SymbolTableCompiler
|
7
|
+
extend(T::Sig)
|
8
|
+
|
9
|
+
sig do
|
10
|
+
params(
|
11
|
+
gem: Gemfile::Gem,
|
12
|
+
indent: Integer
|
13
|
+
).returns(String)
|
14
|
+
end
|
15
|
+
def compile(
|
16
|
+
gem,
|
17
|
+
indent = 0
|
18
|
+
)
|
19
|
+
Tapioca::Compilers::SymbolTable::SymbolGenerator.new(gem, indent).generate
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "bundler"
|
5
|
+
|
6
|
+
module Tapioca
|
7
|
+
class Gemfile
|
8
|
+
extend(T::Sig)
|
9
|
+
|
10
|
+
Spec = T.type_alias(T.any(::Bundler::StubSpecification, ::Gem::Specification))
|
11
|
+
|
12
|
+
sig { void }
|
13
|
+
def initialize
|
14
|
+
@gemfile = T.let(File.new(Bundler.default_gemfile), File)
|
15
|
+
@lockfile = T.let(File.new(Bundler.default_lockfile), File)
|
16
|
+
@dependencies = T.let(nil, T.nilable(T::Array[Gem]))
|
17
|
+
@definition = T.let(nil, T.nilable(Bundler::Definition))
|
18
|
+
end
|
19
|
+
|
20
|
+
sig { returns(T::Array[Gem]) }
|
21
|
+
def dependencies
|
22
|
+
@dependencies ||= begin
|
23
|
+
specs = definition.specs.to_a
|
24
|
+
|
25
|
+
definition
|
26
|
+
.resolve
|
27
|
+
.materialize(specs)
|
28
|
+
.reject { |spec| ignore_gem_spec?(spec) }
|
29
|
+
.map { |spec| Gem.new(spec) }
|
30
|
+
.uniq(&:rbi_file_name)
|
31
|
+
.sort_by(&:rbi_file_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
sig { params(gem_name: String).returns(T.nilable(Gem)) }
|
36
|
+
def gem(gem_name)
|
37
|
+
dependencies.detect { |dep| dep.name == gem_name }
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { params(initialize_file: T.nilable(String), require_file: T.nilable(String)).void }
|
41
|
+
def require_bundle(initialize_file, require_file)
|
42
|
+
require(initialize_file) if initialize_file && File.exist?(initialize_file)
|
43
|
+
|
44
|
+
runtime = Bundler::Runtime.new(File.dirname(gemfile.path), definition)
|
45
|
+
groups = Bundler.definition.groups
|
46
|
+
runtime.setup(*groups).require(*groups)
|
47
|
+
|
48
|
+
require(require_file) if require_file && File.exist?(require_file)
|
49
|
+
|
50
|
+
load_rails_engines
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
sig { returns(File) }
|
56
|
+
attr_reader(:gemfile, :lockfile)
|
57
|
+
|
58
|
+
sig { returns(Bundler::Definition) }
|
59
|
+
def definition
|
60
|
+
@definition ||= Bundler::Dsl.evaluate(gemfile, lockfile, {})
|
61
|
+
end
|
62
|
+
|
63
|
+
sig { params(spec: Spec).returns(T::Boolean) }
|
64
|
+
def ignore_gem_spec?(spec)
|
65
|
+
spec.name == "sorbet" ||
|
66
|
+
spec.full_gem_path.start_with?(gemfile_dir)
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(String) }
|
70
|
+
def gemfile_dir
|
71
|
+
File.expand_path(gemfile.path + "/..")
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { void }
|
75
|
+
def load_rails_engines
|
76
|
+
return unless Object.const_defined?("Rails::Engine")
|
77
|
+
engines = Object.const_get("Rails::Engine").descendants.reject(&:abstract_railtie?)
|
78
|
+
|
79
|
+
engines.each do |engine|
|
80
|
+
errored_files = []
|
81
|
+
|
82
|
+
engine.config.eager_load_paths.each do |load_path|
|
83
|
+
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
|
84
|
+
require(file)
|
85
|
+
rescue LoadError, StandardError
|
86
|
+
errored_files << file
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Try files that have errored one more time
|
91
|
+
# It might have been a load order problem
|
92
|
+
errored_files.each do |file|
|
93
|
+
require(file)
|
94
|
+
rescue LoadError, StandardError
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Gem
|
101
|
+
extend(T::Sig)
|
102
|
+
|
103
|
+
sig { params(spec: Spec).void }
|
104
|
+
def initialize(spec)
|
105
|
+
@spec = T.let(spec, Spec)
|
106
|
+
end
|
107
|
+
|
108
|
+
sig { returns(String) }
|
109
|
+
def full_gem_path
|
110
|
+
@spec.full_gem_path.to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
sig { returns(T::Array[Pathname]) }
|
114
|
+
def files
|
115
|
+
@spec.full_require_paths.flat_map do |path|
|
116
|
+
Pathname.new(path).glob("**/*.rb")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
sig { returns(String) }
|
121
|
+
def name
|
122
|
+
@spec.name
|
123
|
+
end
|
124
|
+
|
125
|
+
sig { returns(::Gem::Version) }
|
126
|
+
def version
|
127
|
+
@spec.version
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { returns(String) }
|
131
|
+
def rbi_file_name
|
132
|
+
"#{name}@#{version}.rbi"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
require 'thor'
|
6
|
+
|
7
|
+
module Tapioca
|
8
|
+
class Generator < ::Thor::Shell::Color
|
9
|
+
extend(T::Sig)
|
10
|
+
|
11
|
+
DEFAULT_OUTDIR = "sorbet/rbi/gems"
|
12
|
+
|
13
|
+
sig { returns(Pathname) }
|
14
|
+
attr_reader :outdir
|
15
|
+
sig { returns(T.nilable(String)) }
|
16
|
+
attr_reader :prerequire
|
17
|
+
sig { returns(T.nilable(String)) }
|
18
|
+
attr_reader :postrequire
|
19
|
+
sig { returns(String) }
|
20
|
+
attr_reader :command
|
21
|
+
sig { returns(T::Hash[String, String]) }
|
22
|
+
attr_reader :typed_overrides
|
23
|
+
|
24
|
+
sig do
|
25
|
+
params(
|
26
|
+
outdir: T.nilable(String),
|
27
|
+
prerequire: T.nilable(String),
|
28
|
+
postrequire: T.nilable(String),
|
29
|
+
command: T.nilable(String),
|
30
|
+
typed_overrides: T.nilable(T::Hash[String, String])
|
31
|
+
).void
|
32
|
+
end
|
33
|
+
def initialize(outdir: nil, prerequire: nil, postrequire: nil, command: nil, typed_overrides: nil)
|
34
|
+
@outdir = T.let(Pathname.new(outdir || DEFAULT_OUTDIR), Pathname)
|
35
|
+
@prerequire = T.let(prerequire, T.nilable(String))
|
36
|
+
@postrequire = T.let(postrequire, T.nilable(String))
|
37
|
+
@command = T.let(command || default_command, String)
|
38
|
+
@typed_overrides = T.let(typed_overrides || {}, T::Hash[String, String])
|
39
|
+
@bundle = T.let(nil, T.nilable(Gemfile))
|
40
|
+
@compiler = T.let(nil, T.nilable(Compilers::SymbolTableCompiler))
|
41
|
+
@existing_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
42
|
+
@expected_rbis = T.let(nil, T.nilable(T::Hash[String, String]))
|
43
|
+
super()
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(gem_names: T::Array[String]).void }
|
47
|
+
def build_gem_rbis(gem_names)
|
48
|
+
require_gem_file
|
49
|
+
|
50
|
+
gems_to_generate(gem_names).map do |gem|
|
51
|
+
say("Processing '#{gem.name}' gem:", :green)
|
52
|
+
indent do
|
53
|
+
compile_rbi(gem)
|
54
|
+
puts
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
say("All operations performed in working directory.", [:green, :bold])
|
59
|
+
say("Please review changes and commit them.", [:green, :bold])
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { void }
|
63
|
+
def sync_rbis_with_gemfile
|
64
|
+
anything_done = [
|
65
|
+
perform_removals,
|
66
|
+
perform_additions,
|
67
|
+
].any?
|
68
|
+
|
69
|
+
if anything_done
|
70
|
+
say("All operations performed in working directory.", [:green, :bold])
|
71
|
+
say("Please review changes and commit them.", [:green, :bold])
|
72
|
+
else
|
73
|
+
say("No operations performed, all RBIs are up-to-date.", [:green, :bold])
|
74
|
+
end
|
75
|
+
|
76
|
+
puts
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
sig { returns(String) }
|
82
|
+
def default_command
|
83
|
+
command = File.basename($PROGRAM_NAME)
|
84
|
+
args = ARGV.join(" ")
|
85
|
+
|
86
|
+
"#{command} #{args}"
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { returns(Gemfile) }
|
90
|
+
def bundle
|
91
|
+
@bundle ||= Gemfile.new
|
92
|
+
end
|
93
|
+
|
94
|
+
sig { returns(Compilers::SymbolTableCompiler) }
|
95
|
+
def compiler
|
96
|
+
@compiler ||= Compilers::SymbolTableCompiler.new
|
97
|
+
end
|
98
|
+
|
99
|
+
sig { void }
|
100
|
+
def require_gem_file
|
101
|
+
say("Requiring all gems to prepare for compiling... ")
|
102
|
+
bundle.require_bundle(prerequire, postrequire)
|
103
|
+
say(" Done", :green)
|
104
|
+
puts
|
105
|
+
end
|
106
|
+
|
107
|
+
sig { returns(T::Hash[String, String]) }
|
108
|
+
def existing_rbis
|
109
|
+
@existing_rbis ||= Dir.glob("*@*.rbi", T.unsafe(base: outdir))
|
110
|
+
.map { |f| File.basename(f, ".*").split('@') }
|
111
|
+
.to_h
|
112
|
+
end
|
113
|
+
|
114
|
+
sig { returns(T::Hash[String, String]) }
|
115
|
+
def expected_rbis
|
116
|
+
@expected_rbis ||= bundle.dependencies
|
117
|
+
.map { |gem| [gem.name, gem.version.to_s] }
|
118
|
+
.to_h
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { params(gem_name: String, version: String).returns(Pathname) }
|
122
|
+
def rbi_filename(gem_name, version)
|
123
|
+
outdir / "#{gem_name}@#{version}.rbi"
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { params(gem_name: String).returns(Pathname) }
|
127
|
+
def existing_rbi(gem_name)
|
128
|
+
rbi_filename(gem_name, T.must(existing_rbis[gem_name]))
|
129
|
+
end
|
130
|
+
|
131
|
+
sig { params(gem_name: String).returns(Pathname) }
|
132
|
+
def expected_rbi(gem_name)
|
133
|
+
rbi_filename(gem_name, T.must(expected_rbis[gem_name]))
|
134
|
+
end
|
135
|
+
|
136
|
+
sig { params(gem_name: String).returns(T::Boolean) }
|
137
|
+
def rbi_exists?(gem_name)
|
138
|
+
existing_rbis.key?(gem_name)
|
139
|
+
end
|
140
|
+
|
141
|
+
sig { returns(T::Array[String]) }
|
142
|
+
def removed_rbis
|
143
|
+
(existing_rbis.keys - expected_rbis.keys).sort
|
144
|
+
end
|
145
|
+
|
146
|
+
sig { returns(T::Array[String]) }
|
147
|
+
def added_rbis
|
148
|
+
expected_rbis.select do |name, value|
|
149
|
+
existing_rbis[name] != value
|
150
|
+
end.keys.sort
|
151
|
+
end
|
152
|
+
|
153
|
+
sig { params(filename: Pathname).void }
|
154
|
+
def add(filename)
|
155
|
+
say("++ Adding: #{filename}")
|
156
|
+
end
|
157
|
+
|
158
|
+
sig { params(filename: Pathname).void }
|
159
|
+
def remove(filename)
|
160
|
+
say("-- Removing: #{filename}")
|
161
|
+
filename.unlink
|
162
|
+
end
|
163
|
+
|
164
|
+
sig { params(old_filename: Pathname, new_filename: Pathname).void }
|
165
|
+
def move(old_filename, new_filename)
|
166
|
+
say("-> Moving: #{old_filename} to #{new_filename}")
|
167
|
+
old_filename.rename(new_filename.to_s)
|
168
|
+
end
|
169
|
+
|
170
|
+
sig { void }
|
171
|
+
def perform_removals
|
172
|
+
say("Removing RBI files of gems that have been removed:", [:blue, :bold])
|
173
|
+
puts
|
174
|
+
|
175
|
+
anything_done = T.let(false, T::Boolean)
|
176
|
+
|
177
|
+
gems = removed_rbis
|
178
|
+
|
179
|
+
indent do
|
180
|
+
if gems.empty?
|
181
|
+
say("Nothing to do.")
|
182
|
+
else
|
183
|
+
gems.each do |removed|
|
184
|
+
filename = existing_rbi(removed)
|
185
|
+
remove(filename)
|
186
|
+
end
|
187
|
+
|
188
|
+
anything_done = true
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
puts
|
193
|
+
|
194
|
+
anything_done
|
195
|
+
end
|
196
|
+
|
197
|
+
sig { void }
|
198
|
+
def perform_additions
|
199
|
+
say("Generating RBI files of gems that are added or updated:", [:blue, :bold])
|
200
|
+
puts
|
201
|
+
|
202
|
+
anything_done = T.let(false, T::Boolean)
|
203
|
+
|
204
|
+
gems = added_rbis
|
205
|
+
|
206
|
+
indent do
|
207
|
+
if gems.empty?
|
208
|
+
say("Nothing to do.")
|
209
|
+
else
|
210
|
+
require_gem_file
|
211
|
+
|
212
|
+
gems.each do |gem_name|
|
213
|
+
filename = expected_rbi(gem_name)
|
214
|
+
|
215
|
+
if rbi_exists?(gem_name)
|
216
|
+
old_filename = existing_rbi(gem_name)
|
217
|
+
move(old_filename, filename) unless old_filename == filename
|
218
|
+
end
|
219
|
+
|
220
|
+
gem = T.must(bundle.gem(gem_name))
|
221
|
+
compile_rbi(gem)
|
222
|
+
add(filename)
|
223
|
+
|
224
|
+
puts
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
anything_done = true
|
229
|
+
end
|
230
|
+
|
231
|
+
puts
|
232
|
+
|
233
|
+
anything_done
|
234
|
+
end
|
235
|
+
|
236
|
+
sig do
|
237
|
+
params(gem_names: T::Array[String])
|
238
|
+
.returns(T::Array[Gemfile::Gem])
|
239
|
+
end
|
240
|
+
def gems_to_generate(gem_names)
|
241
|
+
return bundle.dependencies if gem_names.empty?
|
242
|
+
|
243
|
+
gem_names.map do |gem_name|
|
244
|
+
gem = bundle.gem(gem_name)
|
245
|
+
if gem.nil?
|
246
|
+
say("Error: Cannot find gem '#{gem_name}'", :red)
|
247
|
+
exit(1)
|
248
|
+
end
|
249
|
+
gem
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
sig { params(command: String, typed_sigil: String).returns(String) }
|
254
|
+
def rbi_header(command, typed_sigil)
|
255
|
+
<<~HEAD
|
256
|
+
# This file is autogenerated. Do not edit it by hand. Regenerate it with:
|
257
|
+
# #{command}
|
258
|
+
|
259
|
+
# typed: #{typed_sigil}
|
260
|
+
|
261
|
+
HEAD
|
262
|
+
end
|
263
|
+
|
264
|
+
sig { params(gem: Gemfile::Gem).void }
|
265
|
+
def compile_rbi(gem)
|
266
|
+
compiler = Compilers::SymbolTableCompiler.new
|
267
|
+
gem_name = set_color(gem.name, :yellow, :bold)
|
268
|
+
say("Compiling #{gem_name}, this may take a few seconds... ")
|
269
|
+
|
270
|
+
typed_sigil = typed_overrides.fetch(gem.name, "true")
|
271
|
+
|
272
|
+
content = compiler.compile(gem)
|
273
|
+
content.prepend(rbi_header(command, typed_sigil))
|
274
|
+
|
275
|
+
FileUtils.mkdir_p(outdir)
|
276
|
+
filename = outdir / gem.rbi_file_name
|
277
|
+
File.write(filename.to_s, content)
|
278
|
+
|
279
|
+
say("Done", :green)
|
280
|
+
|
281
|
+
outdir.glob("#{gem.name}@*.rbi") do |file|
|
282
|
+
remove(file) unless file.basename.to_s == gem.rbi_file_name
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|