tapioca 0.19.1 → 0.20.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/README.md +84 -0
- data/lib/ruby_lsp/tapioca/run_gem_rbi_check.rb +1 -1
- data/lib/tapioca/cli.rb +9 -0
- data/lib/tapioca/commands/abstract_dsl.rb +76 -20
- data/lib/tapioca/commands/abstract_gem.rb +1 -0
- data/lib/tapioca/commands/annotations.rb +1 -1
- data/lib/tapioca/commands/gem_generate.rb +20 -4
- data/lib/tapioca/dsl/compiler.rb +24 -10
- data/lib/tapioca/dsl/compilers/aasm.rb +24 -17
- data/lib/tapioca/dsl/compilers/active_job.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
- data/lib/tapioca/dsl/compilers/active_record_delegated_types.rb +64 -16
- data/lib/tapioca/dsl/compilers/active_record_fixtures.rb +17 -11
- data/lib/tapioca/dsl/compilers/active_record_relations.rb +2 -2
- data/lib/tapioca/dsl/compilers/config.rb +1 -1
- data/lib/tapioca/dsl/compilers/json_api_client_resource.rb +1 -1
- data/lib/tapioca/dsl/compilers/sidekiq_worker.rb +1 -1
- data/lib/tapioca/dsl/compilers/url_helpers.rb +312 -8
- data/lib/tapioca/dsl/compilers.rb +1 -1
- data/lib/tapioca/dsl/helpers/graphql_type_helper.rb +10 -3
- data/lib/tapioca/dsl/pipeline.rb +6 -27
- data/lib/tapioca/gem/events.rb +1 -1
- data/lib/tapioca/gem/listeners/documentation.rb +13 -8
- data/lib/tapioca/gem/listeners/methods.rb +79 -13
- data/lib/tapioca/gem/listeners/sorbet_enums.rb +1 -1
- data/lib/tapioca/gem/pipeline.rb +3 -3
- data/lib/tapioca/gemfile.rb +13 -0
- data/lib/tapioca/helpers/env_helper.rb +1 -1
- data/lib/tapioca/helpers/file_helper.rb +42 -0
- data/lib/tapioca/helpers/rbi_files_helper.rb +96 -5
- data/lib/tapioca/helpers/rbi_helper.rb +17 -3
- data/lib/tapioca/helpers/sorbet_helper.rb +1 -1
- data/lib/tapioca/internal.rb +2 -0
- data/lib/tapioca/loaders/gem.rb +33 -0
- data/lib/tapioca/loaders/loader.rb +0 -13
- data/lib/tapioca/rbi_ext/model.rb +16 -2
- data/lib/tapioca/rbs/bootsnap_cache.rb +52 -0
- data/lib/tapioca/rbs/rewriter.rb +91 -25
- data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +1 -2
- data/lib/tapioca/runtime/generic_type_registry.rb +38 -14
- data/lib/tapioca/runtime/reflection.rb +18 -1
- data/lib/tapioca/sorbet_ext/generic_name_patch.rb +0 -20
- data/lib/tapioca/sorbet_ext/generic_type_patch.rb +48 -0
- data/lib/tapioca/version.rb +1 -1
- data/lib/tapioca.rb +11 -10
- metadata +9 -6
|
@@ -80,7 +80,9 @@ module Tapioca
|
|
|
80
80
|
signature = Runtime::Reflection.signature_of(method)
|
|
81
81
|
return_type = signature&.return_type
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
# Wrap as non-nilable for required arguments. `coerce_input` supports both
|
|
84
|
+
# required and optional; optional arguments are re-wrapped below based on `type.non_null?`
|
|
85
|
+
valid_return_type?(return_type) ? (T::Utils.unwrap_nilable(return_type) || return_type).to_s : "T.untyped"
|
|
84
86
|
when GraphQL::Schema::InputObject.singleton_class
|
|
85
87
|
type_for_constant(unwrapped_type)
|
|
86
88
|
when Module
|
|
@@ -89,15 +91,20 @@ module Tapioca
|
|
|
89
91
|
"T.untyped"
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
prepared = false
|
|
92
95
|
if prepare_method
|
|
93
96
|
prepare_signature = Runtime::Reflection.signature_of(prepare_method)
|
|
94
97
|
prepare_return_type = prepare_signature&.return_type
|
|
95
98
|
if valid_return_type?(prepare_return_type)
|
|
96
99
|
parsed_type = prepare_return_type&.to_s
|
|
100
|
+
prepared = true
|
|
97
101
|
end
|
|
98
102
|
end
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
# A `prepare` method receives (and returns) the argument's fully coerced value, list
|
|
105
|
+
# wrapper included, so its return type already accounts for `type.list?` and must not be
|
|
106
|
+
# wrapped again.
|
|
107
|
+
if type.list? && !prepared
|
|
101
108
|
parsed_type = "T::Array[#{parsed_type}]"
|
|
102
109
|
end
|
|
103
110
|
|
|
@@ -112,7 +119,7 @@ module Tapioca
|
|
|
112
119
|
|
|
113
120
|
#: (Module[top] constant) -> String
|
|
114
121
|
def type_for_constant(constant)
|
|
115
|
-
if constant.
|
|
122
|
+
if constant.method_defined?(:prepare)
|
|
116
123
|
prepare_method = constant.instance_method(:prepare)
|
|
117
124
|
|
|
118
125
|
prepare_signature = Runtime::Reflection.signature_of(prepare_method)
|
data/lib/tapioca/dsl/pipeline.rb
CHANGED
|
@@ -64,6 +64,9 @@ module Tapioca
|
|
|
64
64
|
|
|
65
65
|
# It's OK if there are no constants to process if we received a valid file/path.
|
|
66
66
|
if constants_to_process.empty? && requested_paths.none? { |p| File.exist?(p) }
|
|
67
|
+
# When running within the add-on, return early so this expected case is not logged as an error
|
|
68
|
+
return [] if @lsp_addon
|
|
69
|
+
|
|
67
70
|
report_error(<<~ERROR)
|
|
68
71
|
No classes/modules can be matched for RBI generation.
|
|
69
72
|
Please check that the requested classes/modules include processable DSL methods.
|
|
@@ -138,7 +141,7 @@ module Tapioca
|
|
|
138
141
|
active_compilers.each do |compiler|
|
|
139
142
|
constants.merge(compiler.processable_constants)
|
|
140
143
|
end
|
|
141
|
-
constants =
|
|
144
|
+
constants = filter_anonymous_constants(constants)
|
|
142
145
|
constants -= skipped_constants
|
|
143
146
|
|
|
144
147
|
unless requested_constants.empty? && requested_paths.empty?
|
|
@@ -154,32 +157,8 @@ module Tapioca
|
|
|
154
157
|
end
|
|
155
158
|
|
|
156
159
|
#: (Set[Module[top]] constants) -> Set[Module[top]]
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
constants_by_name = constants
|
|
160
|
-
.group_by { |c| Runtime::Reflection.name_of(c) }
|
|
161
|
-
.select { |name, _| !name.nil? }
|
|
162
|
-
|
|
163
|
-
constants_by_name = T.cast(constants_by_name, T::Hash[String, T::Array[T::Module[T.anything]]])
|
|
164
|
-
|
|
165
|
-
# Find the constants that have been reloaded
|
|
166
|
-
reloaded_constants = constants_by_name.select { |_, constants| constants.size > 1 }.keys
|
|
167
|
-
|
|
168
|
-
unless reloaded_constants.empty? || @lsp_addon
|
|
169
|
-
reloaded_constant_names = reloaded_constants.map { |name| "`#{name}`" }.join(", ")
|
|
170
|
-
|
|
171
|
-
$stderr.puts("WARNING: Multiple constants with the same name: #{reloaded_constant_names}")
|
|
172
|
-
$stderr.puts("Make sure some object is not holding onto these constants during an app reload.")
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Look up all the constants back from their names. The resulting constant set will be the
|
|
176
|
-
# set of constants that are actually in memory with those names.
|
|
177
|
-
filtered_constants = constants_by_name
|
|
178
|
-
.keys
|
|
179
|
-
.map { |name| T.cast(Runtime::Reflection.constantize(name), T::Module[T.anything]) }
|
|
180
|
-
.select { |mod| Runtime::Reflection.constant_defined?(mod) }
|
|
181
|
-
|
|
182
|
-
Set.new.compare_by_identity.merge(filtered_constants)
|
|
160
|
+
def filter_anonymous_constants(constants)
|
|
161
|
+
constants.keep_if { |constant| Runtime::Reflection.name_of(constant) }
|
|
183
162
|
end
|
|
184
163
|
|
|
185
164
|
#: (Module[top] constant) -> RBI::File?
|
data/lib/tapioca/gem/events.rb
CHANGED
|
@@ -109,7 +109,7 @@ module Tapioca
|
|
|
109
109
|
#| untyped signature,
|
|
110
110
|
#| Array[[Symbol, String]] parameters
|
|
111
111
|
#| ) -> void
|
|
112
|
-
def initialize(symbol, constant, method, node, signature, parameters)
|
|
112
|
+
def initialize(symbol, constant, method, node, signature, parameters)
|
|
113
113
|
super(symbol, constant)
|
|
114
114
|
@node = node
|
|
115
115
|
@method = method
|
|
@@ -15,7 +15,7 @@ module Tapioca
|
|
|
15
15
|
"shareable_constant_value:",
|
|
16
16
|
"rubocop:",
|
|
17
17
|
"@requires_ancestor:",
|
|
18
|
-
] #: Array[String]
|
|
18
|
+
].freeze #: Array[String]
|
|
19
19
|
|
|
20
20
|
#: (Pipeline pipeline, Rubydex::Graph gem_graph) -> void
|
|
21
21
|
def initialize(pipeline, gem_graph)
|
|
@@ -69,13 +69,11 @@ module Tapioca
|
|
|
69
69
|
end
|
|
70
70
|
return [] unless declaration
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.map { |comment| comment.string.gsub(/^#+ ?/, "") }
|
|
78
|
-
.reject { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
|
|
72
|
+
# Definitions often share a comment block, such as a license header, so de-duplicate them
|
|
73
|
+
lines = declaration.definitions
|
|
74
|
+
.map { |definition| comment_lines(definition) }
|
|
75
|
+
.uniq
|
|
76
|
+
.flatten
|
|
79
77
|
|
|
80
78
|
# Strip leading and trailing blank lines, matching YARD's behavior
|
|
81
79
|
lines = lines.reverse_each.drop_while(&:empty?).reverse_each.drop_while(&:empty?)
|
|
@@ -83,6 +81,13 @@ module Tapioca
|
|
|
83
81
|
lines.map! { |line| RBI::Comment.new(line) }
|
|
84
82
|
end
|
|
85
83
|
|
|
84
|
+
#: (Rubydex::Definition definition) -> Array[String]
|
|
85
|
+
def comment_lines(definition)
|
|
86
|
+
lines = definition.comments.map { |comment| comment.string.gsub(/^#+ ?/, "") }
|
|
87
|
+
lines.reject! { |line| IGNORED_COMMENTS.any? { |comment| line.include?(comment) } || rbs_comment?(line) }
|
|
88
|
+
lines
|
|
89
|
+
end
|
|
90
|
+
|
|
86
91
|
# @override
|
|
87
92
|
#: (NodeAdded event) -> bool
|
|
88
93
|
def ignore?(event)
|
|
@@ -69,6 +69,7 @@ module Tapioca
|
|
|
69
69
|
|
|
70
70
|
begin
|
|
71
71
|
signature = signature_of!(method)
|
|
72
|
+
signature ||= inferred_attr_writer_signature(method, constant)
|
|
72
73
|
method = signature.method if signature #: UnboundMethod
|
|
73
74
|
|
|
74
75
|
case @pipeline.method_definition_in_gem(method.name, constant)
|
|
@@ -101,7 +102,7 @@ module Tapioca
|
|
|
101
102
|
sanitized_parameters = parameters.each_with_index.map do |(type, name), index|
|
|
102
103
|
fallback_arg_name = "_arg#{index}"
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
sig_name = if name
|
|
105
106
|
name.to_s
|
|
106
107
|
else
|
|
107
108
|
# For attr_writer methods, Sorbet signatures have the name
|
|
@@ -125,10 +126,14 @@ module Tapioca
|
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
|
|
128
|
-
# Sanitize param names
|
|
129
|
-
|
|
129
|
+
# Sanitize param names, except for anonymous splat, keyword splat,
|
|
130
|
+
# and block parameters. Ruby reflects those as `:*`, `:**`, and `:&`,
|
|
131
|
+
# and Sorbet signatures use the same names to store their types.
|
|
132
|
+
is_anonymous_parameter = anonymous_parameter_name?(type, sig_name)
|
|
133
|
+
sig_name = fallback_arg_name unless is_anonymous_parameter || valid_parameter_name?(sig_name)
|
|
134
|
+
param_name = is_anonymous_parameter ? nil : sig_name
|
|
130
135
|
|
|
131
|
-
[type,
|
|
136
|
+
[type, param_name, sig_name]
|
|
132
137
|
end
|
|
133
138
|
|
|
134
139
|
rbi_method = RBI::Method.new(
|
|
@@ -137,26 +142,27 @@ module Tapioca
|
|
|
137
142
|
visibility: visibility,
|
|
138
143
|
)
|
|
139
144
|
|
|
140
|
-
sanitized_parameters.each do |type,
|
|
145
|
+
sanitized_parameters.each do |type, param_name, _sig_name|
|
|
141
146
|
case type
|
|
142
147
|
when :req
|
|
143
|
-
rbi_method << RBI::ReqParam.new(
|
|
148
|
+
rbi_method << RBI::ReqParam.new(param_name)
|
|
144
149
|
when :opt
|
|
145
|
-
rbi_method << RBI::OptParam.new(
|
|
150
|
+
rbi_method << RBI::OptParam.new(param_name, "T.unsafe(nil)")
|
|
146
151
|
when :rest
|
|
147
|
-
rbi_method << RBI::RestParam.new(
|
|
152
|
+
rbi_method << RBI::RestParam.new(param_name)
|
|
148
153
|
when :keyreq
|
|
149
|
-
rbi_method << RBI::KwParam.new(
|
|
154
|
+
rbi_method << RBI::KwParam.new(param_name)
|
|
150
155
|
when :key
|
|
151
|
-
rbi_method << RBI::KwOptParam.new(
|
|
156
|
+
rbi_method << RBI::KwOptParam.new(param_name, "T.unsafe(nil)")
|
|
152
157
|
when :keyrest
|
|
153
|
-
rbi_method << RBI::KwRestParam.new(
|
|
158
|
+
rbi_method << RBI::KwRestParam.new(param_name)
|
|
154
159
|
when :block
|
|
155
|
-
rbi_method << RBI::BlockParam.new(
|
|
160
|
+
rbi_method << RBI::BlockParam.new(param_name)
|
|
156
161
|
end
|
|
157
162
|
end
|
|
158
163
|
|
|
159
|
-
|
|
164
|
+
parameters_for_signature = sanitized_parameters.map { |type, _param_name, sig_name| [type, sig_name] }
|
|
165
|
+
@pipeline.push_method(symbol_name, constant, method, rbi_method, signature, parameters_for_signature)
|
|
160
166
|
tree << rbi_method
|
|
161
167
|
end
|
|
162
168
|
|
|
@@ -192,6 +198,66 @@ module Tapioca
|
|
|
192
198
|
}
|
|
193
199
|
end
|
|
194
200
|
|
|
201
|
+
#: (UnboundMethod method, Module[top] constant) -> untyped
|
|
202
|
+
def inferred_attr_writer_signature(method, constant)
|
|
203
|
+
reader_method = attr_reader_for_writer(method, constant)
|
|
204
|
+
return unless reader_method
|
|
205
|
+
|
|
206
|
+
reader_signature = signature_of(reader_method)
|
|
207
|
+
return unless reader_signature
|
|
208
|
+
|
|
209
|
+
build_attr_writer_signature(method, reader_method, reader_signature)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
#: (UnboundMethod method, Module[top] constant) -> UnboundMethod?
|
|
213
|
+
def attr_reader_for_writer(method, constant)
|
|
214
|
+
method_name = method.name.to_s
|
|
215
|
+
return unless method_name.end_with?("=")
|
|
216
|
+
return unless method.parameters == [[:req]]
|
|
217
|
+
|
|
218
|
+
reader_method = T.let(constant.instance_method(method_name.delete_suffix("=").to_sym), UnboundMethod)
|
|
219
|
+
reader_method = original_method(reader_method)
|
|
220
|
+
return unless same_source_location?(method, reader_method)
|
|
221
|
+
return unless method_owned_by_constant?(reader_method, constant)
|
|
222
|
+
|
|
223
|
+
reader_method
|
|
224
|
+
rescue NameError
|
|
225
|
+
nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
#: (UnboundMethod writer_method, UnboundMethod reader_method, untyped reader_signature) -> untyped
|
|
229
|
+
def build_attr_writer_signature(writer_method, reader_method, reader_signature)
|
|
230
|
+
return unless reader_signature.arg_types.empty?
|
|
231
|
+
return unless reader_signature.kwarg_types.empty?
|
|
232
|
+
return if reader_signature.rest_type
|
|
233
|
+
return if reader_signature.keyrest_type
|
|
234
|
+
return if reader_signature.block_type
|
|
235
|
+
|
|
236
|
+
T::Private::Methods::Signature.new(
|
|
237
|
+
method: writer_method,
|
|
238
|
+
method_name: writer_method.name,
|
|
239
|
+
raw_arg_types: { reader_method.name => reader_signature.return_type },
|
|
240
|
+
raw_return_type: reader_signature.return_type,
|
|
241
|
+
bind: nil,
|
|
242
|
+
mode: reader_signature.mode,
|
|
243
|
+
check_level: reader_signature.check_level,
|
|
244
|
+
on_failure: reader_signature.on_failure,
|
|
245
|
+
override_allow_incompatible: reader_signature.override_allow_incompatible,
|
|
246
|
+
defined_raw: reader_signature.defined_raw,
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
#: (UnboundMethod method) -> UnboundMethod
|
|
251
|
+
def original_method(method)
|
|
252
|
+
T.let(signature_of(method)&.method || method, UnboundMethod)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
#: (UnboundMethod method, UnboundMethod other_method) -> bool
|
|
256
|
+
def same_source_location?(method, other_method)
|
|
257
|
+
source_location = method.source_location
|
|
258
|
+
!!source_location && source_location == other_method.source_location
|
|
259
|
+
end
|
|
260
|
+
|
|
195
261
|
#: (Module[top] constant, String method_name) -> bool
|
|
196
262
|
def struct_method?(constant, method_name)
|
|
197
263
|
return false unless T::Props::ClassMethods === constant
|
|
@@ -11,7 +11,7 @@ module Tapioca
|
|
|
11
11
|
#: (ScopeNodeAdded event) -> void
|
|
12
12
|
def on_scope(event)
|
|
13
13
|
constant = event.constant
|
|
14
|
-
return unless T::Enum > event.constant
|
|
14
|
+
return unless T::Enum > event.constant
|
|
15
15
|
|
|
16
16
|
enum_block = RBI::TEnumBlock.new
|
|
17
17
|
|
data/lib/tapioca/gem/pipeline.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Tapioca
|
|
|
7
7
|
include Runtime::Reflection
|
|
8
8
|
include RBIHelper
|
|
9
9
|
|
|
10
|
-
IGNORED_SYMBOLS = ["YAML", "MiniTest", "Mutex"] #: Array[String]
|
|
10
|
+
IGNORED_SYMBOLS = ["YAML", "MiniTest", "Mutex"].freeze #: Array[String]
|
|
11
11
|
|
|
12
12
|
#: Gemfile::GemSpec
|
|
13
13
|
attr_reader :gem
|
|
@@ -100,7 +100,7 @@ module Tapioca
|
|
|
100
100
|
#| untyped signature,
|
|
101
101
|
#| Array[[Symbol, String]] parameters
|
|
102
102
|
#| ) -> void
|
|
103
|
-
def push_method(symbol, constant, method, node, signature, parameters)
|
|
103
|
+
def push_method(symbol, constant, method, node, signature, parameters)
|
|
104
104
|
@events << Gem::MethodNodeAdded.new(symbol, constant, method, node, signature, parameters)
|
|
105
105
|
end
|
|
106
106
|
|
|
@@ -356,7 +356,7 @@ module Tapioca
|
|
|
356
356
|
|
|
357
357
|
#: (Class[top] constant) -> String?
|
|
358
358
|
def compile_superclass(constant)
|
|
359
|
-
superclass = nil #: Class[top]?
|
|
359
|
+
superclass = nil #: Class[top]?
|
|
360
360
|
|
|
361
361
|
while (superclass = superclass_of(constant))
|
|
362
362
|
constant_name = name_of(constant)
|
data/lib/tapioca/gemfile.rb
CHANGED
|
@@ -48,8 +48,21 @@ module Tapioca
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
#: (String path) -> bool
|
|
52
|
+
def excluded_gem_path?(path)
|
|
53
|
+
excluded_gem_specs.any? { |spec| spec.contains_path?(path) }
|
|
54
|
+
end
|
|
55
|
+
|
|
51
56
|
private
|
|
52
57
|
|
|
58
|
+
#: -> Array[GemSpec]
|
|
59
|
+
def excluded_gem_specs
|
|
60
|
+
@excluded_gem_specs ||= @excluded_gems.filter_map do |name|
|
|
61
|
+
spec = ::Gem.loaded_specs[name]
|
|
62
|
+
GemSpec.new(spec) if spec
|
|
63
|
+
end #: Array[GemSpec]?
|
|
64
|
+
end
|
|
65
|
+
|
|
53
66
|
#: File
|
|
54
67
|
attr_reader(:gemfile, :lockfile)
|
|
55
68
|
|
|
@@ -5,7 +5,7 @@ module Tapioca
|
|
|
5
5
|
# @requires_ancestor: Thor
|
|
6
6
|
module EnvHelper
|
|
7
7
|
#: (Hash[Symbol, untyped] options) -> void
|
|
8
|
-
def set_environment(options)
|
|
8
|
+
def set_environment(options)
|
|
9
9
|
ENV["RAILS_ENV"] = ENV["RACK_ENV"] = options[:environment]
|
|
10
10
|
ENV["RUBY_DEBUG_LAZY"] = "1"
|
|
11
11
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module FileHelper
|
|
8
|
+
#: (Pathname filename, Pathname | String old_path, Pathname | String new_path) -> String?
|
|
9
|
+
def file_diff(filename, old_path, new_path)
|
|
10
|
+
filename = filename.to_s
|
|
11
|
+
stdout, stderr, status = Open3.capture3(
|
|
12
|
+
"diff",
|
|
13
|
+
"-u",
|
|
14
|
+
"--label=Current #{filename}",
|
|
15
|
+
old_path.to_s,
|
|
16
|
+
"--label=Expected #{filename} (After running `bin/tapioca dsl`)",
|
|
17
|
+
new_path.to_s,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
unless [0, 1].include?(status.exitstatus)
|
|
21
|
+
error_msg("Failed to create #{filename} diff. #{stderr.chomp}")
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
stdout
|
|
26
|
+
rescue SystemCallError => e
|
|
27
|
+
error_msg("Failed to create #{filename} diff. #{e.message}")
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
RED = "\e[31m" #: String
|
|
34
|
+
CLEAR = "\e[0m" #: String
|
|
35
|
+
|
|
36
|
+
#: (String message) -> void
|
|
37
|
+
def error_msg(message)
|
|
38
|
+
message = "#{RED}#{message}#{CLEAR}"
|
|
39
|
+
Kernel.warn(message)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -78,7 +78,7 @@ module Tapioca
|
|
|
78
78
|
res = sorbet(
|
|
79
79
|
"--no-config",
|
|
80
80
|
"--error-url-base=#{error_url_base}",
|
|
81
|
-
"--stop-after
|
|
81
|
+
"--stop-after resolver",
|
|
82
82
|
dsl_dir,
|
|
83
83
|
gem_dir,
|
|
84
84
|
)
|
|
@@ -129,12 +129,16 @@ module Tapioca
|
|
|
129
129
|
ERR
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
redef_errors = errors.select { |error| error.code == 4010 }
|
|
134
|
-
update_gem_rbis_strictnesses(redef_errors, gem_dir)
|
|
135
|
-
end
|
|
132
|
+
handled_errors = apply_validation_fixes(errors, gem_dir: gem_dir, auto_strictness: auto_strictness)
|
|
136
133
|
|
|
137
134
|
Kernel.raise Tapioca::Error, error_messages.join("\n") if parse_errors.any?
|
|
135
|
+
|
|
136
|
+
unhandled_errors = errors - handled_errors
|
|
137
|
+
unhandled_errors.reject! { |error| ignored_validation_error?(error, gem_dir: gem_dir, dsl_dir: dsl_dir) }
|
|
138
|
+
|
|
139
|
+
if unhandled_errors.empty?
|
|
140
|
+
say(" No errors found\n\n", [:green, :bold])
|
|
141
|
+
end
|
|
138
142
|
end
|
|
139
143
|
|
|
140
144
|
private
|
|
@@ -258,6 +262,93 @@ module Tapioca
|
|
|
258
262
|
)
|
|
259
263
|
end
|
|
260
264
|
|
|
265
|
+
SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG =
|
|
266
|
+
"--suppress-payload-superclass-redefinition-for" #: String
|
|
267
|
+
|
|
268
|
+
#: (Array[Spoom::Sorbet::Errors::Error] errors) -> void
|
|
269
|
+
def update_sorbet_config_for_payload_superclass_redefinitions(errors)
|
|
270
|
+
errors
|
|
271
|
+
.filter_map { |error| payload_superclass_constant_from_error(error) }
|
|
272
|
+
.uniq
|
|
273
|
+
.each { |constant| add_payload_superclass_suppression_to_config(constant) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
#: (Spoom::Sorbet::Errors::Error error) -> String?
|
|
277
|
+
def payload_superclass_constant_from_error(error)
|
|
278
|
+
error.more.each do |line|
|
|
279
|
+
if line =~ /--suppress-payload-superclass-redefinition-for=([^\s`]+)/
|
|
280
|
+
return T.must(Regexp.last_match(1))
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
#: (
|
|
288
|
+
#| Array[Spoom::Sorbet::Errors::Error] errors,
|
|
289
|
+
#| gem_dir: String,
|
|
290
|
+
#| auto_strictness: bool,
|
|
291
|
+
#| ) -> Array[Spoom::Sorbet::Errors::Error]
|
|
292
|
+
def apply_validation_fixes(errors, gem_dir:, auto_strictness:)
|
|
293
|
+
handled_errors = [] #: Array[Spoom::Sorbet::Errors::Error]
|
|
294
|
+
|
|
295
|
+
if auto_strictness
|
|
296
|
+
redef_errors = errors.select { |error| error.code == 4010 }
|
|
297
|
+
update_gem_rbis_strictnesses(redef_errors, gem_dir) if redef_errors.any?
|
|
298
|
+
handled_errors.concat(redef_errors)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Automatically fix payload superclass redefinition errors.
|
|
302
|
+
payload_superclass_errors = errors.select { |error| payload_superclass_error?(error) }
|
|
303
|
+
if payload_superclass_errors.any?
|
|
304
|
+
update_sorbet_config_for_payload_superclass_redefinitions(payload_superclass_errors)
|
|
305
|
+
end
|
|
306
|
+
handled_errors.concat(payload_superclass_errors)
|
|
307
|
+
|
|
308
|
+
handled_errors
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
#: (Spoom::Sorbet::Errors::Error error) -> bool
|
|
312
|
+
def payload_superclass_error?(error)
|
|
313
|
+
error.more.any? { |line| line.include?(SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG) }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
#: (Spoom::Sorbet::Errors::Error error, gem_dir: String, dsl_dir: String) -> bool
|
|
317
|
+
def ignored_validation_error?(error, gem_dir:, dsl_dir:)
|
|
318
|
+
return false if Dir.exist?(gem_dir) && !Dir.glob("#{gem_dir}/**/*.rbi").empty?
|
|
319
|
+
|
|
320
|
+
[5002, 5067].include?(error.code) && T.must(error.file).start_with?(dsl_dir)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
#: (String constant) -> void
|
|
324
|
+
def add_payload_superclass_suppression_to_config(constant)
|
|
325
|
+
flag = "#{SUPPRESS_PAYLOAD_SUPERCLASS_REDEFINITION_FLAG}=#{constant}"
|
|
326
|
+
config_path = Tapioca::SORBET_CONFIG_FILE
|
|
327
|
+
config = File.exist?(config_path) ? File.read(config_path) : ""
|
|
328
|
+
flag_already_present = config.lines(chomp: true).include?(flag)
|
|
329
|
+
|
|
330
|
+
if flag_already_present
|
|
331
|
+
say(
|
|
332
|
+
"\n Payload superclass of `#{constant}` was redefined; `#{flag}` is already in sorbet/config\n",
|
|
333
|
+
[:yellow, :bold],
|
|
334
|
+
)
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
FileUtils.mkdir_p(File.dirname(config_path))
|
|
339
|
+
if config.empty?
|
|
340
|
+
File.write(config_path, "#{flag}\n")
|
|
341
|
+
else
|
|
342
|
+
suffix = config.end_with?("\n") ? "" : "\n"
|
|
343
|
+
File.write(config_path, "#{config}#{suffix}#{flag}\n")
|
|
344
|
+
end
|
|
345
|
+
say(
|
|
346
|
+
"\n Added `#{flag}` to sorbet/config (payload superclass of `#{constant}` was redefined)",
|
|
347
|
+
[:yellow, :bold],
|
|
348
|
+
)
|
|
349
|
+
say("\n")
|
|
350
|
+
end
|
|
351
|
+
|
|
261
352
|
#: (Array[Spoom::Sorbet::Errors::Error] errors, String gem_dir) -> void
|
|
262
353
|
def update_gem_rbis_strictnesses(errors, gem_dir)
|
|
263
354
|
files = []
|
|
@@ -37,7 +37,7 @@ module Tapioca
|
|
|
37
37
|
create_typed_param(RBI::OptParam.new(name, default), type)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
#: (String name, type: String) -> RBI::TypedParam
|
|
40
|
+
#: (String? name, type: String) -> RBI::TypedParam
|
|
41
41
|
def create_rest_param(name, type:)
|
|
42
42
|
create_typed_param(RBI::RestParam.new(name), type)
|
|
43
43
|
end
|
|
@@ -52,12 +52,12 @@ module Tapioca
|
|
|
52
52
|
create_typed_param(RBI::KwOptParam.new(name, default), type)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
#: (String name, type: String) -> RBI::TypedParam
|
|
55
|
+
#: (String? name, type: String) -> RBI::TypedParam
|
|
56
56
|
def create_kw_rest_param(name, type:)
|
|
57
57
|
create_typed_param(RBI::KwRestParam.new(name), type)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
#: (String name, type: String) -> RBI::TypedParam
|
|
60
|
+
#: (String? name, type: String) -> RBI::TypedParam
|
|
61
61
|
def create_block_param(name, type:)
|
|
62
62
|
create_typed_param(RBI::BlockParam.new(name), type)
|
|
63
63
|
end
|
|
@@ -110,5 +110,19 @@ module Tapioca
|
|
|
110
110
|
def valid_parameter_name?(name)
|
|
111
111
|
Prism.parse_success?("def sentinel_method_name(#{name}:); end")
|
|
112
112
|
end
|
|
113
|
+
|
|
114
|
+
#: (Symbol type, String name) -> bool
|
|
115
|
+
def anonymous_parameter_name?(type, name)
|
|
116
|
+
case type
|
|
117
|
+
when :rest
|
|
118
|
+
name == "*"
|
|
119
|
+
when :keyrest
|
|
120
|
+
name == "**"
|
|
121
|
+
when :block
|
|
122
|
+
name == "&"
|
|
123
|
+
else
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
113
127
|
end
|
|
114
128
|
end
|
|
@@ -24,7 +24,7 @@ module Tapioca
|
|
|
24
24
|
|
|
25
25
|
#: (String, rbi_mode: bool) { (String stderr) -> void } -> void
|
|
26
26
|
def sorbet_syntax_check!(source, rbi_mode:, &on_failure)
|
|
27
|
-
quoted_source =
|
|
27
|
+
quoted_source = source.shellescape
|
|
28
28
|
|
|
29
29
|
result = if rbi_mode
|
|
30
30
|
# --e-rbi cannot be used on its own, so we pass a dummy value like `-e ""`
|
data/lib/tapioca/internal.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "tapioca/runtime/dynamic_mixin_compiler"
|
|
|
12
12
|
require "tapioca/sorbet_ext/backcompat_patches"
|
|
13
13
|
require "tapioca/sorbet_ext/name_patch"
|
|
14
14
|
require "tapioca/sorbet_ext/generic_name_patch"
|
|
15
|
+
require "tapioca/sorbet_ext/generic_type_patch"
|
|
15
16
|
require "tapioca/sorbet_ext/proc_bind_patch"
|
|
16
17
|
require "tapioca/sorbet_ext/void_patch"
|
|
17
18
|
require "tapioca/runtime/generic_type_registry"
|
|
@@ -53,6 +54,7 @@ require "tapioca/helpers/rbi_helper"
|
|
|
53
54
|
require "tapioca/helpers/package_url"
|
|
54
55
|
require "tapioca/helpers/cli_helper"
|
|
55
56
|
require "tapioca/helpers/config_helper"
|
|
57
|
+
require "tapioca/helpers/file_helper"
|
|
56
58
|
require "tapioca/helpers/rbi_files_helper"
|
|
57
59
|
require "tapioca/helpers/env_helper"
|
|
58
60
|
|
data/lib/tapioca/loaders/gem.rb
CHANGED
|
@@ -49,6 +49,39 @@ module Tapioca
|
|
|
49
49
|
@halt_upon_load_error = halt_upon_load_error
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
#: -> void
|
|
53
|
+
def load_gem_extensions
|
|
54
|
+
say("Loading gem extension classes... ")
|
|
55
|
+
|
|
56
|
+
# Extensions are loaded before the bundle is required so that they can patch the gems
|
|
57
|
+
# they apply to as those gems are being loaded.
|
|
58
|
+
Dir.glob("#{Tapioca::TAPIOCA_DIR}/gem/extensions/**/*.rb").each do |extension|
|
|
59
|
+
require File.expand_path(extension)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
::Gem.find_files("tapioca/gem/extensions/*.rb").each do |extension|
|
|
63
|
+
next if @bundle.excluded_gem_path?(extension)
|
|
64
|
+
|
|
65
|
+
require File.expand_path(extension)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
say("Done", :green)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#: (Tapioca::Gemfile gemfile, String? initialize_file, String? require_file, bool halt_upon_load_error) -> void
|
|
72
|
+
def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error)
|
|
73
|
+
require_helper(initialize_file)
|
|
74
|
+
load_gem_extensions
|
|
75
|
+
|
|
76
|
+
load_rails_application(halt_upon_load_error: halt_upon_load_error)
|
|
77
|
+
|
|
78
|
+
gemfile.require_bundle
|
|
79
|
+
|
|
80
|
+
require_helper(require_file)
|
|
81
|
+
|
|
82
|
+
load_rails_engines
|
|
83
|
+
end
|
|
84
|
+
|
|
52
85
|
#: -> void
|
|
53
86
|
def require_gem_file
|
|
54
87
|
say("Requiring all gems to prepare for compiling... ")
|
|
@@ -15,19 +15,6 @@ module Tapioca
|
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
-
#: (Tapioca::Gemfile gemfile, String? initialize_file, String? require_file, bool halt_upon_load_error) -> void
|
|
19
|
-
def load_bundle(gemfile, initialize_file, require_file, halt_upon_load_error)
|
|
20
|
-
require_helper(initialize_file)
|
|
21
|
-
|
|
22
|
-
load_rails_application(halt_upon_load_error: halt_upon_load_error)
|
|
23
|
-
|
|
24
|
-
gemfile.require_bundle
|
|
25
|
-
|
|
26
|
-
require_helper(require_file)
|
|
27
|
-
|
|
28
|
-
load_rails_engines
|
|
29
|
-
end
|
|
30
|
-
|
|
31
18
|
#: (?environment_load: bool, ?eager_load: bool, ?app_root: String, ?halt_upon_load_error: bool) -> void
|
|
32
19
|
def load_rails_application(environment_load: false, eager_load: false, app_root: ".", halt_upon_load_error: true)
|
|
33
20
|
return unless File.exist?(File.expand_path("config/application.rb", app_root))
|