sorbet_view 0.16.1 → 0.17.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90d970f9f128a630593f84d69ad5f1882b382754dbe1066a86881e73b8d02b1e
4
- data.tar.gz: efd75504c02a0dcf59621011cf978e648de00157bf317bed2e52565d9d8fb56d
3
+ metadata.gz: 241f449c7d85bd172dd1f14e69f2dd3c3baf9a2590d932a81437bcb7256697e9
4
+ data.tar.gz: 486cc137199d7a8e0fbda04baf528f139abd43585870108305ca9ece45f99a6e
5
5
  SHA512:
6
- metadata.gz: 1053ce748716793362dd0b66296a5710a270fb97e48ef81fc944b3e737078603bcdcc145f6d8f90ea2298df33e389847239505c8d427edf49fb89d4ec01d5b41
7
- data.tar.gz: e71fab2eff5ffda2a5acb7e80b479882e3309b82b5bccb5c1e153d08896e2c1bdcd09f9c03800c9249be4fc5b9b558400453a0c69048b71543ce1eee5c2f075e
6
+ metadata.gz: 78b9b224b0c202d58ab0f707f9333d8972298b11b707e735efd8f8da58eb93b21c24061f5a2b6fc0ea2fb207e2840df83351b6759a4ae8372d84391a390813c7
7
+ data.tar.gz: faf509cd68a7f7eb35f2e826a3bdc514681afad57e8900ce079269b58ec4d5321d5cba66e1aee7199f8eeb59eec0a58cf97bcc0440e142e351a0ea846f618a22
@@ -97,13 +97,14 @@ module SorbetView
97
97
  return unless node.respond_to?(:content) && node.content
98
98
 
99
99
  content_token = node.content
100
- code = content_token.value.to_s.strip
100
+ raw_value = content_token.value.to_s
101
+ code = raw_value.strip
101
102
  return if code.empty?
102
103
 
103
- # Herb lines are 1-based, we use 0-based
104
+ # Herb lines are 1-based, we use 0-based; raw content begins right after
105
+ # the opening tag, so advance past leading whitespace stripped from `code`.
104
106
  loc = content_token.location
105
- line = loc.start.line - 1
106
- column = loc.start.column
107
+ line, column = advance_past_leading_whitespace(raw_value, loc.start.line - 1, loc.start.column)
107
108
 
108
109
  tag = node.respond_to?(:tag_opening) ? node.tag_opening.value.to_s : '<%'
109
110
  type = case tag
@@ -120,10 +121,12 @@ module SorbetView
120
121
  return unless node.respond_to?(:content) && node.content
121
122
 
122
123
  loc = node.content.location
124
+ raw_value = node.content.value.to_s
125
+ line, column = advance_past_leading_whitespace(raw_value, loc.start.line - 1, loc.start.column)
123
126
  segments << RubySegment.new(
124
127
  code: 'end',
125
- line: loc.start.line - 1,
126
- column: loc.start.column,
128
+ line: line,
129
+ column: column,
127
130
  type: :statement
128
131
  )
129
132
  end
@@ -137,14 +140,16 @@ module SorbetView
137
140
  source.scan(INDICATOR_PATTERN) do
138
141
  match = T.must(Regexp.last_match)
139
142
  indicator = match[1] || ''
140
- code = (match[2] || '').strip
143
+ raw_code = match[2] || ''
144
+ code = raw_code.strip
141
145
  offset = T.must(match.begin(0))
142
146
 
143
147
  next if code.empty?
144
148
 
145
149
  line, column = offset_to_line_column(line_offsets, offset)
146
150
  tag_prefix_len = 2 + indicator.length
147
- code_column = column + tag_prefix_len
151
+ content_column = column + tag_prefix_len
152
+ code_line, code_column = advance_past_leading_whitespace(raw_code, line, content_column)
148
153
 
149
154
  type = case indicator
150
155
  when '#' then :comment
@@ -152,7 +157,7 @@ module SorbetView
152
157
  else :statement
153
158
  end
154
159
 
155
- segments << RubySegment.new(code: code, line: line, column: code_column, type: type)
160
+ segments << RubySegment.new(code: code, line: code_line, column: code_column, type: type)
156
161
  end
157
162
 
158
163
  segments
@@ -173,6 +178,23 @@ module SorbetView
173
178
  column = offset - (line_offsets[line] || 0)
174
179
  [line, column]
175
180
  end
181
+
182
+ # Returns the (line, column) of the first non-whitespace char in raw_value,
183
+ # given the position where raw_value begins. Used so that stripping leading
184
+ # whitespace from ERB content does not desync the segment's start position.
185
+ sig { params(raw_value: String, line: Integer, column: Integer).returns([Integer, Integer]) }
186
+ def advance_past_leading_whitespace(raw_value, line, column)
187
+ leading = raw_value[/\A\s*/] || ''
188
+ return [line, column] if leading.empty?
189
+
190
+ newlines = leading.count("\n")
191
+ if newlines == 0
192
+ [line, column + leading.length]
193
+ else
194
+ last_nl = T.must(leading.rindex("\n"))
195
+ [line + newlines, leading.length - last_nl - 1]
196
+ end
197
+ end
176
198
  end
177
199
  end
178
200
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module SorbetView
5
- VERSION = '0.16.1'
5
+ VERSION = '0.17.1'
6
6
  end
@@ -28,6 +28,9 @@ module Tapioca
28
28
  @module_cache = T.let({}, T::Hash[String, RBI::Scope])
29
29
  @project = T.let(SrbLens::Project.load_or_index(Dir.pwd), T.untyped)
30
30
 
31
+ # Ensure all controllers are loaded
32
+ Rails.application.eager_load! if defined?(Rails) && Rails.respond_to?(:application)
33
+
31
34
  controllers = ObjectSpace.each_object(Class).select do |klass|
