Skip to content

feat: Ruby language support for jsii-rosetta - #3710

Draft
omarqureshi wants to merge 11 commits into
aws:mainfrom
omarqureshi:ruby-language-bindings
Draft

feat: Ruby language support for jsii-rosetta#3710
omarqureshi wants to merge 11 commits into
aws:mainfrom
omarqureshi:ruby-language-bindings

Conversation

@omarqureshi

@omarqureshi omarqureshi commented Jul 4, 2026

Copy link
Copy Markdown

Closes #3711.

Adds Ruby language support to jsii-rosetta — a new Ruby AST visitor (src/languages/ruby.ts) that translates TypeScript example code to idiomatic Ruby, registered as the RUBY target.

Part of the coordinated Ruby language support effort:

What's included

  • A Ruby visitor covering: imports → require/require_relative, classes / behavioral interfaces / enums, methods (including def self. for statics) and constructors → initialize, properties → attr_reader/attr_accessor and class constants, object literals → hashes, new X()X.new, property access with snake_case conversion, string & template-literal rendering with proper escaping, if/elsif/else, for..of.each, spreads → splats, ternaries, and comment/YARD rendering.
  • Translation fixtures and focused regression tests (test/ruby-translations.test.ts).

Notes

  • Rebased onto current main; earlier TypeScript 6.0 upgrade commits are dropped as redundant (main is already on TS 6.0 / jsii 6.0, using types: ['*']).
  • A self-review surfaced and fixed several correctness bugs before submission — if/elsif/else chains emitting a doubled end, Ruby string-interpolation escaping (#{}), static method/constant rendering, as-cast pass-through (no spurious .to_i/.to_s), and super() argument semantics — all covered by the new tests.

The visitor is self-contained (no dependency on the Ruby-capable compiler), so the test suite passes on its own.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Adds a Ruby language visitor (src/languages/ruby.ts) that translates TypeScript example code to idiomatic Ruby, registered as the RUBY target, plus translation test fixtures.
omarqureshi and others added 4 commits July 15, 2026 10:31
… the member

Static readonly (const) access like BlockPublicAccess.BLOCK_ALL fell into the
type-reference branch, which resolved the jsii symbol to the containing type and
emitted just AWSCDK::S3::BlockPublicAccess, dropping .BLOCK_ALL. Detect it with
isStaticReadonlyAccess and render <type>.<CONST_NAME> (dot access, constant
casing) to match pacmak's rubyConstName. Enum members keep their :: form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`import * as s3tables from 'aws-cdk-lib/aws-s3tables'` translated to
`require 'aws-cdk-lib-aws-s3tables'` (the whole specifier had `/` -> `-`),
which is not a real gem. A submodule is autoloaded from its package, so the
require is just the gem: `require 'aws-cdk-lib'`. Keep only the npm package
name (two segments for a scoped `@scope/name`, one otherwise).

Adds import->require translation tests covering plain, submodule, scoped,
scoped-submodule and relative specifiers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The array and hash renderers mirrored the source's line breaks faithfully,
which produced inconsistent output: elements sharing a line while `]` dropped
to its own line, and a hash property stranded on the closing line of a
preceding multi-line value (`InputFormat.csv({...}), bucket: bucket`).

Normalise instead: if the literal spans multiple lines (its source text
contains a newline), put every element/property on its own line via a
trailing separator, with the closing bracket on its own line; otherwise keep
it inline. Adds array/hash formatting tests (broken + inline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An uninitialised declaration (`declare const bucket: s3.IBucket`) is a "given"
the reader supplies. It rendered as a bare `bucket = nil`, which drops the type
and reads like a mistake. Keep the type as a trailing comment resolved to its
Ruby name: `bucket = nil # AWSCDK::S3::IBucket`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@omarqureshi

Copy link
Copy Markdown
Author

I have published https://rubygems.omarqureshi.net/docs/AWSCDK/index.html. As a note, the following applies:

/index.html is a combination of content that I have written as well as reading the assembly to figure out classes/interfaces/enums as well as CDK modules.

Each modules landing page is the README documentation from aws-cdk with code examples run through rosetta.

Each class/interface/enum is run through rosetta and the documentation generated by YARD

…guages

Adding typed dispatch for ConditionalExpression/PostfixUnaryExpression gave
DefaultVisitor handlers that always rendered an UnknownSyntax placeholder —
but the dispatch default case they replaced reported the node as unsupported
and then, in best-effort mode, passed the raw source text through. Python,
Java, C# and Go doc snippets containing ternaries or ++/-- therefore
regressed from readable raw TypeScript to '(ConditionalExpression ...)'
placeholders.

Extract the old default-case fallback into AstRenderer.renderUnsupported and
call it from both the dispatch default case and the two DefaultVisitor
handlers, so the fallback contract (diagnostic + raw text passthrough when
bestEffort, placeholder otherwise) cannot drift between the two paths again.
Ruby's overrides are untouched and still translate both constructs.

Verified against a build of main: Python output is byte-identical to the
pre-branch behavior in both best-effort and strict modes. Full suite green
apart from 18 pre-existing translations.test.ts failures reproduced
identically on the pre-fix branch head.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
omarqureshi added a commit to omarqureshi/aws-cdk-rfcs that referenced this pull request Jul 22, 2026
- The monorepo-CI claim now states explicitly that green requires pinning a
  pre-release build of jsii-compiler#2663, and that the checks on jsii#5178
  stay red by construction until #2663 merges and releases — an ordering
  constraint, not an implementation gap.
- New "Upstreaming sequence" subsection ordering the open PRs (#2663 ->
  #5178 -> #3710/docgen -> #38248) so a reviewer cannot misread the red CI.
- Phase 0 registration list now includes `jsii`, matching the appendix
  governance list; name availability re-verified 2026-07-22 (all still
  unregistered).
- Visitor description made durable (40+ AST handlers, no line counts); the
  rosetta narrative now points at the already-open aws/jsii-rosetta#3710.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
omarqureshi and others added 5 commits July 22, 2026 19:26
The .rb expectation files were written in the initial Ruby-target commit
and never regenerated when three later visitor fixes deliberately changed
output: multi-line array/hash normalisation (09c7429 — trailing separators,
one element per line) and static-readonly member access (8ca8d19 —
EnumType.ENUM_VALUE_A, matching pacmak, which emits consts as class
methods: `def self.ENUM_VALUE_A()`; bare `::` would be a NameError since
no Ruby constant exists). The suite therefore failed 18 fixtures against
its own intended behavior — main is green, so these were branch-internal
inconsistencies, not upstream drift.

Regenerated through the exact harness path (SnippetTranslator with the
fixtures compilation directory). Full suite: 701 passed, 0 failed across
29 suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Acronyms are library data: aws-cdk-lib declares its own in
targets.ruby.acronyms (compiler-validated schema, embedded in every .jsii),
and findRubyName already reads them from the referenced assembly. The
hardcoded CDK_ACRONYMS list existed only so fixtures and unit tests could
render CDK-ish casing without assemblies — test scaffolding leaked into
production code, and a CDK word list inside jsii tooling is a layering
violation regardless.

Delete the list. rubyModuleName now honours exactly the caller-declared
acronyms (assembly config is authoritative; nothing merges over it), and a
snippet whose type references cannot be resolved to an assembly gets plain
PascalCase — an honest guess instead of fake authority (Cdk::SomeClass,
not CDK::SomeClass). The one remaining CDK-ism (guessRubyModuleName's
aws-cdk-lib -> AWSCDK mapping for unresolvable refs) is documented in-code
with its structural fix: callers hold the assembly and should eventually
supply naming config through the API.

Unit tests now exercise the mechanism with test-owned data (declared
acronyms honoured, undeclared ones inert, short tokens like CE/DB/RAM do
not over-match inside Certificate/Database/Ramp); the six affected
translation fixtures are regenerated. Full suite: 702 passed, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pace fixture

class_with_namespace.ts was the only fixture with no expected output in
any language — silently skipped everywhere. Translating it crashed in
submodule-reference resolution ("Cannot read properties") on a symbol
with no declarations; the crash reproduces against unmodified upstream
code, so the guard is an upstream-relevant fix in its own right.

With the guard in place the Ruby translation is correct (verified against
a fresh harness render, byte-identical), so the fixture now pins Ruby
behavior for aliased namespace imports; other languages remain skipped,
matching the suite's partial-coverage pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A snippet importing two submodules of the same package emitted the same
require twice; jsii-pacmak papered over it in README post-processing, but
raw tablet consumers still saw duplicates (submodule-import.rb codified
them). The visitor now tracks emitted require targets per instance —
translate.ts constructs a fresh visitor per snippet render, so state
cannot leak across snippets (verified in source and by probe; the
one-visitor-per-conversion path in commands/convert.ts dedupes across a
whole markdown document, which is the desirable behavior there too).

Different gems still each emit their require; the regenerated fixture
pins the single-require form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Arrow functions had no typed dispatch anywhere — every language passed
them through as raw TypeScript via the renderer's default case, which is
how `() => arnLookup` leaked into otherwise-Ruby doc snippets (Lazy
producers, add_validation callbacks and friends).

Add ArrowFunction/FunctionExpression dispatch with the established
contract: the DefaultVisitor delegates to renderUnsupported, so every
other language keeps byte-identical raw-text passthrough by construction;
the Ruby visitor renders lambdas — `(bell) => bell.ring()` becomes
`->(bell) { bell.ring }`, block bodies become multi-line lambdas, and
parameters are snake_cased. Destructuring/default/rest parameters fall
back to the shared unsupported path with a diagnostic.

The rendered output is not a visual approximation: the runtime coerces
Procs into single-method interface implementations at jsii call sites
(jsii repo, Jsii::Utils.coerce_callable), so a translated
`Lazy.string({produce: -> { arn_lookup }})` is a working program.

Suite: 710 passed, 0 failed across 30 suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Ruby language support for jsii-rosetta

1 participant