Class: JsxRosetta::Backend::ViewComponent::ExpressionTranslator
- Inherits:
-
Object
- Object
- JsxRosetta::Backend::ViewComponent::ExpressionTranslator
- 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.
* Names in `imported_names` (top-level `import` declarations) are
treated the same as local bindings — `nil` at leaf position, bail
at member-chain root. Without this, `styles.listContainer` from
`import styles from "./X.module.css"` snake-cases to a bare
`styles` reference that NameErrors at render time.
* 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
-
#initialize(prop_names:, local_binding_names: [], prop_aliases: {}, imported_names: []) ⇒ ExpressionTranslator
constructor
prop_aliases maps a local-binding name (the alias) to the underlying prop name.
- #translate(source) ⇒ Object
-
#translate_condition(source) ⇒ Object
Render-condition entry point.
- #with_locals(names) ⇒ Object
Constructor Details
#initialize(prop_names:, local_binding_names: [], prop_aliases: {}, imported_names: []) ⇒ 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.
90 91 92 93 94 95 96 |
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 90 def initialize(prop_names:, local_binding_names: [], prop_aliases: {}, imported_names: []) @prop_names = prop_names.to_set @local_binding_names = local_binding_names.to_set @prop_aliases = prop_aliases.dup @imported_names = imported_names.to_set @local_stack = [] end |
Instance Method Details
#translate(source) ⇒ Object
105 106 107 |
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 105 def translate(source) do_translate(source, condition_mode: false) end |
#translate_condition(source) ⇒ Object
Render-condition entry point. Same recursive translator as ‘translate`, but bucket-4 hits (known-but-unresolvable locals / imports) emit `@snake_case` instead of returning `nil` (member- chain root, unary/binary operand) or `“nil”` (leaf identifier). The promoted names come back via `Result#promoted_locals` so the caller can surface a TODO naming the bindings that need to become controller-passed props.
Only safe here because driving an ‘if` with a known-but-nil value silently disables the branch — destroying the source’s intent. Promoting to an ivar trades silence for a clear render-time error (NameError on @ivar if the user never threads the prop) that the accompanying TODO points the reviewer at.
122 123 124 |
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 122 def translate_condition(source) do_translate(source, condition_mode: true) end |
#with_locals(names) ⇒ Object
98 99 100 101 102 103 |
# File 'lib/jsx_rosetta/backend/view_component/expression_translator.rb', line 98 def with_locals(names) @local_stack.push(names.compact) yield ensure @local_stack.pop end |