sorbet_view 0.18.0 → 0.19.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/lib/sorbet_view/compiler/template_context.rb +110 -8
- data/lib/sorbet_view/version.rb +1 -1
- data/lib/tapioca/dsl/compilers/sorbet_view.rb +100 -31
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4a6dcbfa73d97af80ddfc038337a094df6adcfe3be2cdb95997e2eb6304993e
|
|
4
|
+
data.tar.gz: 5d96c0eabf3534f72abebb91738f22546f157266368d7f92cf6faf6ac38ce44e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6781e594f6fbcb73e9b7a62c9931c97c5f8cbf5375530dff6a6e4a35c7fce73e2333d586aa81e079c32d92b19c59b5b493abe27b0e638ca24e6bc9654d7987d
|
|
7
|
+
data.tar.gz: 7a026a08b7eb1ebddbb586799095a9de5c39159e26886bc3b0ad2c11ce8382b28bc57c79ef7768c0b929cd3c4083c49663fa11a0b7d81aeb8777b0ce3c3ebd4a
|
|
@@ -51,6 +51,8 @@ module SorbetView
|
|
|
51
51
|
class << self
|
|
52
52
|
extend T::Sig
|
|
53
53
|
|
|
54
|
+
@all_concrete_controllers = T.let(nil, T.nilable(T::Array[T.untyped]))
|
|
55
|
+
|
|
54
56
|
private
|
|
55
57
|
|
|
56
58
|
# Strip the matching input_dir prefix from a template path
|
|
@@ -107,11 +109,7 @@ module SorbetView
|
|
|
107
109
|
|
|
108
110
|
dir = File.dirname(relative)
|
|
109
111
|
|
|
110
|
-
parts =
|
|
111
|
-
[basename]
|
|
112
|
-
else
|
|
113
|
-
dir.split('/') + [basename]
|
|
114
|
-
end
|
|
112
|
+
parts = dir == '.' ? [basename] : dir.split('/') + [basename]
|
|
115
113
|
|
|
116
114
|
parts += format_parts
|
|
117
115
|
|
|
@@ -125,11 +123,12 @@ module SorbetView
|
|
|
125
123
|
|
|
126
124
|
sig { params(path: String, ruby_path: String, config: Configuration).returns(TemplateContext) }
|
|
127
125
|
def resolve_controller_view(path, ruby_path, config)
|
|
126
|
+
helper_includes = resolve_runtime_helper_includes(path, config)
|
|
128
127
|
new(
|
|
129
128
|
class_name: "SorbetView::Generated::#{path_to_class_name(path, config)}",
|
|
130
129
|
superclass: '::ActionView::Base',
|
|
131
130
|
includes: [
|
|
132
|
-
|
|
131
|
+
*helper_includes,
|
|
133
132
|
*config.extra_includes
|
|
134
133
|
],
|
|
135
134
|
template_path: path,
|
|
@@ -153,11 +152,12 @@ module SorbetView
|
|
|
153
152
|
|
|
154
153
|
sig { params(path: String, ruby_path: String, config: Configuration).returns(TemplateContext) }
|
|
155
154
|
def resolve_layout(path, ruby_path, config)
|
|
155
|
+
helper_includes = resolve_runtime_helper_includes(path, config)
|
|
156
156
|
new(
|
|
157
157
|
class_name: "SorbetView::Generated::#{path_to_class_name(path, config)}",
|
|
158
158
|
superclass: '::ActionView::Base',
|
|
159
159
|
includes: [
|
|
160
|
-
|
|
160
|
+
*helper_includes,
|
|
161
161
|
*config.extra_includes
|
|
162
162
|
],
|
|
163
163
|
template_path: path,
|
|
@@ -167,11 +167,12 @@ module SorbetView
|
|
|
167
167
|
|
|
168
168
|
sig { params(path: String, ruby_path: String, config: Configuration).returns(TemplateContext) }
|
|
169
169
|
def resolve_partial(path, ruby_path, config)
|
|
170
|
+
helper_includes = resolve_runtime_helper_includes(path, config)
|
|
170
171
|
new(
|
|
171
172
|
class_name: "SorbetView::Generated::#{path_to_class_name(path, config)}",
|
|
172
173
|
superclass: '::ActionView::Base',
|
|
173
174
|
includes: [
|
|
174
|
-
|
|
175
|
+
*helper_includes,
|
|
175
176
|
*config.extra_includes
|
|
176
177
|
],
|
|
177
178
|
template_path: path,
|
|
@@ -189,6 +190,107 @@ module SorbetView
|
|
|
189
190
|
ruby_path: ruby_path
|
|
190
191
|
)
|
|
191
192
|
end
|
|
193
|
+
|
|
194
|
+
sig { params(path: String, config: Configuration).returns(T::Array[String]) }
|
|
195
|
+
def resolve_runtime_helper_includes(path, config)
|
|
196
|
+
return ['::ApplicationController::HelperMethods'] unless defined?(::ActionController::Base)
|
|
197
|
+
|
|
198
|
+
relative = strip_input_dir(path, config)
|
|
199
|
+
names = []
|
|
200
|
+
if relative.start_with?('layouts/')
|
|
201
|
+
names = helper_modules_for_layout(relative)
|
|
202
|
+
elsif File.basename(relative).start_with?('_')
|
|
203
|
+
names = helper_modules_for_partial(relative)
|
|
204
|
+
else
|
|
205
|
+
names = helper_modules_for_controller_view(relative)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
return ['::ApplicationController::HelperMethods'] if names.empty?
|
|
209
|
+
|
|
210
|
+
names.map { |name| "::#{name}" }
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
$stderr.puts "[SorbetView] resolve_runtime_helper_includes failed: #{e.class}: #{e.message}"
|
|
213
|
+
['::ApplicationController::HelperMethods']
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
sig { params(relative: String).returns(T::Array[String]) }
|
|
217
|
+
def helper_modules_for_controller_view(relative)
|
|
218
|
+
controller_path = File.dirname(relative)
|
|
219
|
+
return [] if controller_path.nil? || controller_path.empty? || controller_path == '.'
|
|
220
|
+
|
|
221
|
+
controller = find_controller_by_path(controller_path)
|
|
222
|
+
helper_module_names_for(controller)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
sig { params(relative: String).returns(T::Array[String]) }
|
|
226
|
+
def helper_modules_for_layout(relative)
|
|
227
|
+
filename = File.basename(relative)
|
|
228
|
+
layout_name = T.must(filename.split('.').first).to_s
|
|
229
|
+
return [] if layout_name.empty?
|
|
230
|
+
|
|
231
|
+
all_concrete_controllers.flat_map do |controller|
|
|
232
|
+
next [] unless controller.respond_to?(:_layout)
|
|
233
|
+
next [] unless controller._layout == layout_name
|
|
234
|
+
|
|
235
|
+
helper_module_names_for(controller)
|
|
236
|
+
end.uniq.sort
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
sig { params(relative: String).returns(T::Array[String]) }
|
|
240
|
+
def helper_modules_for_partial(relative)
|
|
241
|
+
dir = File.dirname(relative)
|
|
242
|
+
return [] if dir.nil? || dir.empty? || dir == '.'
|
|
243
|
+
|
|
244
|
+
# Partials can live under subdirectories that share a parent controller
|
|
245
|
+
# (e.g. users/forms/_field.html.erb should use UsersController helpers).
|
|
246
|
+
parts = dir.split('/')
|
|
247
|
+
candidates = parts.length.downto(1).map { |i| parts.first(i).join('/') }
|
|
248
|
+
|
|
249
|
+
candidates.each do |controller_path|
|
|
250
|
+
controller = find_controller_by_path(controller_path)
|
|
251
|
+
names = helper_module_names_for(controller)
|
|
252
|
+
return names unless names.empty?
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
[]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
sig { params(controller_path: String).returns(T.untyped) }
|
|
259
|
+
def find_controller_by_path(controller_path)
|
|
260
|
+
all_concrete_controllers.find do |controller|
|
|
261
|
+
controller.respond_to?(:controller_path) && controller.controller_path == controller_path
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
sig { returns(T::Array[T.untyped]) }
|
|
266
|
+
def all_concrete_controllers
|
|
267
|
+
controllers = @all_concrete_controllers
|
|
268
|
+
return controllers if controllers
|
|
269
|
+
|
|
270
|
+
controllers = ObjectSpace.each_object(Class).select do |klass|
|
|
271
|
+
klass < ::ActionController::Base && !klass.abstract?
|
|
272
|
+
rescue StandardError
|
|
273
|
+
false
|
|
274
|
+
end.sort_by { |klass| (klass.name || "AnonymousController:#{klass.object_id}").to_s }
|
|
275
|
+
|
|
276
|
+
@all_concrete_controllers = controllers
|
|
277
|
+
controllers
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
sig { params(controller: T.untyped).returns(T::Array[String]) }
|
|
281
|
+
def helper_module_names_for(controller)
|
|
282
|
+
return [] unless controller
|
|
283
|
+
return [] unless controller.respond_to?(:_helpers)
|
|
284
|
+
|
|
285
|
+
# Use the actual runtime helper chain for this controller.
|
|
286
|
+
controller._helpers.ancestors.filter_map do |mod|
|
|
287
|
+
next unless mod.is_a?(Module) && !mod.is_a?(Class)
|
|
288
|
+
name = mod.name
|
|
289
|
+
next if name.nil? || name.empty?
|
|
290
|
+
next if name.start_with?('ActionController::') || name.start_with?('AbstractController::')
|
|
291
|
+
name
|
|
292
|
+
end.uniq
|
|
293
|
+
end
|
|
192
294
|
end
|
|
193
295
|
end
|
|
194
296
|
end
|
data/lib/sorbet_view/version.rb
CHANGED
|
@@ -26,6 +26,7 @@ module Tapioca
|
|
|
26
26
|
sig { override.void }
|
|
27
27
|
def decorate
|
|
28
28
|
@module_cache = T.let({}, T::Hash[String, RBI::Scope])
|
|
29
|
+
@layout_contributions = T.let({}, T::Hash[String, T::Array[T::Hash[Symbol, T.untyped]]])
|
|
29
30
|
@project = T.let(SrbLens::Project.load_or_index(Dir.pwd), T.untyped)
|
|
30
31
|
|
|
31
32
|
# Ensure all controllers are loaded
|
|
@@ -37,10 +38,17 @@ module Tapioca
|
|
|
37
38
|
false
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
# Deterministic order: ObjectSpace has no order guarantee, and "last controller wins" for
|
|
42
|
+
# shared layout classes made RBI diffs unstable. Sort by constant name; anonymous classes
|
|
43
|
+
# fall back to object_id.
|
|
44
|
+
controllers = controllers.sort_by { |c| controller_sort_key(c) }
|
|
45
|
+
|
|
40
46
|
controllers.each do |controller|
|
|
41
47
|
process_controller(controller)
|
|
42
48
|
end
|
|
43
49
|
|
|
50
|
+
emit_merged_layout_includes
|
|
51
|
+
|
|
44
52
|
generate_ivar_mapping(controllers)
|
|
45
53
|
process_components
|
|
46
54
|
compile_all_templates
|
|
@@ -61,7 +69,7 @@ module Tapioca
|
|
|
61
69
|
controllers.each do |controller|
|
|
62
70
|
path = controller.controller_path
|
|
63
71
|
|
|
64
|
-
controller.action_methods.each do |action_name|
|
|
72
|
+
controller.action_methods.to_a.sort.each do |action_name|
|
|
65
73
|
next unless action_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
66
74
|
|
|
67
75
|
action_ivars = extract_ivars_from_srb_lens(controller, action_name.to_s)
|
|
@@ -120,11 +128,7 @@ module Tapioca
|
|
|
120
128
|
common_keys.each do |key|
|
|
121
129
|
type_a = T.must(a[key])
|
|
122
130
|
type_b = T.must(b[key])
|
|
123
|
-
result[key] =
|
|
124
|
-
type_a
|
|
125
|
-
else
|
|
126
|
-
"T.all(#{type_a}, #{type_b})"
|
|
127
|
-
end
|
|
131
|
+
result[key] = type_a == type_b ? type_a : "T.all(#{type_a}, #{type_b})"
|
|
128
132
|
end
|
|
129
133
|
result
|
|
130
134
|
end
|
|
@@ -213,7 +217,8 @@ module Tapioca
|
|
|
213
217
|
|
|
214
218
|
sig { params(klass: T.untyped, class_name: String).void }
|
|
215
219
|
def generate_component_rbi(klass, class_name)
|
|
216
|
-
|
|
220
|
+
# Instance method order is not guaranteed; sort for stable RBI.
|
|
221
|
+
methods = klass.instance_methods(false).sort
|
|
217
222
|
|
|
218
223
|
method_sigs = methods.filter_map do |method_name|
|
|
219
224
|
method_name_s = method_name.to_s
|
|
@@ -262,7 +267,7 @@ module Tapioca
|
|
|
262
267
|
# Templates compile to format-nested classes (e.g. Users::Show::Html, Users::Show::TurboStream),
|
|
263
268
|
# so emit RBI for each format variant found on disk. Falls back to the action-base class
|
|
264
269
|
# when no format-suffixed templates exist.
|
|
265
|
-
actions = controller.action_methods.to_a
|
|
270
|
+
actions = controller.action_methods.to_a.sort
|
|
266
271
|
actions.each do |action_name|
|
|
267
272
|
next unless action_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
268
273
|
|
|
@@ -272,13 +277,90 @@ module Tapioca
|
|
|
272
277
|
|
|
273
278
|
class_names.each do |class_name|
|
|
274
279
|
create_class_from_path(class_name) do |klass|
|
|
275
|
-
klass
|
|
276
|
-
controller_helper_modules.each do |mod_name|
|
|
277
|
-
klass.create_include("::#{mod_name}")
|
|
278
|
-
end
|
|
280
|
+
apply_includes_for_action!(klass, helper_module_name, controller_helper_modules)
|
|
279
281
|
end
|
|
280
282
|
end
|
|
281
283
|
end
|
|
284
|
+
|
|
285
|
+
layout_name = controller._layout if controller.respond_to?(:_layout)
|
|
286
|
+
if layout_name.is_a?(String) && !layout_name.empty?
|
|
287
|
+
base_class_name = "SorbetView::Generated::Layouts::#{camelize(layout_name)}"
|
|
288
|
+
format_suffixes = find_template_format_suffixes('layouts', layout_name)
|
|
289
|
+
class_names = format_suffixes.empty? ? [base_class_name] : format_suffixes.map { |fs| "#{base_class_name}::#{fs}" }
|
|
290
|
+
|
|
291
|
+
class_names.each do |class_name|
|
|
292
|
+
contribs = T.must(@layout_contributions)[class_name] ||= []
|
|
293
|
+
contribs << {
|
|
294
|
+
controller_name: controller_sort_key(controller),
|
|
295
|
+
helper_module_name: helper_module_name,
|
|
296
|
+
controller_helper_modules: controller_helper_modules
|
|
297
|
+
}
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Stable sort key for controllers (anonymous classes have no #name).
|
|
303
|
+
sig { params(controller: T.untyped).returns(String) }
|
|
304
|
+
def controller_sort_key(controller)
|
|
305
|
+
(controller.name || "AnonymousController:#{controller.object_id}").to_s
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Apply include lines for a single controller's view class. Order: SorbetView::Helpers
|
|
309
|
+
# module first (if any), then controller-specific helper modules in Rails MRO order
|
|
310
|
+
# (see extract_controller_helper_modules).
|
|
311
|
+
sig do
|
|
312
|
+
params(
|
|
313
|
+
klass: T.untyped,
|
|
314
|
+
helper_module_name: T.nilable(String),
|
|
315
|
+
controller_helper_modules: T::Array[String]
|
|
316
|
+
).void
|
|
317
|
+
end
|
|
318
|
+
def apply_includes_for_action!(klass, helper_module_name, controller_helper_modules)
|
|
319
|
+
klass.create_include(helper_module_name) if helper_module_name
|
|
320
|
+
controller_helper_modules.each do |mod_name|
|
|
321
|
+
klass.create_include("::#{mod_name}")
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Layout template classes are shared by every controller that uses the same layout.
|
|
326
|
+
# Record per-controller includes, then merge once with deterministic rules:
|
|
327
|
+
# - contributors sorted by controller_sort_key
|
|
328
|
+
# - within each contributor: helper module first, then MRO order (unchanged)
|
|
329
|
+
# - same constant name only included once: first contribution in the sorted list wins
|
|
330
|
+
# (preserves a single stable precedence for duplicate modules).
|
|
331
|
+
sig { void }
|
|
332
|
+
def emit_merged_layout_includes
|
|
333
|
+
T.must(@layout_contributions).sort_by(&:first).each do |class_name, contribs|
|
|
334
|
+
lines = merge_layout_include_lines(contribs)
|
|
335
|
+
next if lines.empty?
|
|
336
|
+
|
|
337
|
+
create_class_from_path(class_name) do |klass_rbi|
|
|
338
|
+
lines.each { |line| klass_rbi.create_include(line) }
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Returns the exact `create_include` string for each line (unprefixed SorbetView path or ::Foo).
|
|
344
|
+
sig { params(contributions: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Array[String]) }
|
|
345
|
+
def merge_layout_include_lines(contributions)
|
|
346
|
+
seen = T.let({}, T::Hash[String, T::Boolean])
|
|
347
|
+
out = T.let([], T::Array[String])
|
|
348
|
+
|
|
349
|
+
contributions.sort_by { |c| c[:controller_name].to_s }.each do |c|
|
|
350
|
+
h = c[:helper_module_name]
|
|
351
|
+
if h && !seen[h]
|
|
352
|
+
seen[h] = true
|
|
353
|
+
out << h
|
|
354
|
+
end
|
|
355
|
+
c[:controller_helper_modules].to_a.each do |mod_name|
|
|
356
|
+
next if seen[mod_name]
|
|
357
|
+
|
|
358
|
+
seen[mod_name] = true
|
|
359
|
+
out << "::#{mod_name}"
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
out
|
|
282
364
|
end
|
|
283
365
|
|
|
284
366
|
# Scan view dirs for format suffixes of a given action's templates.
|
|
@@ -300,7 +382,7 @@ module Tapioca
|
|
|
300
382
|
end
|
|
301
383
|
end
|
|
302
384
|
|
|
303
|
-
suffixes.uniq
|
|
385
|
+
suffixes.uniq.sort
|
|
304
386
|
end
|
|
305
387
|
|
|
306
388
|
# Returns [[method_name, params, return_type], ...]
|
|
@@ -308,42 +390,29 @@ module Tapioca
|
|
|
308
390
|
def extract_helper_methods(controller)
|
|
309
391
|
return [] unless controller.respond_to?(:_helper_methods)
|
|
310
392
|
|
|
311
|
-
controller._helper_methods.filter_map do |name|
|
|
393
|
+
rows = controller._helper_methods.filter_map do |name|
|
|
312
394
|
name_s = name.to_s
|
|
313
395
|
next unless name_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/)
|
|
314
396
|
|
|
315
397
|
method_info = find_method_info(controller, name_s)
|
|
316
|
-
params =
|
|
317
|
-
build_params_from_srb_lens(method_info)
|
|
318
|
-
else
|
|
319
|
-
build_params_from_reflection(controller, name_s)
|
|
320
|
-
end
|
|
398
|
+
params = method_info ? build_params_from_srb_lens(method_info) : build_params_from_reflection(controller, name_s)
|
|
321
399
|
return_type = method_info&.return_type || 'T.untyped'
|
|
322
400
|
return_type = 'T.untyped' if return_type.empty?
|
|
323
401
|
|
|
324
402
|
[name_s, params, return_type]
|
|
325
403
|
end
|
|
404
|
+
rows.sort_by { |row| T.must(row).first }
|
|
326
405
|
end
|
|
327
406
|
|
|
328
|
-
# Returns
|
|
407
|
+
# Returns runtime helper module names for the controller.
|
|
329
408
|
sig { params(controller: T.untyped).returns(T::Array[String]) }
|
|
330
409
|
def extract_controller_helper_modules(controller)
|
|
331
410
|
return [] unless controller.respond_to?(:_helpers)
|
|
332
411
|
|
|
333
|
-
|
|
334
|
-
::ApplicationController._helpers.ancestors
|
|
335
|
-
else
|
|
336
|
-
[]
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
controller_helpers = controller._helpers.ancestors
|
|
340
|
-
specific_modules = controller_helpers - base_helpers
|
|
341
|
-
|
|
342
|
-
specific_modules.filter_map do |mod|
|
|
412
|
+
controller._helpers.ancestors.filter_map do |mod|
|
|
343
413
|
next unless mod.is_a?(Module) && !mod.is_a?(Class)
|
|
344
414
|
name = mod.name
|
|
345
415
|
next if name.nil? || name.empty?
|
|
346
|
-
next if name.include?('HelperMethods')
|
|
347
416
|
next if name.start_with?('ActionController::') || name.start_with?('AbstractController::')
|
|
348
417
|
name
|
|
349
418
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sorbet_view
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kazuma
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: herb
|