Skip to content

Commit

Permalink
fix: dotnet abstract classes should have proxy implementations
Browse files Browse the repository at this point in the history
Abstract classes have can implementations that are not shared thorugh jsii. When trying to instantiate one for these types as a return value, the runtime crashes.
This change creates proxy class types for abstract classes. These types can be instantiated, and have implementations for any methods/properties marked as abstract.

Fixes aws#223
  • Loading branch information
costleya committed Sep 21, 2018
1 parent 04cb6bb commit ad1b15b
Show file tree
Hide file tree
Showing 26 changed files with 865 additions and 146 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using Amazon.JSII.Generator.Class;
using Amazon.JSII.JsonModel.Spec;
using Xunit;

namespace Amazon.JSII.Generator.UnitTests.Class
{
public class AbstractClassProxyGeneratorTests : GeneratorTestBase
{
private const string Prefix = nameof(Generator) + "." + nameof(AbstractClassProxyGenerator) + ".";

private string Render(ClassType classType)
{
Symbols.MapTypeToPackage("myFqn", classType.Assembly);
Symbols.MapNamespace(classType.QualifiedNamespace, "MyNamespace");
Symbols.MapTypeName("myFqn", "MyClass", TypeKind.Class);

var generator = new AbstractClassProxyGenerator(classType.Assembly, classType, Symbols, Namespaces);

var syntaxTree = generator.CreateSyntaxTree();
return syntaxTree.ToString();
}

[Fact(DisplayName = Prefix + nameof(IncludesAttribute))]
public void IncludesAttribute()
{
var classType = new ClassType
(
"myFqn",
"myPackage",
"myClass",
true,
initializer: new Method(true, false, false)
);

var actual = Render(classType);
var expected =
@"namespace MyNamespace
{
[JsiiInterfaceProxy(typeof(MyClass), ""myFqn"")]
internal class MyClassProxy : MyClass
{
private MyClassProxy(ByRefValue reference): base(reference)
{
}
}
}";
Assert.Equal(expected, actual, ignoreLineEndingDifferences: true);
}

[Fact(DisplayName = Prefix + nameof(IncludesDocs))]
public void IncludesDocs()
{
// Docs are not currently generated as part of the C# code.
ClassType classType = new ClassType
(
fullyQualifiedName: "myFqn",
assembly: "myPackage",
name: "myClass",
isAbstract: true,
initializer: new Method(true, false, false),
docs: new Docs {{"foo", "bar"}}
);

string actual = Render(classType);
string expected =
@"namespace MyNamespace
{
/// <remarks>foo: bar</remarks>
[JsiiInterfaceProxy(typeof(MyClass), ""myFqn"")]
internal class MyClassProxy : MyClass
{
private MyClassProxy(ByRefValue reference): base(reference)
{
}
}
}";
Assert.Equal(expected, actual, ignoreLineEndingDifferences: true);
}

[Fact(DisplayName = Prefix + nameof(IncludesProperties))]
public void IncludesProperties()
{
ClassType classType = new ClassType
(
fullyQualifiedName: "myFqn",
assembly: "myPackage",
name: "myClass",
isAbstract: true,
initializer: new Method(true, false, false),
properties: new[]
{
new Property
(
name: "myProp",
type: new TypeReference("myPropTypeFqn"),
isImmutable: false,
isAbstract: true,
isProtected: false
),
new Property
(
name: "notMyProp",
type: new TypeReference("myPropTypeFqn"),
isImmutable: false,
isAbstract: false,
isProtected: false
)
}
);

Symbols.MapTypeName("myPropTypeFqn", "MyPropType", TypeKind.Class);
Symbols.MapPropertyName("myFqn", "myProp", "MyProp");

string actual = Render(classType);
string expected =
@"namespace MyNamespace
{
[JsiiInterfaceProxy(typeof(MyClass), ""myFqn"")]
internal class MyClassProxy : MyClass
{
private MyClassProxy(ByRefValue reference): base(reference)
{
}
[JsiiProperty(""myProp"", ""{\""fqn\"":\""myPropTypeFqn\""}"")]
public override MyPropType MyProp
{
get => GetInstanceProperty<MyPropType>();
set => SetInstanceProperty(value);
}
}
}";
Assert.Equal(expected, actual, ignoreLineEndingDifferences: true);
}

[Fact(DisplayName = Prefix + nameof(IncludesMethods))]
public void IncludesMethods()
{
ClassType classType = new ClassType
(
fullyQualifiedName: "myFqn",
assembly: "myPackage",
name: "myClass",
isAbstract: true,
initializer: new Method(true, false, false),
methods: new[]
{
new Method
(
false,
false,
true,
name: "myMethod"
),
new Method
(
false,
false,
false,
name: "notMyMethod"
)
}
);

Symbols.MapMethodName("myFqn", "myMethod", "MyMethod");

string actual = Render(classType);
string expected =
@"namespace MyNamespace
{
[JsiiInterfaceProxy(typeof(MyClass), ""myFqn"")]
internal class MyClassProxy : MyClass
{
private MyClassProxy(ByRefValue reference): base(reference)
{
}
[JsiiMethod(""myMethod"", null, ""[]"")]
public override void MyMethod()
{
InvokeInstanceVoidMethod(new object[]{});
}
}
}";
Assert.Equal(expected, actual, ignoreLineEndingDifferences: true);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
using Amazon.JSII.Generator.Class;
using Amazon.JSII.Generator.Interface;
using Amazon.JSII.JsonModel.Spec;
using Microsoft.CodeAnalysis;
using Xunit;
using TypeKind = Amazon.JSII.JsonModel.Spec.TypeKind;

namespace Amazon.JSII.Generator.UnitTests.Class
{
public class ClassGeneratorTests : GeneratorTestBase
{
const string Prefix = nameof(Generator) + "." + nameof(InterfaceGenerator) + ".";
const string Prefix = nameof(Generator) + "." + nameof(ClassGenerator) + ".";

string Render(ClassType classType)
private string Render(ClassType classType)
{
Symbols.MapTypeToPackage("myFqn", classType.Assembly);
Symbols.MapNamespace(classType.QualifiedNamespace, "MyNamespace");
Symbols.MapTypeName("myFqn", "MyClass", JsonModel.Spec.TypeKind.Class);
Symbols.MapTypeName("myFqn", "MyClass", TypeKind.Class);

ClassGenerator generator = new ClassGenerator(classType.Assembly, classType, Symbols, Namespaces);

Expand All @@ -36,7 +36,7 @@ public void IncludesAttribute()

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
public class MyClass : DeputyBase
Expand Down Expand Up @@ -71,7 +71,7 @@ public void IncludesAbstractKeyword()

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
public abstract class MyClass : DeputyBase
Expand Down Expand Up @@ -103,12 +103,12 @@ public void IncludesDocs()
name: "myClass",
isAbstract: false,
initializer: new Method(true, false, false),
docs: new Docs { { "foo", "bar" } }
docs: new Docs {{"foo", "bar"}}
);

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
/// <remarks>foo: bar</remarks>
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
Expand Down Expand Up @@ -144,7 +144,7 @@ public void IncludesProperties()
{
new Property
(
name: "myProp",
name: "myProp",
type: new TypeReference("myPropTypeFqn"),
isImmutable: false,
isAbstract: false,
Expand All @@ -153,12 +153,12 @@ public void IncludesProperties()
}
);

Symbols.MapTypeName("myPropTypeFqn", "MyPropType", JsonModel.Spec.TypeKind.Class);
Symbols.MapTypeName("myPropTypeFqn", "MyPropType", TypeKind.Class);
Symbols.MapPropertyName("myFqn", "myProp", "MyProp");

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
public class MyClass : DeputyBase
Expand Down Expand Up @@ -212,7 +212,7 @@ public void IncludesMethods()

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
public class MyClass : DeputyBase
Expand Down Expand Up @@ -252,11 +252,11 @@ public void IncludesBase()
@base: new TypeReference("myBaseTypeFqn")
);

Symbols.MapTypeName("myBaseTypeFqn", "MyBaseType", JsonModel.Spec.TypeKind.Class);
Symbols.MapTypeName("myBaseTypeFqn", "MyBaseType", TypeKind.Class);

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
public class MyClass : MyBaseType
Expand Down Expand Up @@ -294,12 +294,12 @@ public void IncludesInterfaces()
}
);

Symbols.MapTypeName("myInterfaceFqn1", "MyInterface1", JsonModel.Spec.TypeKind.Interface);
Symbols.MapTypeName("myInterfaceFqn2", "MyInterface2", JsonModel.Spec.TypeKind.Interface);
Symbols.MapTypeName("myInterfaceFqn1", "MyInterface1", TypeKind.Interface);
Symbols.MapTypeName("myInterfaceFqn2", "MyInterface2", TypeKind.Interface);

string actual = Render(classType);
string expected =
@"namespace MyNamespace
@"namespace MyNamespace
{
[JsiiClass(typeof(MyClass), ""myFqn"", ""[]"")]
public class MyClass : DeputyBase, IMyInterface1, IMyInterface2
Expand All @@ -320,4 +320,4 @@ protected MyClass(DeputyProps props): base(props)
Assert.Equal(expected, actual, ignoreLineEndingDifferences: true);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Amazon.JSII.JsonModel.Spec;
using NSubstitute;
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using Type = Amazon.JSII.JsonModel.Spec.Type;

namespace Amazon.JSII.Generator.UnitTests
{
Expand Down Expand Up @@ -91,6 +90,15 @@ public static void MapTypeName(this ISymbolMap symbols, string fullyQualifiedNam
frameworkName = $"I{frameworkName}";
}

if (kind == TypeKind.Class)
{
string proxyName = $"{frameworkName}Proxy";

symbols
.GetAbstractClassProxyName(Arg.Is<ClassType>(t => t.FullyQualifiedName == fullyQualifiedName), disambiguate: Arg.Any<bool>())
.Returns(proxyName);
}

symbols
.GetName(Arg.Is<Type>(t => t.FullyQualifiedName == fullyQualifiedName), disambiguate: Arg.Any<bool>())
.Returns(frameworkName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,25 +281,47 @@ void SaveType(Type type)
switch (type.Kind)
{
case TypeKind.Class:
SaveTypeFile($"{symbols.GetName(type)}.cs", new ClassGenerator(assembly.Name, (ClassType)type, symbols).CreateSyntaxTree());
{
var classType = (ClassType) type;

if (classType.IsAbstract)
{
SaveTypeFile($"{symbols.GetAbstractClassProxyName(classType)}.cs",
new AbstractClassProxyGenerator(assembly.Name, classType, symbols).CreateSyntaxTree());
}

SaveTypeFile($"{symbols.GetName(type)}.cs",
new ClassGenerator(assembly.Name, classType, symbols).CreateSyntaxTree());
return;
}
case TypeKind.Enum:
SaveTypeFile($"{symbols.GetName(type)}.cs", new EnumGenerator(assembly.Name, (EnumType)type, symbols).CreateSyntaxTree());
{
SaveTypeFile($"{symbols.GetName(type)}.cs",
new EnumGenerator(assembly.Name, (EnumType) type, symbols).CreateSyntaxTree());
return;
}
case TypeKind.Interface:
InterfaceType interfaceType = (InterfaceType)type;
{
InterfaceType interfaceType = (InterfaceType) type;

SaveTypeFile($"{symbols.GetName(interfaceType)}.cs", new InterfaceGenerator(assembly.Name, interfaceType, symbols).CreateSyntaxTree());
SaveTypeFile($"{symbols.GetInterfaceProxyName(interfaceType)}.cs", new InterfaceProxyGenerator(assembly.Name, interfaceType, symbols).CreateSyntaxTree());
SaveTypeFile($"{symbols.GetName(interfaceType)}.cs",
new InterfaceGenerator(assembly.Name, interfaceType, symbols).CreateSyntaxTree());
SaveTypeFile($"{symbols.GetInterfaceProxyName(interfaceType)}.cs",
new InterfaceProxyGenerator(assembly.Name, interfaceType, symbols).CreateSyntaxTree());

if (interfaceType.IsDataType == true)
{
SaveTypeFile($"{symbols.GetInterfaceDefaultName(interfaceType)}.cs", new InterfaceDefaultGenerator(assembly.Name, interfaceType, symbols).CreateSyntaxTree());
SaveTypeFile($"{symbols.GetInterfaceDefaultName(interfaceType)}.cs",
new InterfaceDefaultGenerator(assembly.Name, interfaceType, symbols)
.CreateSyntaxTree());
}

return;
}
default:
{
throw new ArgumentException($"Unkown type kind: {type.Kind}", nameof(type));
}
}

void SaveTypeFile(string filename, SyntaxTree syntaxTree)
Expand Down
Loading

0 comments on commit ad1b15b

Please sign in to comment.