From e005e0bf8a121af0268d745baab30b7aaf914c0e Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Mon, 5 Oct 2026 15:42:51 +0000 Subject: [PATCH 1/2] [patch] Call super() first in a JavaScript derived class's constructor The JavaScript generator wrote `class Circle extends Shape` but never wrote a `super(...)` call in the constructor, so every `new Circle()` threw ReferenceError before it could assign to `this`. C++ and C# call the base's default constructor implicitly, so the same AST worked in those targets. A constructor in a class with a BaseType now opens with `super();`. An initialiser named after the base (the C++ `: Base(args)` idiom) supplies the argument as `super(value);` rather than being assigned to `this.Base`. Fixes #103 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N47Hdpq8iSMutiKdDS81pa --- .../Languages/JavaScriptGeneratorTests.cs | 92 +++++++++++++++++++ Coder/Languages/JavaScriptGenerator.cs | 44 ++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/Coder.Test/Languages/JavaScriptGeneratorTests.cs b/Coder.Test/Languages/JavaScriptGeneratorTests.cs index f0c3f6d..dac344a 100644 --- a/Coder.Test/Languages/JavaScriptGeneratorTests.cs +++ b/Coder.Test/Languages/JavaScriptGeneratorTests.cs @@ -160,4 +160,96 @@ public void CanGenerate_RejectsUnknownNode() Assert.IsFalse(Generator.CanGenerate(new AstLeafNode(1.0))); Assert.ThrowsExactly(() => Generator.Generate(new AstLeafNode(1.0))); } + /// + /// Builds a Shape base class and a Circle that extends it with a constructor, the + /// shape that throws on construction when the derived constructor never calls super. + /// + /// Adds the constructor's base-call initialiser, if any. + /// A source file holding both classes. + private static SourceFile DerivedClassFile(Action? circleConstructor = null) + { + ClassDeclaration shape = new("Shape"); + shape.Members.Add(new FieldDeclaration("name", "string") { InitialValue = new LiteralExpression("shape") }); + + ClassDeclaration circle = new("Circle") { BaseType = "Shape" }; + circle.Members.Add(new FieldDeclaration("radius", "double")); + FunctionDeclaration constructor = new("Circle") { Kind = FunctionKind.Constructor }; + constructor.Parameters.Add(new Parameter("radius", "double")); + circleConstructor?.Invoke(constructor); + constructor.Initialisers.Add(new MemberInitialiser("radius") { Value = new VariableReference("radius") }); + circle.Members.Add(constructor); + + return new SourceFile("shapes") { Members = { shape, circle } }; + } + + /// + /// Tests that a derived class's constructor calls super() before it assigns to this. + /// + [TestMethod] + public void DerivedConstructor_CallsSuperBeforeInitialisers() + { + string code = Generator.Generate(DerivedClassFile()); + + int superCall = code.IndexOf("super();", StringComparison.Ordinal); + int assignment = code.IndexOf("this.radius = radius;", StringComparison.Ordinal); + Assert.IsGreaterThanOrEqualTo(0, superCall, $"The derived constructor should call super():{Environment.NewLine}{code}"); + Assert.IsLessThan(assignment, superCall, $"super() should come before the first assignment to this:{Environment.NewLine}{code}"); + } + + /// + /// Tests that a class with no base type gets no super call, which would be a syntax error. + /// + [TestMethod] + public void BaseClassConstructor_DoesNotCallSuper() + { + ClassDeclaration shape = new("Shape"); + FunctionDeclaration constructor = new("Shape") { Kind = FunctionKind.Constructor }; + constructor.Initialisers.Add(new MemberInitialiser("name") { Value = new LiteralExpression("shape") }); + shape.Members.Add(constructor); + + string code = Generator.Generate(shape); + + Assert.DoesNotContain("super", code); + } + + /// + /// Tests that an initialiser named after the base, the C++ : Base(args) idiom, passes its + /// value to super instead of being assigned to a member called after the base. + /// + [TestMethod] + public void DerivedConstructor_BaseNamedInitialiser_BecomesTheSuperArgument() + { + string code = Generator.Generate(DerivedClassFile(constructor => + constructor.Initialisers.Add(new MemberInitialiser("Shape") { Value = new VariableReference("radius") }))); + + StringAssert.Contains(code, "super(radius);", StringComparison.Ordinal); + Assert.DoesNotContain("this.Shape", code); + Assert.DoesNotContain("super();", code); + } + + /// + /// Tests that an instance of a derived class can be constructed under Node.js, and that both the + /// base's field and the derived constructor's assignment land on it. + /// + [TestMethod] + public void DerivedClass_ConstructsUnderNode() + { + string? node = ToolchainHarness.FindOnPath("--version", "node"); + if (node is null) + { + Assert.Inconclusive("No Node.js runtime on the path, so nothing was run."); + return; + } + + string generated = Generator.Generate(DerivedClassFile()); + + ToolchainHarness.InTemporaryDirectory(directory => + { + string path = Path.Combine(directory, "shapes.mjs"); + File.WriteAllText(path, generated + "const c = new Circle(2);\nconsole.log(c.radius, c.name);\n"); + (int exitCode, string output) = ToolchainHarness.Run(node, path, directory); + Assert.AreEqual(0, exitCode, $"Node.js could not construct the derived class:{Environment.NewLine}{output}{Environment.NewLine}{generated}"); + StringAssert.Contains(output, "2 shape", StringComparison.Ordinal); + }); + } } diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index 444d062..10531fc 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -366,7 +366,7 @@ protected override void GenerateClassDeclaration(ClassDeclaration classDecl, Cod switch (member) { case FunctionDeclaration method: - GenerateMethod(method, code); + GenerateMethod(method, classDecl.BaseType, code); break; case EnumDeclaration nested: @@ -416,8 +416,9 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code) /// Emits a function as a class method, which drops the function keyword. /// /// The function to emit as a method. + /// The class the method's class extends, if any. /// The writer to emit into. - private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) + private void GenerateMethod(FunctionDeclaration method, TypeReference? baseType, CodeBlocker code) { WritePreamble(method, code, PreambleSite.Method); @@ -457,10 +458,19 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) return; } + MemberInitialiser? baseInitialiser = method.Kind == FunctionKind.Constructor && baseType is not null + ? WriteSuperCall(method, baseType, code) + : null; + // JavaScript assigns where C++ initialises, before the body's own statements and in the order // declared, which is what the initialiser means where there is no initialiser list. foreach (MemberInitialiser initialiser in method.Initialisers) { + if (ReferenceEquals(initialiser, baseInitialiser)) + { + continue; + } + code.Write($"this.{initialiser.Name} = "); if (initialiser.Value is not null) @@ -477,6 +487,36 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code) } } + /// + /// Emits the base constructor call a derived class's constructor has to make. + /// + /// The constructor being emitted. + /// The class it extends. + /// The writer to emit into. + /// The initialiser that supplied the call's argument, or null when the call passes none. + /// + /// A derived constructor that touches this, or returns at all, without calling super + /// throws, so the call comes first. C++ and C# call the base's default constructor implicitly, and + /// a call with no arguments is the same thing. An initialiser named after the base is the C++ + /// : Base(args) idiom, so its value is passed to the base rather than assigned to a member. + /// + private MemberInitialiser? WriteSuperCall(FunctionDeclaration constructor, TypeReference baseType, CodeBlocker code) + { + MemberInitialiser? baseInitialiser = constructor.Initialisers + .FirstOrDefault(initialiser => string.Equals(initialiser.Name, baseType.Name, StringComparison.Ordinal)); + + code.Write("super("); + + if (baseInitialiser?.Value is not null) + { + GenerateInternal(baseInitialiser.Value, code); + } + + code.Write(")"); + EndStatement(code); + return baseInitialiser; + } + /// protected override void GenerateParameter(Parameter parameter, CodeBlocker code, int position) { From 4fc48b2c216d0681b2aaf4f15e3092593af7015e Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Mon, 5 Oct 2026 16:02:51 +0000 Subject: [PATCH 2/2] [patch] Move JavaScript initialiser assignments out of GenerateMethod The super() call pushed GenerateMethod's cognitive complexity to 16, over SonarCloud's limit of 15 (S3776). The initialiser loop, including skipping the initialiser already passed to super, now lives in WriteInitialiserAssignments. Output is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N47Hdpq8iSMutiKdDS81pa --- Coder/Languages/JavaScriptGenerator.cs | 32 ++++++++++++++++---------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Coder/Languages/JavaScriptGenerator.cs b/Coder/Languages/JavaScriptGenerator.cs index 10531fc..9ce6799 100644 --- a/Coder/Languages/JavaScriptGenerator.cs +++ b/Coder/Languages/JavaScriptGenerator.cs @@ -462,15 +462,28 @@ private void GenerateMethod(FunctionDeclaration method, TypeReference? baseType, ? WriteSuperCall(method, baseType, code) : null; - // JavaScript assigns where C++ initialises, before the body's own statements and in the order - // declared, which is what the initialiser means where there is no initialiser list. - foreach (MemberInitialiser initialiser in method.Initialisers) + WriteInitialiserAssignments(method, baseInitialiser, code); + + foreach (AstNode statement in method.Body) { - if (ReferenceEquals(initialiser, baseInitialiser)) - { - continue; - } + GenerateInternal(statement, code); + } + } + /// + /// Writes a method's member initialisers as assignments to this. + /// + /// The method whose initialisers to write. + /// The initialiser already passed to super, which is skipped. + /// The writer to emit into. + /// + /// JavaScript assigns where C++ initialises, before the body's own statements and in the order + /// declared, which is what the initialiser means where there is no initialiser list. + /// + private void WriteInitialiserAssignments(FunctionDeclaration method, MemberInitialiser? baseInitialiser, CodeBlocker code) + { + foreach (MemberInitialiser initialiser in method.Initialisers.Where(initialiser => !ReferenceEquals(initialiser, baseInitialiser))) + { code.Write($"this.{initialiser.Name} = "); if (initialiser.Value is not null) @@ -480,11 +493,6 @@ private void GenerateMethod(FunctionDeclaration method, TypeReference? baseType, EndStatement(code); } - - foreach (AstNode statement in method.Body) - { - GenerateInternal(statement, code); - } } ///