Class: SorbetView::Compiler::RubyGenerator

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/sorbet_view/compiler/ruby_generator.rb

Constant Summary collapse

LOCALS_PREFIX =
'locals:'
LOCALS_SIG_PREFIX =
'locals_sig:'

Instance Method Summary collapse

Instance Method Details

#_generate(segments:, context:, config:, component_mode: false) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/sorbet_view/compiler/ruby_generator.rb', line 41

def _generate(segments:, context:, config:, component_mode: false)
  locals = T.let(nil, T.nilable(String))
  locals_sig = T.let(nil, T.nilable(String))
  code_segments = T.let([], T::Array[RubySegment])

  # Extract locals declarations from comments, collect code segments
  segments.each do |seg|
    if seg.type == :comment
      text = seg.code.strip
      if text.start_with?(LOCALS_PREFIX)
        locals = text.delete_prefix(LOCALS_PREFIX).strip
      elsif text.start_with?(LOCALS_SIG_PREFIX)
        locals_sig = text.delete_prefix(LOCALS_SIG_PREFIX).strip
      end
    else
      code_segments << seg
    end
  end

  lines = T.let([], T::Array[String])
  mapping_entries = T.let([], T::Array[SourceMap::MappingEntry])

  # Boilerplate header
  lines << "# typed: #{config.typed_level}"
  lines << '# generated by sorbet_view - do not edit'
  lines << "# source: #{context.template_path}"
  lines << ''
  lines << "class #{context.class_name}#{context.superclass_clause}"

  unless component_mode
    lines << '  extend T::Sig'
    context.includes.each { |inc| lines << "  include #{inc}" }
    lines << ''

    if config.extra_body.length > 0
      config.extra_body.each_line { |l| lines << "  #{l.chomp}" }
      lines << ''
    end

    lines << '  sig { returns(T::Hash[Symbol, T.untyped]) }'
    lines << '  def local_assigns = {}'
    lines << ''

    if locals_sig
      lines << "  #{locals_sig}"
    end
  end

  # Declare instance variables in initialize (avoids Sorbet 5005)
  resolved_ivars = resolve_ivar_types(code_segments, context, config, component_mode)
  unless resolved_ivars.empty? || component_mode
    lines << '  def initialize'
    resolved_ivars.each do |ivar, type|
      if type == 'NilClass'
        lines << "    #{ivar} = T.let(nil, NilClass)"
      else
        lines << "    #{ivar} = T.let(T.unsafe(nil), #{type})"
      end
    end
    lines << '  end'
    lines << ''
  end

  method_args = component_mode ? '' : (locals || '()')
  lines << "  def __sorbet_view_render#{method_args}"

  # Body: extracted Ruby code
  code_segments.each do |seg|
    seg_lines = seg.code.split("\n", -1)
    seg_lines = [seg.code] if seg_lines.empty?

    seg_lines.each_with_index do |code_line, i|
      ruby_line = lines.length
      stripped = code_line.strip

      next if stripped.empty? && i > 0 # skip trailing empty from split

      lines << "    #{stripped}"

      # Build mapping entry — for the first line we already advanced past
      # leading whitespace in the adapter; for subsequent lines, use the
      # actual indent of the source line so the template column matches.
      template_start_col = if i == 0
        seg.column
      else
        code_line.length - code_line.lstrip.length
      end

      mapping_entries << SourceMap::MappingEntry.new(
        template_range: SourceMap::Range.new(
          start: SourceMap::Position.new(line: seg.line + i, column: template_start_col),
          end_: SourceMap::Position.new(line: seg.line + i, column: template_start_col + stripped.length)
        ),
        ruby_range: SourceMap::Range.new(
          start: SourceMap::Position.new(line: ruby_line, column: 4),
          end_: SourceMap::Position.new(line: ruby_line, column: 4 + stripped.length)
        ),
        type: seg.type == :expression ? :expression : :code
      )
    end
  end

  lines << '  end'
  lines << 'end'
  lines << '' # trailing newline

  CompileResult.new(
    ruby_source: lines.join("\n"),
    source_map: SourceMap::SourceMap.new(
      template_path: context.template_path,
      ruby_path: context.ruby_path,
      entries: mapping_entries
    ),
    locals: locals,
    locals_sig: locals_sig
  )
end

#generate(segments:, context:, config:, component_mode: false) ⇒ Object



29
30
31
# File 'lib/sorbet_view/compiler/ruby_generator.rb', line 29

def generate(segments:, context:, config:, component_mode: false)
  return Perf.measure('generator.generate') { _generate(segments: segments, context: context, config: config, component_mode: component_mode) }
end

#invalidate_ivar_cache!Object



160
161
162
# File 'lib/sorbet_view/compiler/ruby_generator.rb', line 160

def invalidate_ivar_cache!
  @ivar_mapping = nil
end