Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions Coder.Test/Languages/JavaScriptGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,96 @@
Assert.IsFalse(Generator.CanGenerate(new AstLeafNode<double>(1.0)));
Assert.ThrowsExactly<NotSupportedException>(() => Generator.Generate(new AstLeafNode<double>(1.0)));
}
/// <summary>
/// Builds a <c>Shape</c> base class and a <c>Circle</c> that extends it with a constructor, the
/// shape that throws on construction when the derived constructor never calls <c>super</c>.
/// </summary>
/// <param name="circleConstructor">Adds the constructor's base-call initialiser, if any.</param>
/// <returns>A source file holding both classes.</returns>
private static SourceFile DerivedClassFile(Action<FunctionDeclaration>? circleConstructor = null)
{
ClassDeclaration shape = new("Shape");
shape.Members.Add(new FieldDeclaration("name", "string") { InitialValue = new LiteralExpression<string>("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 } };
}

/// <summary>
/// Tests that a derived class's constructor calls <c>super()</c> before it assigns to <c>this</c>.
/// </summary>
[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}");
}

/// <summary>
/// Tests that a class with no base type gets no <c>super</c> call, which would be a syntax error.
/// </summary>
[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<string>("shape") });
shape.Members.Add(constructor);

string code = Generator.Generate(shape);

Assert.DoesNotContain("super", code);
}

/// <summary>
/// Tests that an initialiser named after the base, the C++ <c>: Base(args)</c> idiom, passes its
/// value to <c>super</c> instead of being assigned to a member called after the base.
/// </summary>
[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);

Check warning on line 225 in Coder.Test/Languages/JavaScriptGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaEMyh_s37OYPLjGZrtp&open=AaEMyh_s37OYPLjGZrtp&pullRequest=156
Assert.DoesNotContain("this.Shape", code);
Assert.DoesNotContain("super();", code);
}

/// <summary>
/// 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.
/// </summary>
[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);

Check warning on line 252 in Coder.Test/Languages/JavaScriptGeneratorTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'StringAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Coder&issues=AaEMyh_s37OYPLjGZrtq&open=AaEMyh_s37OYPLjGZrtq&pullRequest=156
});
}
}
62 changes: 55 additions & 7 deletions Coder/Languages/JavaScriptGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -416,8 +416,9 @@ private void GenerateField(VariableDeclaration field, CodeBlocker code)
/// Emits a function as a class method, which drops the <c>function</c> keyword.
/// </summary>
/// <param name="method">The function to emit as a method.</param>
/// <param name="baseType">The class the method's class extends, if any.</param>
/// <param name="code">The writer to emit into.</param>
private void GenerateMethod(FunctionDeclaration method, CodeBlocker code)
private void GenerateMethod(FunctionDeclaration method, TypeReference? baseType, CodeBlocker code)
{
WritePreamble(method, code, PreambleSite.Method);

Expand Down Expand Up @@ -457,9 +458,31 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code)
return;
}

// 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)
MemberInitialiser? baseInitialiser = method.Kind == FunctionKind.Constructor && baseType is not null
? WriteSuperCall(method, baseType, code)
: null;

WriteInitialiserAssignments(method, baseInitialiser, code);

foreach (AstNode statement in method.Body)
{
GenerateInternal(statement, code);
}
}

/// <summary>
/// Writes a method's member initialisers as assignments to <c>this</c>.
/// </summary>
/// <param name="method">The method whose initialisers to write.</param>
/// <param name="baseInitialiser">The initialiser already passed to <c>super</c>, which is skipped.</param>
/// <param name="code">The writer to emit into.</param>
/// <remarks>
/// 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.
/// </remarks>
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} = ");

Expand All @@ -470,11 +493,36 @@ private void GenerateMethod(FunctionDeclaration method, CodeBlocker code)

EndStatement(code);
}
}

foreach (AstNode statement in method.Body)
/// <summary>
/// Emits the base constructor call a derived class's constructor has to make.
/// </summary>
/// <param name="constructor">The constructor being emitted.</param>
/// <param name="baseType">The class it extends.</param>
/// <param name="code">The writer to emit into.</param>
/// <returns>The initialiser that supplied the call's argument, or null when the call passes none.</returns>
/// <remarks>
/// A derived constructor that touches <c>this</c>, or returns at all, without calling <c>super</c>
/// 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++
/// <c>: Base(args)</c> idiom, so its value is passed to the base rather than assigned to a member.
/// </remarks>
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(statement, code);
GenerateInternal(baseInitialiser.Value, code);
}

code.Write(")");
EndStatement(code);
return baseInitialiser;
}

/// <inheritdoc/>
Expand Down
Loading