tapioca 0.6.1 → 0.7.0
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 +13 -2
- data/README.md +79 -25
- data/Rakefile +10 -14
- data/lib/tapioca/cli.rb +66 -80
- data/lib/tapioca/{generators/base.rb → commands/command.rb} +17 -10
- data/lib/tapioca/{generators → commands}/dsl.rb +59 -45
- data/lib/tapioca/{generators → commands}/gem.rb +93 -30
- data/lib/tapioca/{generators → commands}/init.rb +9 -13
- data/lib/tapioca/{generators → commands}/require.rb +8 -10
- data/lib/tapioca/commands/todo.rb +84 -0
- data/lib/tapioca/commands.rb +13 -0
- data/lib/tapioca/dsl/compiler.rb +185 -0
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/aasm.rb +12 -9
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_controller_helpers.rb +13 -20
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/action_mailer.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_job.rb +11 -9
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_attributes.rb +32 -24
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_model_secure_password.rb +10 -12
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_associations.rb +29 -35
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_columns.rb +26 -24
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_enum.rb +14 -12
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_fixtures.rb +10 -8
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +712 -0
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_scope.rb +21 -20
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_record_typed_store.rb +12 -17
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_resource.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_storage.rb +11 -11
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_concern.rb +19 -14
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/active_support_current_attributes.rb +16 -21
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/config.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/frozen_record.rb +13 -11
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/identity_cache.rb +28 -25
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/mixed_in_class_attributes.rb +12 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/protobuf.rb +10 -8
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/rails_generators.rb +13 -14
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/sidekiq_worker.rb +14 -13
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/smart_properties.rb +12 -13
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/state_machines.rb +12 -10
- data/lib/tapioca/{compilers/dsl → dsl/compilers}/url_helpers.rb +16 -14
- data/lib/tapioca/dsl/compilers.rb +31 -0
- data/lib/tapioca/{compilers/dsl → dsl}/extensions/frozen_record.rb +2 -2
- data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +114 -0
- data/lib/tapioca/dsl/helpers/active_record_constants_helper.rb +29 -0
- data/lib/tapioca/{compilers/dsl → dsl/helpers}/param_helper.rb +2 -2
- data/lib/tapioca/{compilers/dsl_compiler.rb → dsl/pipeline.rb} +41 -33
- data/lib/tapioca/gem/events.rb +120 -0
- data/lib/tapioca/gem/listeners/base.rb +48 -0
- data/lib/tapioca/gem/listeners/dynamic_mixins.rb +32 -0
- data/lib/tapioca/gem/listeners/methods.rb +183 -0
- data/lib/tapioca/gem/listeners/mixins.rb +101 -0
- data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +21 -0
- data/lib/tapioca/gem/listeners/sorbet_enums.rb +26 -0
- data/lib/tapioca/gem/listeners/sorbet_helpers.rb +29 -0
- data/lib/tapioca/gem/listeners/sorbet_props.rb +33 -0
- data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +23 -0
- data/lib/tapioca/gem/listeners/sorbet_signatures.rb +79 -0
- data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +51 -0
- data/lib/tapioca/gem/listeners/subconstants.rb +37 -0
- data/lib/tapioca/gem/listeners/yard_doc.rb +96 -0
- data/lib/tapioca/gem/listeners.rb +16 -0
- data/lib/tapioca/gem/pipeline.rb +365 -0
- data/lib/tapioca/gemfile.rb +44 -20
- data/lib/tapioca/helpers/cli_helper.rb +16 -8
- data/lib/tapioca/helpers/config_helper.rb +113 -0
- data/lib/tapioca/helpers/rbi_helper.rb +17 -0
- data/lib/tapioca/helpers/shims_helper.rb +87 -0
- data/lib/tapioca/helpers/sorbet_helper.rb +57 -0
- data/lib/tapioca/helpers/test/dsl_compiler.rb +118 -0
- data/lib/tapioca/helpers/test/isolation.rb +1 -1
- data/lib/tapioca/helpers/test/template.rb +13 -2
- data/lib/tapioca/internal.rb +17 -10
- data/lib/tapioca/rbi_ext/model.rb +2 -48
- data/lib/tapioca/rbi_formatter.rb +37 -0
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +227 -0
- data/lib/tapioca/runtime/generic_type_registry.rb +166 -0
- data/lib/tapioca/runtime/loader.rb +123 -0
- data/lib/tapioca/runtime/reflection.rb +153 -0
- data/lib/tapioca/runtime/trackers/autoload.rb +72 -0
- data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -0
- data/lib/tapioca/runtime/trackers/mixin.rb +80 -0
- data/lib/tapioca/runtime/trackers/required_ancestor.rb +50 -0
- data/lib/tapioca/{trackers.rb → runtime/trackers.rb} +4 -3
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +110 -54
- data/lib/tapioca/sorbet_ext/name_patch.rb +7 -1
- data/lib/tapioca/{compilers → static}/requires_compiler.rb +5 -12
- data/lib/tapioca/static/symbol_loader.rb +83 -0
- data/lib/tapioca/static/symbol_table_parser.rb +63 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +2 -7
- metadata +82 -62
- data/lib/tapioca/compilers/dsl/active_record_relations.rb +0 -711
- data/lib/tapioca/compilers/dsl/base.rb +0 -179
- data/lib/tapioca/compilers/dsl/helper/active_record_constants.rb +0 -27
- data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +0 -198
- data/lib/tapioca/compilers/sorbet.rb +0 -59
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +0 -780
- data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +0 -90
- data/lib/tapioca/compilers/symbol_table_compiler.rb +0 -17
- data/lib/tapioca/compilers/todos_compiler.rb +0 -32
- data/lib/tapioca/generators/todo.rb +0 -76
- data/lib/tapioca/generators.rb +0 -9
- data/lib/tapioca/generic_type_registry.rb +0 -149
- data/lib/tapioca/helpers/active_record_column_type_helper.rb +0 -98
- data/lib/tapioca/loader.rb +0 -119
- data/lib/tapioca/reflection.rb +0 -151
- data/lib/tapioca/trackers/autoload.rb +0 -70
- data/lib/tapioca/trackers/constant_definition.rb +0 -42
- data/lib/tapioca/trackers/mixin.rb +0 -78
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Gem
|
|
8
|
+
class Pipeline
|
|
9
|
+
extend T::Sig
|
|
10
|
+
include Runtime::Reflection
|
|
11
|
+
include RBIHelper
|
|
12
|
+
|
|
13
|
+
IGNORED_SYMBOLS = T.let(["YAML", "MiniTest", "Mutex"], T::Array[String])
|
|
14
|
+
|
|
15
|
+
sig { returns(Gemfile::GemSpec) }
|
|
16
|
+
attr_reader :gem
|
|
17
|
+
|
|
18
|
+
sig { params(gem: Gemfile::GemSpec, include_doc: T::Boolean).void }
|
|
19
|
+
def initialize(gem, include_doc: false)
|
|
20
|
+
@root = T.let(RBI::Tree.new, RBI::Tree)
|
|
21
|
+
@gem = gem
|
|
22
|
+
@seen = T.let(Set.new, T::Set[String])
|
|
23
|
+
@alias_namespace = T.let(Set.new, T::Set[String])
|
|
24
|
+
|
|
25
|
+
@events = T.let([], T::Array[Gem::Event])
|
|
26
|
+
|
|
27
|
+
@payload_symbols = T.let(Static::SymbolLoader.payload_symbols, T::Set[String])
|
|
28
|
+
@bootstrap_symbols = T.let(Static::SymbolLoader.gem_symbols(@gem).union(Static::SymbolLoader.engine_symbols),
|
|
29
|
+
T::Set[String])
|
|
30
|
+
@bootstrap_symbols.each { |symbol| push_symbol(symbol) }
|
|
31
|
+
|
|
32
|
+
@node_listeners = T.let([], T::Array[Gem::Listeners::Base])
|
|
33
|
+
@node_listeners << Gem::Listeners::SorbetTypeVariables.new(self)
|
|
34
|
+
@node_listeners << Gem::Listeners::Mixins.new(self)
|
|
35
|
+
@node_listeners << Gem::Listeners::DynamicMixins.new(self)
|
|
36
|
+
@node_listeners << Gem::Listeners::Methods.new(self)
|
|
37
|
+
@node_listeners << Gem::Listeners::SorbetHelpers.new(self)
|
|
38
|
+
@node_listeners << Gem::Listeners::SorbetEnums.new(self)
|
|
39
|
+
@node_listeners << Gem::Listeners::SorbetProps.new(self)
|
|
40
|
+
@node_listeners << Gem::Listeners::SorbetRequiredAncestors.new(self)
|
|
41
|
+
@node_listeners << Gem::Listeners::SorbetSignatures.new(self)
|
|
42
|
+
@node_listeners << Gem::Listeners::Subconstants.new(self)
|
|
43
|
+
@node_listeners << Gem::Listeners::YardDoc.new(self) if include_doc
|
|
44
|
+
@node_listeners << Gem::Listeners::RemoveEmptyPayloadScopes.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(RBI::Tree) }
|
|
48
|
+
def compile
|
|
49
|
+
dispatch(next_event) until @events.empty?
|
|
50
|
+
@root
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(symbol: String).void }
|
|
54
|
+
def push_symbol(symbol)
|
|
55
|
+
@events << Gem::SymbolFound.new(symbol)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
|
|
59
|
+
def push_constant(symbol, constant)
|
|
60
|
+
@events << Gem::ConstantFound.new(symbol, constant)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(symbol: String, constant: Module, node: RBI::Const).void.checked(:never) }
|
|
64
|
+
def push_const(symbol, constant, node)
|
|
65
|
+
@events << Gem::ConstNodeAdded.new(symbol, constant, node)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { params(symbol: String, constant: Module, node: RBI::Scope).void.checked(:never) }
|
|
69
|
+
def push_scope(symbol, constant, node)
|
|
70
|
+
@events << Gem::ScopeNodeAdded.new(symbol, constant, node)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig do
|
|
74
|
+
params(
|
|
75
|
+
symbol: String,
|
|
76
|
+
constant: Module,
|
|
77
|
+
node: RBI::Method,
|
|
78
|
+
signature: T.untyped,
|
|
79
|
+
parameters: T::Array[[Symbol, String]]
|
|
80
|
+
).void.checked(:never)
|
|
81
|
+
end
|
|
82
|
+
def push_method(symbol, constant, node, signature, parameters)
|
|
83
|
+
@events << Gem::MethodNodeAdded.new(symbol, constant, node, signature, parameters)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
sig { params(symbol_name: String).returns(T::Boolean) }
|
|
87
|
+
def symbol_in_payload?(symbol_name)
|
|
88
|
+
symbol_name = symbol_name[2..-1] if symbol_name.start_with?("::")
|
|
89
|
+
return false unless symbol_name
|
|
90
|
+
@payload_symbols.include?(symbol_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
sig { params(method: UnboundMethod).returns(T::Boolean) }
|
|
94
|
+
def method_in_gem?(method)
|
|
95
|
+
source_location = method.source_location&.first
|
|
96
|
+
return false if source_location.nil?
|
|
97
|
+
|
|
98
|
+
@gem.contains_path?(source_location)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { params(constant: Module).returns(T.nilable(String)) }
|
|
102
|
+
def name_of(constant)
|
|
103
|
+
name = name_of_proxy_target(constant, super(class_of(constant)))
|
|
104
|
+
return name if name
|
|
105
|
+
name = super(constant)
|
|
106
|
+
return if name.nil?
|
|
107
|
+
return unless are_equal?(constant, constantize(name, inherit: true))
|
|
108
|
+
name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
|
|
109
|
+
name
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
sig { returns(Gem::Event) }
|
|
115
|
+
def next_event
|
|
116
|
+
T.must(@events.shift)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { params(event: Gem::Event).void }
|
|
120
|
+
def dispatch(event)
|
|
121
|
+
case event
|
|
122
|
+
when Gem::SymbolFound
|
|
123
|
+
on_symbol(event)
|
|
124
|
+
when Gem::ConstantFound
|
|
125
|
+
on_constant(event)
|
|
126
|
+
when Gem::NodeAdded
|
|
127
|
+
on_node(event)
|
|
128
|
+
else
|
|
129
|
+
raise "Unsupported event #{event.class}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sig { params(event: Gem::SymbolFound).void }
|
|
134
|
+
def on_symbol(event)
|
|
135
|
+
symbol = event.symbol.delete_prefix("::")
|
|
136
|
+
return if symbol_in_payload?(symbol) && !@bootstrap_symbols.include?(symbol)
|
|
137
|
+
|
|
138
|
+
constant = constantize(symbol)
|
|
139
|
+
push_constant(symbol, constant) if constant
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { params(event: Gem::ConstantFound).void.checked(:never) }
|
|
143
|
+
def on_constant(event)
|
|
144
|
+
name = event.symbol
|
|
145
|
+
|
|
146
|
+
return if name.strip.empty?
|
|
147
|
+
return if name.start_with?("#<")
|
|
148
|
+
return if name.downcase == name
|
|
149
|
+
return if alias_namespaced?(name)
|
|
150
|
+
return if seen?(name)
|
|
151
|
+
|
|
152
|
+
constant = event.constant
|
|
153
|
+
return if T::Enum === constant # T::Enum instances are defined via `compile_enums`
|
|
154
|
+
|
|
155
|
+
mark_seen(name)
|
|
156
|
+
compile_constant(name, constant)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
sig { params(event: Gem::NodeAdded).void }
|
|
160
|
+
def on_node(event)
|
|
161
|
+
@node_listeners.each { |listener| listener.dispatch(event) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Compile
|
|
165
|
+
|
|
166
|
+
sig { params(symbol: String, constant: BasicObject).void.checked(:never) }
|
|
167
|
+
def compile_constant(symbol, constant)
|
|
168
|
+
case constant
|
|
169
|
+
when Module
|
|
170
|
+
if name_of(constant) != symbol
|
|
171
|
+
compile_alias(symbol, constant)
|
|
172
|
+
else
|
|
173
|
+
compile_module(symbol, constant)
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
compile_object(symbol, constant)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
sig { params(name: String, constant: Module).void }
|
|
181
|
+
def compile_alias(name, constant)
|
|
182
|
+
return if symbol_in_payload?(name)
|
|
183
|
+
|
|
184
|
+
target = name_of(constant)
|
|
185
|
+
# If target has no name, let's make it an anonymous class or module with `Class.new` or `Module.new`
|
|
186
|
+
target = "#{constant.class}.new" unless target
|
|
187
|
+
|
|
188
|
+
add_to_alias_namespace(name)
|
|
189
|
+
|
|
190
|
+
return if IGNORED_SYMBOLS.include?(name)
|
|
191
|
+
|
|
192
|
+
node = RBI::Const.new(name, target)
|
|
193
|
+
push_const(name, constant, node)
|
|
194
|
+
@root << node
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
sig { params(name: String, value: BasicObject).void.checked(:never) }
|
|
198
|
+
def compile_object(name, value)
|
|
199
|
+
return if symbol_in_payload?(name)
|
|
200
|
+
|
|
201
|
+
klass = class_of(value)
|
|
202
|
+
|
|
203
|
+
klass_name = if klass == ObjectSpace::WeakMap
|
|
204
|
+
# WeakMap is an implicit generic with one type variable
|
|
205
|
+
"ObjectSpace::WeakMap[T.untyped]"
|
|
206
|
+
elsif T::Generic === klass
|
|
207
|
+
generic_name_of(klass)
|
|
208
|
+
else
|
|
209
|
+
name_of(klass)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
if klass_name == "T::Private::Types::TypeAlias"
|
|
213
|
+
type_alias = sanitize_signature_types(T.unsafe(value).aliased_type.to_s)
|
|
214
|
+
node = RBI::Const.new(name, "T.type_alias { #{type_alias} }")
|
|
215
|
+
push_const(name, klass, node)
|
|
216
|
+
@root << node
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
return if klass_name&.start_with?("T::Types::", "T::Private::")
|
|
221
|
+
|
|
222
|
+
type_name = klass_name || "T.untyped"
|
|
223
|
+
node = RBI::Const.new(name, "T.let(T.unsafe(nil), #{type_name})")
|
|
224
|
+
push_const(name, klass, node)
|
|
225
|
+
@root << node
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
sig { params(name: String, constant: Module).void }
|
|
229
|
+
def compile_module(name, constant)
|
|
230
|
+
return unless defined_in_gem?(constant, strict: false)
|
|
231
|
+
return if Tapioca::TypeVariableModule === constant
|
|
232
|
+
|
|
233
|
+
scope =
|
|
234
|
+
if constant.is_a?(Class)
|
|
235
|
+
superclass = compile_superclass(constant)
|
|
236
|
+
RBI::Class.new(name, superclass_name: superclass)
|
|
237
|
+
else
|
|
238
|
+
RBI::Module.new(name)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
push_scope(name, constant, scope)
|
|
242
|
+
@root << scope
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
sig { params(constant: Class).returns(T.nilable(String)) }
|
|
246
|
+
def compile_superclass(constant)
|
|
247
|
+
superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
|
|
248
|
+
|
|
249
|
+
while (superclass = superclass_of(constant))
|
|
250
|
+
constant_name = name_of(constant)
|
|
251
|
+
constant = superclass
|
|
252
|
+
|
|
253
|
+
# Some types have "themselves" as their superclass
|
|
254
|
+
# which can happen via:
|
|
255
|
+
#
|
|
256
|
+
# class A < Numeric; end
|
|
257
|
+
# A = Class.new(A)
|
|
258
|
+
# A.superclass #=> A
|
|
259
|
+
#
|
|
260
|
+
# We compare names here to make sure we skip those
|
|
261
|
+
# superclass instances and walk up the chain.
|
|
262
|
+
#
|
|
263
|
+
# The name comparison is against the name of the constant
|
|
264
|
+
# resolved from the name of the superclass, since
|
|
265
|
+
# this is also possible:
|
|
266
|
+
#
|
|
267
|
+
# B = Class.new
|
|
268
|
+
# class A < B; end
|
|
269
|
+
# B = A
|
|
270
|
+
# A.superclass.name #=> "B"
|
|
271
|
+
# B #=> A
|
|
272
|
+
superclass_name = name_of(superclass)
|
|
273
|
+
next unless superclass_name
|
|
274
|
+
|
|
275
|
+
resolved_superclass = constantize(superclass_name)
|
|
276
|
+
next unless Module === resolved_superclass
|
|
277
|
+
next if name_of(resolved_superclass) == constant_name
|
|
278
|
+
|
|
279
|
+
# We found a suitable superclass
|
|
280
|
+
break
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
return if superclass == ::Object || superclass == ::Delegator
|
|
284
|
+
return if superclass.nil?
|
|
285
|
+
|
|
286
|
+
name = name_of(superclass)
|
|
287
|
+
return if name.nil? || name.empty?
|
|
288
|
+
|
|
289
|
+
push_symbol(name)
|
|
290
|
+
|
|
291
|
+
"::#{name}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
|
|
295
|
+
def defined_in_gem?(constant, strict: true)
|
|
296
|
+
files = Set.new(get_file_candidates(constant))
|
|
297
|
+
.merge(Runtime::Trackers::ConstantDefinition.files_for(constant))
|
|
298
|
+
|
|
299
|
+
return !strict if files.empty?
|
|
300
|
+
|
|
301
|
+
files.any? do |file|
|
|
302
|
+
@gem.contains_path?(file)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
sig { params(constant: Module).returns(T::Array[String]) }
|
|
307
|
+
def get_file_candidates(constant)
|
|
308
|
+
wrapped_module = Pry::WrappedModule.new(constant)
|
|
309
|
+
|
|
310
|
+
wrapped_module.candidates.map(&:file).to_a.compact
|
|
311
|
+
rescue ArgumentError, NameError
|
|
312
|
+
[]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
sig { params(name: String).void }
|
|
316
|
+
def add_to_alias_namespace(name)
|
|
317
|
+
@alias_namespace.add("#{name}::")
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
321
|
+
def alias_namespaced?(name)
|
|
322
|
+
@alias_namespace.any? do |namespace|
|
|
323
|
+
name.start_with?(namespace)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
sig { params(name: String).void }
|
|
328
|
+
def mark_seen(name)
|
|
329
|
+
@seen.add(name)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
sig { params(name: String).returns(T::Boolean) }
|
|
333
|
+
def seen?(name)
|
|
334
|
+
@seen.include?(name)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
sig { params(constant: T.all(Module, T::Generic)).returns(String) }
|
|
338
|
+
def generic_name_of(constant)
|
|
339
|
+
type_name = T.must(constant.name)
|
|
340
|
+
return type_name if type_name =~ /\[.*\]$/
|
|
341
|
+
|
|
342
|
+
type_variables = Runtime::GenericTypeRegistry.lookup_type_variables(constant)
|
|
343
|
+
return type_name unless type_variables
|
|
344
|
+
|
|
345
|
+
type_variable_names = type_variables.map { "T.untyped" }.join(", ")
|
|
346
|
+
|
|
347
|
+
"#{type_name}[#{type_variable_names}]"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
sig { params(constant: Module, class_name: T.nilable(String)).returns(T.nilable(String)) }
|
|
351
|
+
def name_of_proxy_target(constant, class_name)
|
|
352
|
+
return unless class_name == "ActiveSupport::Deprecation::DeprecatedConstantProxy"
|
|
353
|
+
# We are dealing with a ActiveSupport::Deprecation::DeprecatedConstantProxy
|
|
354
|
+
# so try to get the name of the target class
|
|
355
|
+
begin
|
|
356
|
+
target = constant.__send__(:target)
|
|
357
|
+
rescue NoMethodError
|
|
358
|
+
return
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
name_of(target)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
data/lib/tapioca/gemfile.rb
CHANGED
|
@@ -99,6 +99,9 @@ module Tapioca
|
|
|
99
99
|
sig { returns(String) }
|
|
100
100
|
attr_reader :full_gem_path, :version
|
|
101
101
|
|
|
102
|
+
sig { returns(T::Array[Pathname]) }
|
|
103
|
+
attr_reader :files
|
|
104
|
+
|
|
102
105
|
sig { params(spec: Spec).void }
|
|
103
106
|
def initialize(spec)
|
|
104
107
|
@spec = T.let(spec, Tapioca::Gemfile::Spec)
|
|
@@ -106,6 +109,7 @@ module Tapioca
|
|
|
106
109
|
@full_gem_path = T.let(real_gem_path, String)
|
|
107
110
|
@version = T.let(version_string, String)
|
|
108
111
|
@exported_rbi_files = T.let(nil, T.nilable(T::Array[String]))
|
|
112
|
+
@files = T.let(collect_files, T::Array[Pathname])
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
sig { params(gemfile_dir: String).returns(T::Boolean) }
|
|
@@ -113,21 +117,6 @@ module Tapioca
|
|
|
113
117
|
gem_ignored? || gem_in_app_dir?(gemfile_dir)
|
|
114
118
|
end
|
|
115
119
|
|
|
116
|
-
sig { returns(T::Array[Pathname]) }
|
|
117
|
-
def files
|
|
118
|
-
if default_gem?
|
|
119
|
-
# `Bundler::RemoteSpecification` delegates missing methods to
|
|
120
|
-
# `Gem::Specification`, so `files` actually always exists on spec.
|
|
121
|
-
T.unsafe(@spec).files.map do |file|
|
|
122
|
-
ruby_lib_dir.join(file)
|
|
123
|
-
end
|
|
124
|
-
else
|
|
125
|
-
@spec.full_require_paths.flat_map do |path|
|
|
126
|
-
Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
120
|
sig { returns(String) }
|
|
132
121
|
def name
|
|
133
122
|
@spec.name
|
|
@@ -154,7 +143,7 @@ module Tapioca
|
|
|
154
143
|
|
|
155
144
|
sig { returns(T::Array[String]) }
|
|
156
145
|
def exported_rbi_files
|
|
157
|
-
@exported_rbi_files ||= Dir.glob("#{full_gem_path}/rbi/**/*.rbi")
|
|
146
|
+
@exported_rbi_files ||= Dir.glob("#{full_gem_path}/rbi/**/*.rbi").sort
|
|
158
147
|
end
|
|
159
148
|
|
|
160
149
|
sig { returns(T::Boolean) }
|
|
@@ -176,14 +165,49 @@ module Tapioca
|
|
|
176
165
|
|
|
177
166
|
private
|
|
178
167
|
|
|
179
|
-
sig { returns(T::
|
|
168
|
+
sig { returns(T::Array[Pathname]) }
|
|
169
|
+
def collect_files
|
|
170
|
+
if default_gem?
|
|
171
|
+
# `Bundler::RemoteSpecification` delegates missing methods to
|
|
172
|
+
# `Gem::Specification`, so `files` actually always exists on spec.
|
|
173
|
+
T.unsafe(@spec).files.map do |file|
|
|
174
|
+
resolve_to_ruby_lib_dir(file)
|
|
175
|
+
end
|
|
176
|
+
else
|
|
177
|
+
@spec.full_require_paths.flat_map do |path|
|
|
178
|
+
Pathname.glob((Pathname.new(path) / "**/*.rb").to_s)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
sig { returns(T.nilable(T::Boolean)) }
|
|
180
184
|
def default_gem?
|
|
181
185
|
@spec.respond_to?(:default_gem?) && @spec.default_gem?
|
|
182
186
|
end
|
|
183
187
|
|
|
184
|
-
sig { returns(
|
|
185
|
-
def
|
|
186
|
-
|
|
188
|
+
sig { returns(Regexp) }
|
|
189
|
+
def require_paths_prefix_matcher
|
|
190
|
+
@require_paths_prefix_matcher = T.let(@require_paths_prefix_matcher, T.nilable(Regexp))
|
|
191
|
+
|
|
192
|
+
@require_paths_prefix_matcher ||= begin
|
|
193
|
+
require_paths = T.unsafe(@spec).require_paths
|
|
194
|
+
prefix_matchers = require_paths.map { |rp| Regexp.new("^#{rp}/") }
|
|
195
|
+
Regexp.union(prefix_matchers)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
sig { params(file: String).returns(Pathname) }
|
|
200
|
+
def resolve_to_ruby_lib_dir(file)
|
|
201
|
+
# We want to match require prefixes but fallback to an empty match
|
|
202
|
+
# if none of the require prefixes actually match. This is so that
|
|
203
|
+
# we can always replace the match with the Ruby lib directory and
|
|
204
|
+
# we would have properly resolved the file under the Ruby lib dir.
|
|
205
|
+
prefix_matcher = Regexp.union(require_paths_prefix_matcher, //)
|
|
206
|
+
|
|
207
|
+
ruby_lib_dir = RbConfig::CONFIG["rubylibdir"]
|
|
208
|
+
file = file.sub(prefix_matcher, "#{ruby_lib_dir}/")
|
|
209
|
+
|
|
210
|
+
Pathname.new(file).expand_path
|
|
187
211
|
end
|
|
188
212
|
|
|
189
213
|
sig { returns(String) }
|
|
@@ -12,15 +12,23 @@ module Tapioca
|
|
|
12
12
|
|
|
13
13
|
sig { params(message: String, color: T.any(Symbol, T::Array[Symbol])).void }
|
|
14
14
|
def say_error(message = "", *color)
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
# Thor has its own `say_error` now, but it has two problems:
|
|
16
|
+
# 1. it adds the padding around all the messages, even if they continue on
|
|
17
|
+
# the same line, and
|
|
18
|
+
# 2. it accepts a last parameter which breaks the ability to pass color values
|
|
19
|
+
# as splats.
|
|
20
|
+
#
|
|
21
|
+
# So we implement our own version here to work around those problems.
|
|
22
|
+
shell.indent(-shell.padding) do
|
|
23
|
+
super(message, color)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(RBIFormatter) }
|
|
28
|
+
def rbi_formatter(options)
|
|
29
|
+
rbi_formatter = DEFAULT_RBI_FORMATTER
|
|
30
|
+
rbi_formatter.max_line_length = options[:rbi_max_line_length]
|
|
31
|
+
rbi_formatter
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
end
|
|
@@ -6,6 +6,9 @@ require "yaml"
|
|
|
6
6
|
module Tapioca
|
|
7
7
|
module ConfigHelper
|
|
8
8
|
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
|
|
11
|
+
requires_ancestor { Thor }
|
|
9
12
|
|
|
10
13
|
sig { returns(String) }
|
|
11
14
|
attr_reader :command_name
|
|
@@ -60,9 +63,119 @@ module Tapioca
|
|
|
60
63
|
config = YAML.load_file(config_file, fallback: {})
|
|
61
64
|
end
|
|
62
65
|
|
|
66
|
+
validate_config!(config_file, config)
|
|
67
|
+
|
|
63
68
|
Thor::CoreExt::HashWithIndifferentAccess.new(config[command_name] || {})
|
|
64
69
|
end
|
|
65
70
|
|
|
71
|
+
sig { params(config_file: String, config: T::Hash[T.untyped, T.untyped]).void }
|
|
72
|
+
def validate_config!(config_file, config)
|
|
73
|
+
# To ensure that this is not re-entered, we mark during validation
|
|
74
|
+
return if @validating_config
|
|
75
|
+
@validating_config = T.let(true, T.nilable(T::Boolean))
|
|
76
|
+
|
|
77
|
+
commands = T.cast(self, Thor).class.commands
|
|
78
|
+
|
|
79
|
+
errors = config.flat_map do |config_key, config_options|
|
|
80
|
+
command = commands[config_key.to_s]
|
|
81
|
+
|
|
82
|
+
unless command
|
|
83
|
+
next build_error("unknown key `#{config_key}`")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
validate_config_options(command.options, config_key, config_options || {})
|
|
87
|
+
end.compact
|
|
88
|
+
|
|
89
|
+
unless errors.empty?
|
|
90
|
+
print_errors(config_file, errors)
|
|
91
|
+
exit(1)
|
|
92
|
+
end
|
|
93
|
+
ensure
|
|
94
|
+
@validating_config = false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
sig do
|
|
98
|
+
params(
|
|
99
|
+
command_options: T::Hash[Symbol, Thor::Option],
|
|
100
|
+
config_key: String,
|
|
101
|
+
config_options: T::Hash[T.untyped, T.untyped]
|
|
102
|
+
).returns(T::Array[ConfigError])
|
|
103
|
+
end
|
|
104
|
+
def validate_config_options(command_options, config_key, config_options)
|
|
105
|
+
config_options.map do |config_option_key, config_option_value|
|
|
106
|
+
command_option = command_options[config_option_key.to_sym]
|
|
107
|
+
error_msg = "unknown option `#{config_option_key}` for key `#{config_key}`"
|
|
108
|
+
next build_error(error_msg) unless command_option
|
|
109
|
+
|
|
110
|
+
config_option_value_type = case config_option_value
|
|
111
|
+
when FalseClass, TrueClass
|
|
112
|
+
:boolean
|
|
113
|
+
when Numeric
|
|
114
|
+
:numeric
|
|
115
|
+
when Hash
|
|
116
|
+
:hash
|
|
117
|
+
when Array
|
|
118
|
+
:array
|
|
119
|
+
when String
|
|
120
|
+
:string
|
|
121
|
+
else
|
|
122
|
+
:object
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
|
|
126
|
+
"`#{command_option.type.capitalize}` but found #{config_option_value_type.capitalize}"
|
|
127
|
+
next build_error(error_msg) unless config_option_value_type == command_option.type
|
|
128
|
+
end.compact
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class ConfigErrorMessagePart < T::Struct
|
|
132
|
+
const :message, String
|
|
133
|
+
const :colors, T::Array[Symbol]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class ConfigError < T::Struct
|
|
137
|
+
const :message_parts, T::Array[ConfigErrorMessagePart]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
sig { params(msg: String).returns(ConfigError) }
|
|
141
|
+
def build_error(msg)
|
|
142
|
+
parts = msg.split(/(`[^`]+` ?)/)
|
|
143
|
+
|
|
144
|
+
message_parts = parts.map do |part|
|
|
145
|
+
match = part.match(/`([^`]+)`( ?)/)
|
|
146
|
+
|
|
147
|
+
if match
|
|
148
|
+
ConfigErrorMessagePart.new(
|
|
149
|
+
message: "#{match[1]}#{match[2]}",
|
|
150
|
+
colors: [:bold, :blue]
|
|
151
|
+
)
|
|
152
|
+
else
|
|
153
|
+
ConfigErrorMessagePart.new(
|
|
154
|
+
message: part,
|
|
155
|
+
colors: [:yellow]
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
ConfigError.new(
|
|
161
|
+
message_parts: message_parts
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
sig { params(config_file: String, errors: T::Array[ConfigError]).void }
|
|
166
|
+
def print_errors(config_file, errors)
|
|
167
|
+
say_error("\nConfiguration file ", :red)
|
|
168
|
+
say_error("#{config_file} ", :blue, :bold)
|
|
169
|
+
say_error("has the following errors:\n\n", :red)
|
|
170
|
+
|
|
171
|
+
errors.each do |error|
|
|
172
|
+
say_error("- ")
|
|
173
|
+
error.message_parts.each do |part|
|
|
174
|
+
T.unsafe(self).say_error(part.message, *part.colors)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
66
179
|
sig do
|
|
67
180
|
params(options: T.nilable(Thor::CoreExt::HashWithIndifferentAccess))
|
|
68
181
|
.returns(Thor::CoreExt::HashWithIndifferentAccess)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Tapioca
|
|
5
|
+
module RBIHelper
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
sig { params(sig_string: String).returns(String) }
|
|
9
|
+
def sanitize_signature_types(sig_string)
|
|
10
|
+
sig_string
|
|
11
|
+
.gsub(".returns(<VOID>)", ".void")
|
|
12
|
+
.gsub("<VOID>", "void")
|
|
13
|
+
.gsub("<NOT-TYPED>", "T.untyped")
|
|
14
|
+
.gsub(".params()", "")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|