Class: JsxRosetta::Backend::ViewComponent::ExpressionTranslator

Inherits:
Object
  • Object
show all
Defined in:
lib/jsx_rosetta/backend/view_component/expression_translator.rb

Overview

Best-effort, narrowly-scoped JS-to-Ruby translation for the simple expression shapes that JSX components in real codebases use most often: bare identifiers, literals, simple member-expression chains (‘item.label`), and template literals composed of identifier interpolations. Anything more complex (function calls, conditionals, subscripts) returns `nil` from `#translate` so the backend can emit a TODO marker and fall back to the verbatim JS source.

Identifier resolution:

* Names in the active local scope (e.g. loop bindings) translate
  to the bare snake_case identifier.
* Names in `prop_names` translate to a `@snake_case` instance
  variable.
* Names in `local_binding_names` (consts/destructures captured at
  lowering time but not modeled in IR) translate to a `nil`
  placeholder with an inline `# TODO: local 'name'` marker — the
  file still loads, but the reviewer sees what to fill in.
* Anything else translates to the bare snake_case identifier and
  is recorded as unresolved.

Local scopes can be pushed via ‘with_locals` and stack — each entry shadows lower entries.

Defined Under Namespace

Classes: Result

Constant Summary collapse

IDENTIFIER =
/\A[a-zA-Z_$][a-zA-Z_$0-9]*\z/
STRING_LITERAL =

Tighter than ‘A()(.*)1z/m` — the greedy ‘.*` previously matched expressions like `”X“ ? ”Y“ : ”Z“` as a single quoted string. Now the body excludes unescaped quotes of the same kind, so only true string literals match.

/\A(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\z/m
NUMBER_LITERAL =
/\A-?\d+(\.\d+)?\z/
TEMPLATE_LITERAL =
/\A`(.*)`\z/m
TEMPLATE_INTERPOLATION =
/\$\{([a-zA-Z_$][a-zA-Z_$0-9]*(?:\??\.[a-zA-Z_$][a-zA-Z_$0-9]*)*)\}/
MEMBER_CHAIN =
/\A(?<root>[a-zA-Z_$][a-zA-Z_$0-9]*)(?<rest>(?:\??\.[a-zA-Z_$][a-zA-Z_$0-9]*)+)\z/
MEMBER_SEGMENT =
/(\??\.)([a-zA-Z_$][a-zA-Z_$0-9]*)/
THIS_PROPS_CHAIN =

Class-component access patterns. ‘this.props.X` is the same shape as a function-component prop reference, so it maps to `@x` plus any trailing member chain. `this.state.X` has no Ruby analog (state without a backing data source) so we surface a `nil` placeholder with a TODO marker — the file still loads.

/\Athis\.props\.(?<rest>[a-zA-Z_$][a-zA-Z_$0-9]*(?:\??\.[a-zA-Z_$][a-zA-Z_$0-9]*)*)\z/
THIS_STATE_CHAIN =
/\Athis\.state\.(?<rest>[a-zA-Z_$][a-zA-Z_$0-9]*(?:\??\.[a-zA-Z_$][a-zA-Z_$0-9]*)*)\z/
UNARY =
/\A(?<op>!+|-|\+)(?<operand>.+)\z/m
SIMPLE_LITERALS =
{ "null" => "nil", "undefined" => "nil", "true" => "true", "false" => "false" }.freeze
BINARY_PRECEDENCE =

Binary operators we translate, grouped by precedence (lowest first). We split on the lowest-precedence top-level operator and recurse on each side, mirroring how a recursive-descent parser would treat the source: ‘a > 0 && b < 5` splits on `&&` first, then each side splits on its relational operator.

Arithmetic operators (‘+`, `-`, `*`, `/`, `%`) aren’t included —‘-x` and `+x` are unary at the start of an operand, and string- scanning can’t disambiguate without operator-state tracking that mirrors a parser. Real JSX conditions rarely need arithmetic in tests; comparison + logical covers the bulk of them.

[
  %w[|| ??],
  %w[&&],
  %w[=== !== == !=],
  %w[<= >= < >]
].freeze
QUOTE_CHARS =
['"', "'", "`"].freeze
OPEN_BRACKETS =
["(", "[", "{"].freeze
CLOSE_BRACKETS =
[")", "]", "}"].freeze

Instance Method Summary collapse

Constructor Details

#initialize(prop_names:, local_binding_names: [], prop_aliases: {}) ⇒ ExpressionTranslator

prop_aliases maps a local-binding name (the alias) to the underlying prop name. ‘“data-testid”: dataTestId` records `{ “dataTestId” => “data-testid” }` so the use site of `dataTestId` resolves to the prop’s ‘@data_testid` ivar.



80
81
82
83
84
85
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 80

def initialize(prop_names:, local_binding_names: [], prop_aliases: {})
  @prop_names = prop_names.to_set
  @local_binding_names = local_binding_names.to_set
  @prop_aliases = prop_aliases.dup
  @local_stack = []
end

Instance Method Details

#translate(source) ⇒ Object



94
95
96
97
98
99
100
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 94

def translate(source)
  source = source.strip
  unresolved = []

  ruby = translate_ruby(source, unresolved)
  ruby && Result.new(ruby: ruby, unresolved_identifiers: unresolved.uniq)
end

#with_locals(names) ⇒ Object



87
88
89
90
91
92
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 87

def with_locals(names)
  @local_stack.push(names.compact)
  yield
ensure
  @local_stack.pop
end