32
35
  klass < ::ActionController::Base && !klass.abstract?
33
36
  rescue StandardError
@@ -238,27 +241,34 @@ module Tapioca
238
241
  sig { params(controller: T.untyped).void }
239
242
  def process_controller(controller)
240
243
  helper_methods = extract_helper_methods(controller)
241
- return if helper_methods.empty?
244
+ controller_helper_modules = extract_controller_helper_modules(controller)
245
+ return if helper_methods.empty? && controller_helper_modules.empty?
242
246
 
243
247
  path = controller.controller_path
244
248
  parts = path.split('/').map { |p| camelize(p) }
245
249
 
246
- # 1) module SorbetView::Helpers::<controller_path> with helper methods
247
- helper_module_name = "SorbetView::Helpers::#{parts.join('::')}"
248
- create_module_from_path(helper_module_name) do |mod|
249
- helper_methods.each do |method_name, params, return_type|
250
- mod.create_method(method_name, parameters: params, return_type: return_type)
250
+ # 1) module SorbetView::Helpers::<controller_path> with helper methods from helper_method macro
251
+ helper_module_name = nil
252
+ unless helper_methods.empty?
253
+ helper_module_name = "SorbetView::Helpers::#{parts.join('::')}"
254
+ create_module_from_path(helper_module_name) do |mod|
255
+ helper_methods.each do |method_name, params, return_type|
256
+ mod.create_method(method_name, parameters: params, return_type: return_type)
257
+ end
251
258
  end
252
259
  end
253
260
 
254
- # 2) Each action's template class includes the helper module
261
+ # 2) Each action's template class includes the helper module and controller-specific helper modules
255
262
  actions = controller.action_methods.to_a
256
263
  actions.each do |action_name|
257
264
  next unless action_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
258
265
 
259
266
  class_name = "SorbetView::Generated::#{parts.join('::')}::#{camelize(action_name)}"
260
267
  create_class_from_path(class_name) do |klass|
261
- klass.create_include(helper_module_name)
268
+ klass.create_include(helper_module_name) if helper_module_name
269
+ controller_helper_modules.each do |mod_name|
270
+ klass.create_include("::#{mod_name}")
271
+ end
262
272
  end
263
273
  end
264
274
  end
@@ -285,6 +295,30 @@ module Tapioca
285
295
  end
286
296
  end
287
297
 
298
+ # Returns controller-specific helper module names (modules included via `helper` that are not in ApplicationController)
299
+ sig { params(controller: T.untyped).returns(T::Array[String]) }
300
+ def extract_controller_helper_modules(controller)
301
+ return [] unless controller.respond_to?(:_helpers)
302
+
303
+ base_helpers = if defined?(::ApplicationController) && ::ApplicationController.respond_to?(:_helpers)
304
+ ::ApplicationController._helpers.ancestors
305
+ else
306
+ []
307
+ end
308
+
309
+ controller_helpers = controller._helpers.ancestors
310
+ specific_modules = controller_helpers - base_helpers
311
+
312
+ specific_modules.filter_map do |mod|
313
+ next unless mod.is_a?(Module) && !mod.is_a?(Class)
314
+ name = mod.name
315
+ next if name.nil? || name.empty?
316
+ next if name.include?('HelperMethods')
317
+ next if name.start_with?('ActionController::') || name.start_with?('AbstractController::')
318
+ name
319
+ end
320
+ end
321
+
288
322
  sig { params(controller: T.untyped, method_name: String).returns(T.untyped) }
289
323
  def find_method_info(controller, method_name)
290
324
  methods = @project.find_methods("#{controller.name}##{method_name}")
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.16.1
4
+ version: 0.17.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kazuma
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-02 00:00:00.000000000 Z
10
+ date: 2026-04-22 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: herb
@@ -118,7 +118,6 @@ files:
118
118
  - lib/sorbet_view/source_map/source_map.rb
119
119
  - lib/sorbet_view/version.rb
120
120
  - lib/tapioca/dsl/compilers/sorbet_view.rb
121
- - sorbet_view.gemspec
122
121
  - vscode/.gitignore
123
122
  - vscode/.vscode/launch.json
124
123
  - vscode/.vscodeignore
data/sorbet_view.gemspec DELETED
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lib/sorbet_view/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'sorbet_view'
7
- spec.version = SorbetView::VERSION
8
- spec.authors = ['kazuma']
9
- spec.summary = 'Sorbet type-checking for Rails view templates'
10
- spec.description = 'Extracts Ruby code from view templates (ERB, etc.) for Sorbet type-checking, with LSP support'
11
- spec.homepage = 'https://github.com/kazzix14/sorbet_view'
12
- spec.license = 'MIT'
13
- spec.required_ruby_version = '>= 3.2'
14
-
15
- spec.metadata['rubygems_mfa_required'] = 'true'
16
- spec.metadata['homepage_uri'] = spec.homepage
17
- spec.metadata['source_code_uri'] = spec.homepage
18
-
19
- spec.files = Dir.chdir(__dir__) do
20
- `git ls-files -z`.split("\x0").reject do |f|
21
- (File.expand_path(f) == __FILE__) ||
22
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github Gemfile])
23
- end
24
- end
25
- spec.bindir = 'exe'
26
- spec.executables = ['sv']
27
- spec.require_paths = ['lib']
28
-
29
- spec.add_dependency 'herb'
30
- spec.add_dependency 'listen', '~> 3.0'
31
- spec.add_dependency 'psych'
32
- spec.add_dependency 'sorbet-runtime'
33
- spec.add_dependency 'srb_lens', '~> 0.5.1'
34
- end