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.
@@ -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