tapioca 0.5.0 → 0.5.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 +1 -1
- data/lib/tapioca/cli.rb +55 -139
- data/lib/tapioca/compilers/dsl/active_model_secure_password.rb +101 -0
- data/lib/tapioca/compilers/dsl/active_record_fixtures.rb +86 -0
- data/lib/tapioca/compilers/dsl/active_support_concern.rb +0 -2
- data/lib/tapioca/compilers/dsl/base.rb +12 -0
- data/lib/tapioca/compilers/dsl/mixed_in_class_attributes.rb +74 -0
- data/lib/tapioca/compilers/dsl/smart_properties.rb +4 -4
- data/lib/tapioca/compilers/dsl_compiler.rb +7 -6
- data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +198 -0
- data/lib/tapioca/compilers/sorbet.rb +0 -1
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +84 -153
- data/lib/tapioca/compilers/symbol_table_compiler.rb +5 -11
- data/lib/tapioca/config.rb +1 -0
- data/lib/tapioca/config_builder.rb +1 -0
- data/lib/tapioca/gemfile.rb +11 -5
- data/lib/tapioca/generators/base.rb +61 -0
- data/lib/tapioca/generators/dsl.rb +362 -0
- data/lib/tapioca/generators/gem.rb +345 -0
- data/lib/tapioca/generators/init.rb +79 -0
- data/lib/tapioca/generators/require.rb +52 -0
- data/lib/tapioca/generators/todo.rb +76 -0
- data/lib/tapioca/generators.rb +9 -0
- data/lib/tapioca/internal.rb +1 -2
- data/lib/tapioca/loader.rb +2 -2
- data/lib/tapioca/rbi_ext/model.rb +44 -0
- data/lib/tapioca/reflection.rb +8 -1
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +2 -0
- metadata +16 -6
- data/lib/tapioca/generator.rb +0 -717
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28c385b223b0ab1221b81420ad3a40222ff243552e121959e1e2290b1711357e
|
4
|
+
data.tar.gz: 1c06cd621dee12c9afd506a669c2762d2c7ca2f471f2a22b4e38bde6092f9f13
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8f6278d058bef31b5fdac1d94556814de7486355d1bcdf9fb2e29da6f33743ec5c330befb492b51ada82d392d10bae2e0c663eda7a365c1272631add49adce6
|
7
|
+
data.tar.gz: 4a342639d1ab6a79b83f6273ac9d44589919953f70773d0492c8c038250f344541d280801db88be72883dd8c55ff8ccd9fafa9ffcbff32e5421df2e821b59365
|
data/Gemfile
CHANGED
@@ -11,7 +11,6 @@ gem("pry-byebug")
|
|
11
11
|
gem("rubocop-shopify", require: false)
|
12
12
|
gem("rubocop-sorbet", ">= 0.4.1")
|
13
13
|
gem("sorbet")
|
14
|
-
gem("yard", "~> 0.9.25")
|
15
14
|
|
16
15
|
group(:deployment, :development) do
|
17
16
|
gem("rake")
|
@@ -35,4 +34,5 @@ group(:development, :test) do
|
|
35
34
|
gem("nokogiri", require: false)
|
36
35
|
gem("config", require: false)
|
37
36
|
gem("aasm", require: false)
|
37
|
+
gem("bcrypt", require: false)
|
38
38
|
end
|
data/lib/tapioca/cli.rb
CHANGED
@@ -5,8 +5,6 @@ require "thor"
|
|
5
5
|
|
6
6
|
module Tapioca
|
7
7
|
class Cli < Thor
|
8
|
-
include(Thor::Actions)
|
9
|
-
|
10
8
|
class_option :outdir,
|
11
9
|
aliases: ["--out", "-o"],
|
12
10
|
banner: "directory",
|
@@ -29,22 +27,37 @@ module Tapioca
|
|
29
27
|
|
30
28
|
desc "init", "initializes folder structure"
|
31
29
|
def init
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
generator = Generators::Init.new(
|
31
|
+
sorbet_config: Config::SORBET_CONFIG,
|
32
|
+
default_postrequire: Config::DEFAULT_POSTREQUIRE,
|
33
|
+
default_command: Config::DEFAULT_COMMAND
|
34
|
+
)
|
35
|
+
generator.generate
|
35
36
|
end
|
36
37
|
|
37
38
|
desc "require", "generate the list of files to be required by tapioca"
|
38
39
|
def require
|
40
|
+
generator = Generators::Require.new(
|
41
|
+
requires_path: ConfigBuilder.from_options(:require, options).postrequire,
|
42
|
+
sorbet_config_path: Config::SORBET_CONFIG,
|
43
|
+
default_command: Config::DEFAULT_COMMAND
|
44
|
+
)
|
39
45
|
Tapioca.silence_warnings do
|
40
|
-
generator.
|
46
|
+
generator.generate
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
44
50
|
desc "todo", "generate the list of unresolved constants"
|
45
51
|
def todo
|
52
|
+
current_command = T.must(current_command_chain.first)
|
53
|
+
config = ConfigBuilder.from_options(current_command, options)
|
54
|
+
generator = Generators::Todo.new(
|
55
|
+
todos_path: config.todos_path,
|
56
|
+
file_header: config.file_header,
|
57
|
+
default_command: Config::DEFAULT_COMMAND
|
58
|
+
)
|
46
59
|
Tapioca.silence_warnings do
|
47
|
-
generator.
|
60
|
+
generator.generate
|
48
61
|
end
|
49
62
|
end
|
50
63
|
|
@@ -67,13 +80,23 @@ module Tapioca
|
|
67
80
|
type: :boolean,
|
68
81
|
desc: "Supresses file creation output"
|
69
82
|
def dsl(*constants)
|
83
|
+
current_command = T.must(current_command_chain.first)
|
84
|
+
config = ConfigBuilder.from_options(current_command, options)
|
85
|
+
generator = Generators::Dsl.new(
|
86
|
+
requested_constants: constants,
|
87
|
+
outpath: config.outpath,
|
88
|
+
generators: config.generators,
|
89
|
+
exclude_generators: config.exclude_generators,
|
90
|
+
file_header: config.file_header,
|
91
|
+
compiler_path: Tapioca::Compilers::Dsl::COMPILERS_PATH,
|
92
|
+
tapioca_path: Config::TAPIOCA_PATH,
|
93
|
+
default_command: Config::DEFAULT_COMMAND,
|
94
|
+
should_verify: options[:verify],
|
95
|
+
quiet: options[:quiet],
|
96
|
+
verbose: options[:verbose]
|
97
|
+
)
|
70
98
|
Tapioca.silence_warnings do
|
71
|
-
generator.
|
72
|
-
constants,
|
73
|
-
should_verify: options[:verify],
|
74
|
-
quiet: options[:quiet],
|
75
|
-
verbose: options[:verbose]
|
76
|
-
)
|
99
|
+
generator.generate
|
77
100
|
end
|
78
101
|
end
|
79
102
|
|
@@ -104,10 +127,27 @@ module Tapioca
|
|
104
127
|
type: :boolean,
|
105
128
|
default: false,
|
106
129
|
desc: "Verifies RBIs are up-to-date"
|
130
|
+
option :doc,
|
131
|
+
type: :boolean,
|
132
|
+
default: false,
|
133
|
+
desc: "Include YARD documentation from sources when generating RBIs. Warning: this might be slow"
|
107
134
|
def gem(*gems)
|
108
135
|
Tapioca.silence_warnings do
|
109
136
|
all = options[:all]
|
110
137
|
verify = options[:verify]
|
138
|
+
current_command = T.must(current_command_chain.first)
|
139
|
+
config = ConfigBuilder.from_options(current_command, options)
|
140
|
+
generator = Generators::Gem.new(
|
141
|
+
gem_names: all ? [] : gems,
|
142
|
+
gem_excludes: config.exclude,
|
143
|
+
prerequire: config.prerequire,
|
144
|
+
postrequire: config.postrequire,
|
145
|
+
typed_overrides: config.typed_overrides,
|
146
|
+
default_command: Config::DEFAULT_COMMAND,
|
147
|
+
outpath: config.outpath,
|
148
|
+
file_header: config.file_header,
|
149
|
+
doc: config.doc
|
150
|
+
)
|
111
151
|
|
112
152
|
raise MalformattedArgumentError, "Options '--all' and '--verify' are mutually exclusive" if all && verify
|
113
153
|
|
@@ -117,146 +157,22 @@ module Tapioca
|
|
117
157
|
end
|
118
158
|
|
119
159
|
if gems.empty? && !all
|
120
|
-
generator.
|
160
|
+
generator.sync(should_verify: verify)
|
121
161
|
else
|
122
|
-
generator.
|
162
|
+
generator.generate
|
123
163
|
end
|
124
164
|
end
|
125
165
|
end
|
126
166
|
|
127
|
-
desc "generate [gem...]", "DEPRECATED: generate RBIs from gems"
|
128
|
-
option :prerequire,
|
129
|
-
aliases: ["--pre", "-b"],
|
130
|
-
banner: "file",
|
131
|
-
desc: "A file to be required before Bundler.require is called"
|
132
|
-
option :postrequire,
|
133
|
-
aliases: ["--post", "-a"],
|
134
|
-
banner: "file",
|
135
|
-
desc: "A file to be required after Bundler.require is called"
|
136
|
-
option :exclude,
|
137
|
-
aliases: ["-x"],
|
138
|
-
type: :array,
|
139
|
-
banner: "gem [gem ...]",
|
140
|
-
desc: "Excludes the given gem(s) from RBI generation"
|
141
|
-
option :typed_overrides,
|
142
|
-
aliases: ["--typed", "-t"],
|
143
|
-
type: :hash,
|
144
|
-
banner: "gem:level [gem:level ...]",
|
145
|
-
desc: "Overrides for typed sigils for generated gem RBIs"
|
146
|
-
def generate(*gems)
|
147
|
-
gem_names = if gems.empty?
|
148
|
-
"--all"
|
149
|
-
else
|
150
|
-
gems.join(" ")
|
151
|
-
end
|
152
|
-
deprecation_message = <<~MSG
|
153
|
-
DEPRECATION: The `generate` command will be removed in a future release.
|
154
|
-
|
155
|
-
Start using `bin/tapioca gem #{gem_names}` instead.
|
156
|
-
MSG
|
157
|
-
|
158
|
-
say(deprecation_message, :red)
|
159
|
-
say("")
|
160
|
-
|
161
|
-
Tapioca.silence_warnings do
|
162
|
-
generator.build_gem_rbis(gems)
|
163
|
-
end
|
164
|
-
|
165
|
-
say("")
|
166
|
-
say(deprecation_message, :red)
|
167
|
-
end
|
168
|
-
|
169
|
-
desc "sync", "DEPRECATED: sync RBIs to Gemfile"
|
170
|
-
option :prerequire,
|
171
|
-
aliases: ["--pre", "-b"],
|
172
|
-
banner: "file",
|
173
|
-
desc: "A file to be required before Bundler.require is called"
|
174
|
-
option :postrequire,
|
175
|
-
aliases: ["--post", "-a"],
|
176
|
-
banner: "file",
|
177
|
-
desc: "A file to be required after Bundler.require is called"
|
178
|
-
option :exclude,
|
179
|
-
aliases: ["-x"],
|
180
|
-
type: :array,
|
181
|
-
banner: "gem [gem ...]",
|
182
|
-
desc: "Excludes the given gem(s) from RBI generation"
|
183
|
-
option :typed_overrides,
|
184
|
-
aliases: ["--typed", "-t"],
|
185
|
-
type: :hash,
|
186
|
-
banner: "gem:level [gem:level ...]",
|
187
|
-
desc: "Overrides for typed sigils for generated gem RBIs"
|
188
|
-
option :verify,
|
189
|
-
type: :boolean,
|
190
|
-
default: false,
|
191
|
-
desc: "Verifies RBIs are up-to-date"
|
192
|
-
def sync
|
193
|
-
deprecation_message = <<~MSG
|
194
|
-
DEPRECATION: The `sync` command will be removed in a future release.
|
195
|
-
|
196
|
-
Start using `bin/tapioca gem` instead.
|
197
|
-
MSG
|
198
|
-
|
199
|
-
say(deprecation_message, :red)
|
200
|
-
say("")
|
201
|
-
|
202
|
-
Tapioca.silence_warnings do
|
203
|
-
generator.sync_rbis_with_gemfile(should_verify: options[:verify])
|
204
|
-
end
|
205
|
-
|
206
|
-
say("")
|
207
|
-
say(deprecation_message, :red)
|
208
|
-
end
|
209
|
-
|
210
167
|
desc "--version, -v", "show version"
|
211
168
|
def __print_version
|
212
169
|
puts "Tapioca v#{Tapioca::VERSION}"
|
213
170
|
end
|
214
171
|
|
215
|
-
private
|
216
|
-
|
217
|
-
def create_config
|
218
|
-
create_file(Config::SORBET_CONFIG, skip: true) do
|
219
|
-
<<~CONTENT
|
220
|
-
--dir
|
221
|
-
.
|
222
|
-
CONTENT
|
223
|
-
end
|
224
|
-
end
|
225
|
-
|
226
|
-
def create_post_require
|
227
|
-
create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
|
228
|
-
<<~CONTENT
|
229
|
-
# typed: true
|
230
|
-
# frozen_string_literal: true
|
231
|
-
|
232
|
-
# Add your extra requires here (`tapioca require` can be used to boostrap this list)
|
233
|
-
CONTENT
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
def generate_binstub
|
238
|
-
bin_stub_exists = File.exist?("bin/tapioca")
|
239
|
-
installer = Bundler::Installer.new(Bundler.root, Bundler.definition)
|
240
|
-
spec = Bundler.definition.specs.find { |s| s.name == "tapioca" }
|
241
|
-
installer.generate_bundler_executable_stubs(spec, { force: true })
|
242
|
-
if bin_stub_exists
|
243
|
-
shell.say_status(:force, "bin/tapioca", :yellow)
|
244
|
-
else
|
245
|
-
shell.say_status(:create, "bin/tapioca", :green)
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
172
|
no_commands do
|
250
173
|
def self.exit_on_failure?
|
251
174
|
true
|
252
175
|
end
|
253
|
-
|
254
|
-
def generator
|
255
|
-
current_command = T.must(current_command_chain.first)
|
256
|
-
@generator ||= Generator.new(
|
257
|
-
ConfigBuilder.from_options(current_command, options)
|
258
|
-
)
|
259
|
-
end
|
260
176
|
end
|
261
177
|
end
|
262
178
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "active_model"
|
6
|
+
rescue LoadError
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
module Tapioca
|
11
|
+
module Compilers
|
12
|
+
module Dsl
|
13
|
+
# `Tapioca::Compilers::Dsl::ActiveModelSecurePassword` decorates RBI files for all
|
14
|
+
# classes that use [`ActiveModel::SecurePassword`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html).
|
15
|
+
#
|
16
|
+
# For example, with the following class:
|
17
|
+
#
|
18
|
+
# ~~~rb
|
19
|
+
# class User
|
20
|
+
# include ActiveModel::SecurePassword
|
21
|
+
#
|
22
|
+
# has_secure_password
|
23
|
+
# has_secure_password :token
|
24
|
+
# end
|
25
|
+
# ~~~
|
26
|
+
#
|
27
|
+
# this generator will produce an RBI file with the following content:
|
28
|
+
# ~~~rbi
|
29
|
+
# # typed: true
|
30
|
+
#
|
31
|
+
# class User
|
32
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
33
|
+
# def authenticate(unencrypted_password); end
|
34
|
+
#
|
35
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
36
|
+
# def authenticate_password(unencrypted_password); end
|
37
|
+
#
|
38
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
39
|
+
# def authenticate_token(unencrypted_password); end
|
40
|
+
#
|
41
|
+
# sig { returns(T.untyped) }
|
42
|
+
# def password; end
|
43
|
+
#
|
44
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
45
|
+
# def password=(unencrypted_password); end
|
46
|
+
#
|
47
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
48
|
+
# def password_confirmation=(unencrypted_password); end
|
49
|
+
#
|
50
|
+
# sig { returns(T.untyped) }
|
51
|
+
# def token; end
|
52
|
+
#
|
53
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
54
|
+
# def token=(unencrypted_password); end
|
55
|
+
#
|
56
|
+
# sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
|
57
|
+
# def token_confirmation=(unencrypted_password); end
|
58
|
+
# end
|
59
|
+
# ~~~
|
60
|
+
class ActiveModelSecurePassword < Base
|
61
|
+
extend T::Sig
|
62
|
+
|
63
|
+
sig do
|
64
|
+
override
|
65
|
+
.params(root: RBI::Tree, constant: T.all(Class, ::ActiveModel::SecurePassword::ClassMethods))
|
66
|
+
.void
|
67
|
+
end
|
68
|
+
def decorate(root, constant)
|
69
|
+
instance_methods_modules = if constant < ActiveModel::SecurePassword::InstanceMethodsOnActivation
|
70
|
+
# pre Rails 6.0, this used to be a single static module
|
71
|
+
[ActiveModel::SecurePassword::InstanceMethodsOnActivation]
|
72
|
+
else
|
73
|
+
# post Rails 6.0, this is now using a dynmaic module builder pattern
|
74
|
+
# and we can have multiple different ones included into the model
|
75
|
+
constant.ancestors.grep(ActiveModel::SecurePassword::InstanceMethodsOnActivation)
|
76
|
+
end
|
77
|
+
|
78
|
+
return if instance_methods_modules.empty?
|
79
|
+
|
80
|
+
methods = instance_methods_modules.flat_map { |mod| mod.instance_methods(false) }
|
81
|
+
return if methods.empty?
|
82
|
+
|
83
|
+
root.create_path(constant) do |klass|
|
84
|
+
methods.each do |method|
|
85
|
+
create_method_from_def(klass, constant.instance_method(method))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { override.returns(T::Enumerable[Module]) }
|
91
|
+
def gather_constants
|
92
|
+
# This selects all classes that are `ActiveModel::SecurePassword::ClassMethods === klass`.
|
93
|
+
# In other words, we select all classes that have `ActiveModel::SecurePassword::ClassMethods`
|
94
|
+
# as an ancestor of its singleton class, i.e. all classes that have extended the
|
95
|
+
# `ActiveModel::SecurePassword::ClassMethods` module.
|
96
|
+
all_classes.grep(::ActiveModel::SecurePassword::ClassMethods)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "rails"
|
6
|
+
require "active_record"
|
7
|
+
require "active_record/fixtures"
|
8
|
+
require "active_support/test_case"
|
9
|
+
rescue LoadError
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
module Tapioca
|
14
|
+
module Compilers
|
15
|
+
module Dsl
|
16
|
+
# `Tapioca::Compilers::Dsl::ActiveRecordFixtures` decorates RBIs for test fixture methods
|
17
|
+
# that are created dynamically by Rails.
|
18
|
+
#
|
19
|
+
# For example, given an application with a posts table, we can have a fixture file
|
20
|
+
#
|
21
|
+
# ~~~yaml
|
22
|
+
# first_post:
|
23
|
+
# author: John
|
24
|
+
# title: My post
|
25
|
+
# ~~~
|
26
|
+
#
|
27
|
+
# Rails will allow us to invoke `posts(:first_post)` in tests to get the fixture record.
|
28
|
+
# The generated RBI by this generator will produce the following
|
29
|
+
#
|
30
|
+
# ~~~rbi
|
31
|
+
# # test_case.rbi
|
32
|
+
# # typed: true
|
33
|
+
# class ActiveSupport::TestCase
|
34
|
+
# sig { params(fixture_names: Symbol).returns(T.untyped) }
|
35
|
+
# def posts(*fixture_names); end
|
36
|
+
# end
|
37
|
+
# ~~~
|
38
|
+
class ActiveRecordFixtures < Base
|
39
|
+
extend T::Sig
|
40
|
+
|
41
|
+
sig { override.params(root: RBI::Tree, constant: T.class_of(ActiveSupport::TestCase)).void }
|
42
|
+
def decorate(root, constant)
|
43
|
+
method_names = fixture_loader.ancestors # get all ancestors from class that includes AR fixtures
|
44
|
+
.drop(1) # drop the anonymous class itself from the array
|
45
|
+
.reject(&:name) # only collect anonymous ancestors because fixture methods are always on an anonymous module
|
46
|
+
.map! do |mod|
|
47
|
+
[mod.private_instance_methods(false), mod.instance_methods(false)]
|
48
|
+
end
|
49
|
+
.flatten # merge methods into a single list
|
50
|
+
return if method_names.empty?
|
51
|
+
|
52
|
+
root.create_path(constant) do |mod|
|
53
|
+
method_names.each do |name|
|
54
|
+
create_fixture_method(mod, name.to_s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { override.returns(T::Enumerable[Module]) }
|
60
|
+
def gather_constants
|
61
|
+
[ActiveSupport::TestCase]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
sig { returns(Class) }
|
67
|
+
def fixture_loader
|
68
|
+
Class.new do
|
69
|
+
T.unsafe(self).include(ActiveRecord::TestFixtures)
|
70
|
+
T.unsafe(self).fixture_path = Rails.root.join("test", "fixtures")
|
71
|
+
T.unsafe(self).fixtures(:all)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { params(mod: RBI::Scope, name: String).void }
|
76
|
+
def create_fixture_method(mod, name)
|
77
|
+
mod.create_method(
|
78
|
+
name,
|
79
|
+
parameters: [create_rest_param("fixture_names", type: "Symbol")],
|
80
|
+
return_type: "T.untyped"
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -6,6 +6,8 @@ require "tapioca/rbi_ext/model"
|
|
6
6
|
module Tapioca
|
7
7
|
module Compilers
|
8
8
|
module Dsl
|
9
|
+
COMPILERS_PATH = T.let(File.expand_path("..", __FILE__).to_s, String)
|
10
|
+
|
9
11
|
class Base
|
10
12
|
extend T::Sig
|
11
13
|
extend T::Helpers
|
@@ -17,9 +19,13 @@ module Tapioca
|
|
17
19
|
sig { returns(T::Set[Module]) }
|
18
20
|
attr_reader :processable_constants
|
19
21
|
|
22
|
+
sig { returns(T::Array[String]) }
|
23
|
+
attr_reader :errors
|
24
|
+
|
20
25
|
sig { void }
|
21
26
|
def initialize
|
22
27
|
@processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
|
28
|
+
@errors = T.let([], T::Array[String])
|
23
29
|
end
|
24
30
|
|
25
31
|
sig { params(constant: Module).returns(T::Boolean) }
|
@@ -41,6 +47,12 @@ module Tapioca
|
|
41
47
|
sig { abstract.returns(T::Enumerable[Module]) }
|
42
48
|
def gather_constants; end
|
43
49
|
|
50
|
+
# NOTE: This should eventually accept an `Error` object or `Exception` rather than simply a `String`.
|
51
|
+
sig { params(error: String).void }
|
52
|
+
def add_error(error)
|
53
|
+
@errors << error
|
54
|
+
end
|
55
|
+
|
44
56
|
private
|
45
57
|
|
46
58
|
sig { returns(T::Enumerable[Class]) }
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
begin
|
5
|
+
require "active_support/core_ext/class/attribute"
|
6
|
+
rescue LoadError
|
7
|
+
return
|
8
|
+
end
|
9
|
+
|
10
|
+
module Tapioca
|
11
|
+
module Compilers
|
12
|
+
module Dsl
|
13
|
+
# `Tapioca::Compilers::Dsl::MixedInClassAttributes` generates RBI files for modules that dynamically use
|
14
|
+
# `class_attribute` on classes.
|
15
|
+
#
|
16
|
+
# For example, given the following concern
|
17
|
+
#
|
18
|
+
# ~~~rb
|
19
|
+
# module Taggeable
|
20
|
+
# extend ActiveSupport::Concern
|
21
|
+
#
|
22
|
+
# included do
|
23
|
+
# class_attribute :tag
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# ~~~
|
27
|
+
#
|
28
|
+
# this generator will produce the RBI file `taggeable.rbi` with the following content:
|
29
|
+
#
|
30
|
+
# ~~~rbi
|
31
|
+
# # typed: strong
|
32
|
+
#
|
33
|
+
# module Taggeable
|
34
|
+
# include GeneratedInstanceMethods
|
35
|
+
#
|
36
|
+
# mixes_in_class_methods GeneratedClassMethods
|
37
|
+
#
|
38
|
+
# module GeneratedClassMethods
|
39
|
+
# def tag; end
|
40
|
+
# def tag=(value); end
|
41
|
+
# def tag?; end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# module GeneratedInstanceMethods
|
45
|
+
# def tag; end
|
46
|
+
# def tag=(value); end
|
47
|
+
# def tag?; end
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# ~~~
|
51
|
+
class MixedInClassAttributes < Base
|
52
|
+
extend T::Sig
|
53
|
+
|
54
|
+
sig { override.params(root: RBI::Tree, constant: Module).void }
|
55
|
+
def decorate(root, constant)
|
56
|
+
mixin_compiler = DynamicMixinCompiler.new(constant)
|
57
|
+
return if mixin_compiler.empty_attributes?
|
58
|
+
|
59
|
+
root.create_path(constant) do |mod|
|
60
|
+
mixin_compiler.compile_class_attributes(mod)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { override.returns(T::Enumerable[Module]) }
|
65
|
+
def gather_constants
|
66
|
+
# Select all non-anonymous modules that have overridden Module.included
|
67
|
+
all_modules.select do |mod|
|
68
|
+
!mod.is_a?(Class) && name_of(mod) && Tapioca::Reflection.method_of(mod, :included).owner != Module
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -84,10 +84,10 @@ module Tapioca
|
|
84
84
|
|
85
85
|
sig { override.returns(T::Enumerable[Module]) }
|
86
86
|
def gather_constants
|
87
|
-
|
88
|
-
c
|
89
|
-
|
90
|
-
|
87
|
+
all_modules.select do |c|
|
88
|
+
name_of(c) &&
|
89
|
+
c != ::SmartProperties::Validations::Ancestor &&
|
90
|
+
c < ::SmartProperties && ::SmartProperties::ClassMethods === c
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
@@ -34,7 +34,7 @@ module Tapioca
|
|
34
34
|
@error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
|
35
35
|
end
|
36
36
|
|
37
|
-
sig { params(blk: T.proc.params(constant: Module, rbi:
|
37
|
+
sig { params(blk: T.proc.params(constant: Module, rbi: RBI::File).void).void }
|
38
38
|
def run(&blk)
|
39
39
|
constants_to_process = gather_constants(requested_constants)
|
40
40
|
|
@@ -51,6 +51,10 @@ module Tapioca
|
|
51
51
|
|
52
52
|
blk.call(constant, rbi)
|
53
53
|
end
|
54
|
+
|
55
|
+
generators.flat_map(&:errors).each do |msg|
|
56
|
+
report_error(msg)
|
57
|
+
end
|
54
58
|
end
|
55
59
|
|
56
60
|
private
|
@@ -77,7 +81,7 @@ module Tapioca
|
|
77
81
|
constants
|
78
82
|
end
|
79
83
|
|
80
|
-
sig { params(constant: Module).returns(T.nilable(
|
84
|
+
sig { params(constant: Module).returns(T.nilable(RBI::File)) }
|
81
85
|
def rbi_for_constant(constant)
|
82
86
|
file = RBI::File.new(strictness: "true")
|
83
87
|
|
@@ -88,10 +92,7 @@ module Tapioca
|
|
88
92
|
|
89
93
|
return if file.root.empty?
|
90
94
|
|
91
|
-
file
|
92
|
-
file.root.group_nodes!
|
93
|
-
file.root.sort_nodes!
|
94
|
-
file.string
|
95
|
+
file
|
95
96
|
end
|
96
97
|
|
97
98
|
sig { params(error: String).returns(T.noreturn) }
|