Class: JsxRosetta::IR::Lowering

Inherits:
Object
  • Object
show all
Defined in:
lib/jsx_rosetta/ir/lowering.rb

Overview

Lowers a parsed AST::File into an IR::Component tree.

Phase 2 scope:

- Single function-declaration component per file.
- JSX elements with lowercase tags lower to IR::Element; others to
  IR::ComponentInvocation.
- className attributes lower to IR::StyleBinding; everything else
  to IR::Attribute (event handlers like onClick are passed through
  as Attribute for now and will be re-lowered to EventBinding in
  a later phase).
- JS expressions are preserved as opaque source text via
  IR::Interpolation. No JS-to-Ruby translation.
- Pure-whitespace JSXText between elements is dropped (matches
  JSX runtime behavior); other text is preserved verbatim.

Phase 4a additions:

- {children} where `children` is a prop lowers to IR::Slot.
- {cond && X}, {cond ? X : null}, and {cond ? X : Y} lower to
  IR::Conditional. Other LogicalExpression operators (||, ??) are
  left as opaque interpolations.

Defined Under Namespace

Classes: LoweringError

Constant Summary collapse

REACT_HOOKS =
%w[
  useState useEffect useRef useContext useMemo useCallback
  useReducer useImperativeHandle useLayoutEffect useDebugValue
].freeze
EXPORT_TYPES =
%w[ExportNamedDeclaration ExportDefaultDeclaration].freeze
JSX_NODE_TYPES =
%w[JSXElement JSXFragment JSXText JSXExpressionContainer].freeze
HOC_NAMES =
%w[memo forwardRef lazy observer].freeze
JSX_RETURN_PROBES =

Pre-lowering AST scan: maps a node type to a callable returning the AST nodes that contribute return values. Used by body_returns_jsx?.

{
  "ReturnStatement" => ->(n) { [n[:argument]] },
  "BlockStatement" => ->(n) { n[:body] },
  "IfStatement" => ->(n) { [n[:consequent], n[:alternate]] },
  "TryStatement" => ->(n) { [n[:block]] },
  "ConditionalExpression" => ->(n) { [n[:consequent], n[:alternate]] },
  "LogicalExpression" => ->(n) { [n[:left], n[:right]] }
}.freeze
SHAPE_MESSAGES =
{
  hoc_wrapped: "looks like a HOC-wrapped component (React.memo / forwardRef / lazy / observer) — " \
               "this version doesn't peel HOC wrappers; remove the wrapper or upgrade when supported",
  class_component: "looks like a class component — this version translates only function components " \
                   "(rewrite as a function or wait for class-component support)",
  hooks_only: "looks like a custom-hooks module — hooks encode behavior and state, not view markup; " \
              "translate behavior to a Stimulus controller and state to server-rendered ivars",
  columns_data: "looks like a data export (top-level array literal) — not a component; " \
                "data lives in the model or a presenter, not a ViewComponent",
  types_only: "looks like a types/constants module — no functions to translate; " \
              "TypeScript types erase, and Ruby constants belong elsewhere",
  side_effects_only: "looks like a side-effect-only module (top-level calls, no exported functions) — " \
                     "register the equivalent setup in a Rails initializer instead",
  utils_only: "looks like a utility module — only function components and JSX-returning helpers translate; " \
              "pure-data helpers don't have a ViewComponent equivalent",
  mixed_exports: "module mixes shapes (utilities + hooks + types + non-JSX helpers) — " \
                 "split into separate files so each module has a single shape",
  unknown: nil
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source) ⇒ Lowering

Returns a new instance of Lowering.



104
105
106
107
108
109
110
111
112
113
114
# File 'lib/jsx_rosetta/ir/lowering.rb', line 104

def initialize(source)
  @source = source
  @prop_names = []
  @local_jsx = {}
  @local_bindings = []
  @local_arrows = {}
  @local_polymorphic_tags = {}
  @stimulus_methods = []
  @stimulus_seen_names = {}
  @react_hooks = []
end

Class Method Details

.lower(file, source:) ⇒ Object



56
57
58
# File 'lib/jsx_rosetta/ir/lowering.rb', line 56

def self.lower(file, source:)
  new(source).lower_file(file)
end

.lower_all(file, source:) ⇒ Object



60
61
62
# File 'lib/jsx_rosetta/ir/lowering.rb', line 60

def self.lower_all(file, source:)
  new(source).lower_all_components(file)
end

Instance Method Details

#lower_all_components(file) ⇒ Object



124
125
126
127
128
129
# File 'lib/jsx_rosetta/ir/lowering.rb', line 124

def lower_all_components(file)
  candidates = find_component_functions(file.program)
  raise no_component_error(file.program) if candidates.empty?

  candidates.map { |name, function| lower_component(name, function) }
end

#lower_file(file) ⇒ Object



116
117
118
119
120
121
122
# File 'lib/jsx_rosetta/ir/lowering.rb', line 116

def lower_file(file)
  candidates = find_component_functions(file.program)
  raise no_component_error(file.program) if candidates.empty?

  name, function = candidates.first
  lower_component(name, function)
end