tapioca 0.6.0 → 0.6.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 +6 -0
- data/README.md +56 -14
- data/lib/tapioca/cli.rb +1 -0
- data/lib/tapioca/compilers/dsl/active_model_attributes.rb +19 -13
- data/lib/tapioca/compilers/dsl/active_record_associations.rb +1 -1
- data/lib/tapioca/compilers/dsl/active_record_columns.rb +8 -8
- data/lib/tapioca/compilers/dsl/active_record_relations.rb +20 -3
- data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +1 -1
- data/lib/tapioca/compilers/dsl/base.rb +19 -3
- data/lib/tapioca/compilers/dsl/identity_cache.rb +5 -3
- data/lib/tapioca/compilers/dsl/rails_generators.rb +3 -3
- data/lib/tapioca/compilers/dsl/smart_properties.rb +12 -19
- data/lib/tapioca/compilers/dsl/url_helpers.rb +8 -3
- data/lib/tapioca/compilers/dynamic_mixin_compiler.rb +30 -5
- data/lib/tapioca/compilers/requires_compiler.rb +4 -11
- data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +26 -26
- data/lib/tapioca/gemfile.rb +44 -20
- data/lib/tapioca/generators/base.rb +1 -1
- data/lib/tapioca/generic_type_registry.rb +22 -7
- data/lib/tapioca/helpers/active_record_column_type_helper.rb +12 -2
- data/lib/tapioca/helpers/cli_helper.rb +10 -9
- data/lib/tapioca/helpers/config_helper.rb +116 -0
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +92 -54
- data/lib/tapioca/version.rb +1 -1
- metadata +4 -4
@@ -141,7 +141,6 @@ module Tapioca
|
|
141
141
|
return if symbol_ignored?(name)
|
142
142
|
|
143
143
|
klass = class_of(value)
|
144
|
-
return if klass == TypeMember || klass == TypeTemplate
|
145
144
|
|
146
145
|
klass_name = if klass == ObjectSpace::WeakMap
|
147
146
|
# WeakMap is an implicit generic with one type variable
|
@@ -172,6 +171,7 @@ module Tapioca
|
|
172
171
|
sig { params(tree: RBI::Tree, name: String, constant: Module).void }
|
173
172
|
def compile_module(tree, name, constant)
|
174
173
|
return unless defined_in_gem?(constant, strict: false)
|
174
|
+
return if Tapioca::TypeVariableModule === constant
|
175
175
|
|
176
176
|
comments = documentation_comments(name)
|
177
177
|
scope =
|
@@ -282,30 +282,16 @@ module Tapioca
|
|
282
282
|
type_variables = GenericTypeRegistry.lookup_type_variables(constant)
|
283
283
|
return unless type_variables
|
284
284
|
|
285
|
-
# Create a map of subconstants (via their object ids) to their names.
|
286
|
-
# We need this later when we want to lookup the name of the registered type
|
287
|
-
# variable via the value of the type variable constant.
|
288
|
-
subconstant_to_name_lookup = constants_of(constant)
|
289
|
-
.each_with_object({}.compare_by_identity) do |constant_name, table|
|
290
|
-
table[constantize(constant_name.to_s, namespace: constant)] = constant_name.to_s
|
291
|
-
end
|
292
|
-
|
293
285
|
# Map each type variable to its string representation.
|
294
286
|
#
|
295
|
-
# Each entry of `type_variables` maps
|
287
|
+
# Each entry of `type_variables` maps a Module to a String,
|
296
288
|
# and the order they are inserted into the hash is the order they should be
|
297
289
|
# defined in the source code.
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
constant_name = subconstant_to_name_lookup[type_variable]
|
304
|
-
type_variable.name = constant_name
|
305
|
-
# Here, we know that constant_value will be an instance of
|
306
|
-
# T::Types::CustomTypeVariable, which knows how to serialize
|
307
|
-
# itself to a type_member/type_template
|
308
|
-
tree << RBI::TypeMember.new(constant_name, serialized_type_variable)
|
290
|
+
type_variable_declarations = type_variables.map do |type_variable|
|
291
|
+
type_variable_name = type_variable.name
|
292
|
+
next unless type_variable_name
|
293
|
+
|
294
|
+
tree << RBI::TypeMember.new(type_variable_name, type_variable.serialize)
|
309
295
|
end
|
310
296
|
|
311
297
|
return if type_variable_declarations.empty?
|
@@ -392,7 +378,7 @@ module Tapioca
|
|
392
378
|
.select do |mod|
|
393
379
|
name = name_of(mod)
|
394
380
|
|
395
|
-
name && !
|
381
|
+
name && !filtered_mixin?(name)
|
396
382
|
end
|
397
383
|
.map do |mod|
|
398
384
|
add_to_symbol_queue(name_of(mod))
|
@@ -496,7 +482,9 @@ module Tapioca
|
|
496
482
|
sanitized_parameters = parameters.each_with_index.map do |(type, name), index|
|
497
483
|
fallback_arg_name = "_arg#{index}"
|
498
484
|
|
499
|
-
|
485
|
+
name = if name
|
486
|
+
name.to_s
|
487
|
+
else
|
500
488
|
# For attr_writer methods, Sorbet signatures have the name
|
501
489
|
# of the method (without the trailing = sign) as the name of
|
502
490
|
# the only parameter. So, if the parameter does not have a name
|
@@ -512,15 +500,15 @@ module Tapioca
|
|
512
500
|
method_name[-1] == "="
|
513
501
|
)
|
514
502
|
|
515
|
-
|
516
|
-
|
503
|
+
if writer_method_with_sig
|
504
|
+
method_name.delete_suffix("=")
|
517
505
|
else
|
518
506
|
fallback_arg_name
|
519
507
|
end
|
520
508
|
end
|
521
509
|
|
522
510
|
# Sanitize param names
|
523
|
-
name = name
|
511
|
+
name = fallback_arg_name unless valid_parameter_name?(name)
|
524
512
|
|
525
513
|
[type, name]
|
526
514
|
end
|
@@ -613,6 +601,13 @@ module Tapioca
|
|
613
601
|
SymbolLoader.ignore_symbol?(symbol_name)
|
614
602
|
end
|
615
603
|
|
604
|
+
sig { params(mixin_name: String).returns(T::Boolean) }
|
605
|
+
def filtered_mixin?(mixin_name)
|
606
|
+
# filter T:: namespace mixins that aren't T::Props
|
607
|
+
# T::Props and subconstants have semantic value
|
608
|
+
mixin_name.start_with?("T::") && !mixin_name.start_with?("T::Props")
|
609
|
+
end
|
610
|
+
|
616
611
|
SPECIAL_METHOD_NAMES = T.let([
|
617
612
|
"!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^",
|
618
613
|
"<", "<=", "=>", ">", ">=", "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"
|
@@ -624,6 +619,11 @@ module Tapioca
|
|
624
619
|
!!name.match(/^[[:word:]]+[?!=]?$/)
|
625
620
|
end
|
626
621
|
|
622
|
+
sig { params(name: String).returns(T::Boolean) }
|
623
|
+
def valid_parameter_name?(name)
|
624
|
+
name.match?(/^[[[:alnum:]]_]+$/)
|
625
|
+
end
|
626
|
+
|
627
627
|
sig { params(method: UnboundMethod).returns(T::Boolean) }
|
628
628
|
def method_in_gem?(method)
|
629
629
|
source_location = method.source_location&.first
|
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) }
|
@@ -20,7 +20,6 @@ module Tapioca
|
|
20
20
|
# variable to type variable serializers. This allows us to associate type variables
|
21
21
|
# to the constant names that represent them, easily.
|
22
22
|
module GenericTypeRegistry
|
23
|
-
TypeVariable = T.type_alias { T.any(TypeMember, TypeTemplate) }
|
24
23
|
@generic_instances = T.let(
|
25
24
|
{},
|
26
25
|
T::Hash[String, Module]
|
@@ -28,7 +27,7 @@ module Tapioca
|
|
28
27
|
|
29
28
|
@type_variables = T.let(
|
30
29
|
{}.compare_by_identity,
|
31
|
-
T::Hash[Module, T::
|
30
|
+
T::Hash[Module, T::Array[TypeVariableModule]]
|
32
31
|
)
|
33
32
|
|
34
33
|
class << self
|
@@ -60,7 +59,12 @@ module Tapioca
|
|
60
59
|
@generic_instances[name] ||= create_generic_type(constant, name)
|
61
60
|
end
|
62
61
|
|
63
|
-
sig { params(
|
62
|
+
sig { params(instance: Object).returns(T::Boolean) }
|
63
|
+
def generic_type_instance?(instance)
|
64
|
+
@generic_instances.values.any? { |generic_type| generic_type === instance }
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { params(constant: Module).returns(T.nilable(T::Array[TypeVariableModule])) }
|
64
68
|
def lookup_type_variables(constant)
|
65
69
|
@type_variables[constant]
|
66
70
|
end
|
@@ -77,13 +81,13 @@ module Tapioca
|
|
77
81
|
sig do
|
78
82
|
params(
|
79
83
|
constant: T.untyped,
|
80
|
-
type_variable:
|
84
|
+
type_variable: TypeVariableModule,
|
81
85
|
).void
|
82
86
|
end
|
83
87
|
def register_type_variable(constant, type_variable)
|
84
88
|
type_variables = lookup_or_initialize_type_variables(constant)
|
85
89
|
|
86
|
-
type_variables
|
90
|
+
type_variables << type_variable
|
87
91
|
end
|
88
92
|
|
89
93
|
private
|
@@ -109,6 +113,17 @@ module Tapioca
|
|
109
113
|
# Let's set the `name` method to return the proper generic name
|
110
114
|
generic_type.define_singleton_method(:name) { name }
|
111
115
|
|
116
|
+
# We need to define a `<=` method on the cloned constant, so that Sorbet
|
117
|
+
# can do covariance/contravariance checks on the type variables.
|
118
|
+
#
|
119
|
+
# Normally, we would be doing proper covariance/contravariance checks here, but
|
120
|
+
# that is not necessary, since we are not implementing a runtime type checker
|
121
|
+
# here. It is just enough for the checks to pass, so that we can serialize the
|
122
|
+
# signatures, assuming the sigs were well-formed.
|
123
|
+
#
|
124
|
+
# So we act like all subtype checks pass.
|
125
|
+
generic_type.define_singleton_method(:<=) { |_| true }
|
126
|
+
|
112
127
|
# Return the generic type we created
|
113
128
|
generic_type
|
114
129
|
end
|
@@ -140,9 +155,9 @@ module Tapioca
|
|
140
155
|
end
|
141
156
|
end
|
142
157
|
|
143
|
-
sig { params(constant: Module).returns(T::
|
158
|
+
sig { params(constant: Module).returns(T::Array[TypeVariableModule]) }
|
144
159
|
def lookup_or_initialize_type_variables(constant)
|
145
|
-
@type_variables[constant] ||=
|
160
|
+
@type_variables[constant] ||= []
|
146
161
|
end
|
147
162
|
end
|
148
163
|
end
|
@@ -43,13 +43,13 @@ class ActiveRecordColumnTypeHelper
|
|
43
43
|
setter_type = getter_type
|
44
44
|
|
45
45
|
if column&.null
|
46
|
-
return [
|
46
|
+
return [as_nilable_type(getter_type), as_nilable_type(setter_type)]
|
47
47
|
end
|
48
48
|
|
49
49
|
if column_name == @constant.primary_key ||
|
50
50
|
column_name == "created_at" ||
|
51
51
|
column_name == "updated_at"
|
52
|
-
getter_type =
|
52
|
+
getter_type = as_nilable_type(getter_type)
|
53
53
|
end
|
54
54
|
|
55
55
|
[getter_type, setter_type]
|
@@ -63,9 +63,19 @@ class ActiveRecordColumnTypeHelper
|
|
63
63
|
!(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
|
64
64
|
end
|
65
65
|
|
66
|
+
sig { params(type: String).returns(String) }
|
67
|
+
def as_nilable_type(type)
|
68
|
+
if type.start_with?("T.nilable(") || type == "T.untyped"
|
69
|
+
type
|
70
|
+
else
|
71
|
+
"T.nilable(#{type})"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
66
75
|
sig { params(column_type: Object).returns(String) }
|
67
76
|
def handle_unknown_type(column_type)
|
68
77
|
return "T.untyped" unless ActiveModel::Type::Value === column_type
|
78
|
+
return "T.untyped" if Tapioca::GenericTypeRegistry.generic_type_instance?(column_type)
|
69
79
|
|
70
80
|
lookup_return_type_of_method(column_type, :deserialize) ||
|
71
81
|
lookup_return_type_of_method(column_type, :cast) ||
|
@@ -12,15 +12,16 @@ 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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
24
25
|
end
|
25
26
|
end
|
26
27
|
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,122 @@ 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
|
+
|
108
|
+
unless command_option
|
109
|
+
next build_error("unknown option `#{config_option_key}` for key `#{config_key}`")
|
110
|
+
end
|
111
|
+
|
112
|
+
config_option_value_type = case config_option_value
|
113
|
+
when FalseClass, TrueClass
|
114
|
+
:boolean
|
115
|
+
when Numeric
|
116
|
+
:numeric
|
117
|
+
when Hash
|
118
|
+
:hash
|
119
|
+
when Array
|
120
|
+
:array
|
121
|
+
when String
|
122
|
+
:string
|
123
|
+
else
|
124
|
+
:object
|
125
|
+
end
|
126
|
+
|
127
|
+
unless config_option_value_type == command_option.type
|
128
|
+
next build_error("invalid value for option `#{config_option_key}` for key `#{config_key}` " \
|
129
|
+
"- expected `#{command_option.type.capitalize}` but found #{config_option_value_type.capitalize}")
|
130
|
+
end
|
131
|
+
end.compact
|
132
|
+
end
|
133
|
+
|
134
|
+
class ConfigErrorMessagePart < T::Struct
|
135
|
+
const :message, String
|
136
|
+
const :colors, T::Array[Symbol]
|
137
|
+
end
|
138
|
+
|
139
|
+
class ConfigError < T::Struct
|
140
|
+
const :message_parts, T::Array[ConfigErrorMessagePart]
|
141
|
+
end
|
142
|
+
|
143
|
+
sig { params(msg: String).returns(ConfigError) }
|
144
|
+
def build_error(msg)
|
145
|
+
parts = msg.split(/(`[^`]+` ?)/)
|
146
|
+
|
147
|
+
message_parts = parts.map do |part|
|
148
|
+
match = part.match(/`([^`]+)`( ?)/)
|
149
|
+
|
150
|
+
if match
|
151
|
+
ConfigErrorMessagePart.new(
|
152
|
+
message: "#{match[1]}#{match[2]}",
|
153
|
+
colors: [:bold, :blue]
|
154
|
+
)
|
155
|
+
else
|
156
|
+
ConfigErrorMessagePart.new(
|
157
|
+
message: part,
|
158
|
+
colors: [:yellow]
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
ConfigError.new(
|
164
|
+
message_parts: message_parts
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
sig { params(config_file: String, errors: T::Array[ConfigError]).void }
|
169
|
+
def print_errors(config_file, errors)
|
170
|
+
say_error("\nConfiguration file ", :red)
|
171
|
+
say_error("#{config_file} ", :blue, :bold)
|
172
|
+
say_error("has the following errors:\n\n", :red)
|
173
|
+
|
174
|
+
errors.each do |error|
|
175
|
+
say_error("- ")
|
176
|
+
error.message_parts.each do |part|
|
177
|
+
T.unsafe(self).say_error(part.message, *part.colors)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
66
182
|
sig do
|
67
183
|
params(options: T.nilable(Thor::CoreExt::HashWithIndifferentAccess))
|
68
184
|
.returns(Thor::CoreExt::HashWithIndifferentAccess)
|