Skip to content

Commit a4e5db6

Browse files
Omar Qureshiclaude
andcommitted
fix(ruby): distinguish parameterless and private constructors in codegen
The initializer fallback emitted a single catch-all `initialize(*args)` passthrough for two very different cases: - Parameterless constructors (initializer entry with no parameters): stray arguments were silently forwarded to the kernel. Now emitted with exact zero arity, matching how parameterized signatures are enforced. - No initializer entry at all: jsii emits this for classes whose constructor is private — instances only come from factory methods, hydrated via allocate (which never calls #initialize). The passthrough also silently skipped any base-class coercion/checks, relying on the unstated invariant that the jsii compiler propagates initializer entries onto instantiable subclasses (verified: all 27 such classes in jsii-calc have private ctors). Constructing one from Ruby is now an eager NoMethodError naming the class and pointing at the factories, instead of a less helpful kernel rejection. Verified no spec instantiates any of the 27 private-ctor fixture classes. Tests pin zero-arity enforcement, the raising stub, and the unaffected factory path. Full suite: 240 examples, 0 failures; compliance report unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fcf8a59 commit a4e5db6

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

packages/@jsii/ruby-runtime-test/spec/unit/type_validation_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,24 @@
107107
expect { JsiiCalc::Calculator.new('not props') }
108108
.to raise_error(TypeError, /Expected props to be of type/)
109109
end
110+
111+
it 'enforces zero arity for parameterless constructors' do
112+
# Previously a catch-all (*args) silently forwarded stray arguments
113+
# to the kernel.
114+
expect { JsiiCalc::AllTypes.new(123) }.to raise_error(ArgumentError)
115+
expect { JsiiCalc::AllTypes.new }.not_to raise_error
116+
end
117+
118+
it 'raises a helpful error when constructing a private-constructor class' do
119+
# Classes without an initializer entry in the assembly have private
120+
# constructors: instances only come from factories (hydrated via
121+
# allocate, which never calls #initialize).
122+
expect { JsiiCalc::ClassWithPrivateConstructorAndAutomaticProperties.new('a', 'b') }
123+
.to raise_error(NoMethodError, /does not have a visible constructor/)
124+
125+
# The factory path is unaffected.
126+
obj = JsiiCalc::ClassWithPrivateConstructorAndAutomaticProperties.create('Hello', 'Bye')
127+
expect(obj.read_only_string).to eq('Hello')
128+
end
110129
end
111130
end

packages/jsii-pacmak/lib/targets/ruby.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,10 +624,26 @@ export class RubyGenerator extends Generator {
624624
`Jsii::Object.instance_method(:initialize).bind(self).call(${superArgs})`,
625625
);
626626
this.code.close('end');
627+
} else if (initializer) {
628+
// Parameterless constructor (the jsii compiler propagates initializer
629+
// entries onto instantiable subclasses, so a missing parameter list
630+
// here really means "takes no arguments" — enforce that arity rather
631+
// than silently forwarding stray args to the kernel).
632+
this.code.open('def initialize');
633+
this.code.line(
634+
'Jsii::Object.instance_method(:initialize).bind(self).call',
635+
);
636+
this.code.close('end');
627637
} else {
638+
// No initializer entry at all: jsii emits this for classes whose
639+
// constructor is not visible (private). Instances only ever come
640+
// from factory methods and are hydrated via `allocate`, which never
641+
// calls #initialize — so constructing one from Ruby is always a bug.
642+
// Raise eagerly with a pointer to the factories instead of letting
643+
// the kernel reject the create call with a less helpful error.
628644
this.code.open('def initialize(*args)');
629645
this.code.line(
630-
'Jsii::Object.instance_method(:initialize).bind(self).call(*args)',
646+
`raise NoMethodError, "${rubyDq(typeSpec.fqn)} does not have a visible constructor; use the provided factory methods"`,
631647
);
632648
this.code.close('end');
633649
}

0 commit comments

Comments
 (0)