// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.ContractsLight;
using System.Linq;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Microsoft.ReactNative.Managed.CodeGen.SyntaxHelpers;
using StatementSyntax = Microsoft.CodeAnalysis.CSharp.Syntax.StatementSyntax;
using SyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind;
namespace Microsoft.ReactNative.Managed.CodeGen
{
///
/// Code generation for serialization
///
public partial class CodeGenerator
{
internal IEnumerable CreateSerializers(IEnumerable typesToSerialize)
{
var classMembers = new List();
var registrationCalls = new List();
foreach (var group in typesToSerialize.GroupBy(type => type.TypeKind))
{
switch (group.Key)
{
case TypeKind.Enum:
classMembers.Add(CreateEnumSerializers(group));
registrationCalls.Add(InvocationStatement(ReactNativeNames.CreateEnumSerializers));
break;
case TypeKind.Class:
case TypeKind.Struct:
classMembers.Add(CreateObjectSerializers(group));
registrationCalls.Add(InvocationStatement(ReactNativeNames.CreateObjectSerializers));
break;
default:
throw Contract.AssertFailure("Unexpected type to serialize");
}
}
// Generates:
// internal void CreateSerializers()
// {
// ... registrationCalls (ses above)
// }
classMembers.Add(MethodDeclaration(
PredefinedType(Token(SyntaxKind.VoidKeyword)),
ReactNativeNames.CreateSerializers)
.AddModifiers(
Token(SyntaxKind.InternalKeyword))
.WithBody(
Block(
registrationCalls)));
return classMembers;
}
internal MemberDeclarationSyntax CreateEnumSerializers(IEnumerable symbols)
{
var registrationCalls = new List();
foreach (var symbol in symbols)
{
Contract.Assert(symbol.TypeKind == TypeKind.Enum);
Contract.Assert(symbol.EnumUnderlyingType != null);
// Generates:
// (EnumType)reader.ReadValue();
// TODO: Should we add support for strings, today the strings must be integers
registrationCalls.Add(
CodeGenRead(
symbol,
expression: CastExpression(
symbol.ToTypeSyntax(),
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ReactTypes.JSValueReader.ToTypeSyntax(),
GenericName(ReactNativeNames.ReadValueMethodName, symbol.EnumUnderlyingType.ToTypeSyntax())
),
new[]
{
Argument(IdentifierName(ReactNativeNames.ReaderLocalName)),
}
)
)
)
);
// Generates:
// writer.WriteValue((EnumBaseType)value);
// TODO: Should we add support for strings, today the strings must be integers
registrationCalls.Add(
CodeGenWrite(
symbol,
expression: InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ReactTypes.JSValueWriter.ToTypeSyntax(),
GenericName(ReactNativeNames.WriteValueMethodName, symbol.EnumUnderlyingType.ToTypeSyntax())),
IdentifierName(ReactNativeNames.WriterLocalName),
CastExpression(
symbol.EnumUnderlyingType.ToTypeSyntax(),
IdentifierName(ReactNativeNames.ValueLocalName)
)
)
)
);
}
return MethodDeclaration(
PredefinedType(Token(SyntaxKind.VoidKeyword)),
ReactNativeNames.CreateEnumSerializers)
.AddModifiers(
Token(SyntaxKind.InternalKeyword))
.WithBody(
Block(
registrationCalls));
}
internal MemberDeclarationSyntax CreateObjectSerializers(IEnumerable symbols)
{
var registrationCalls = new List();
foreach (var symbol in symbols)
{
Contract.Assert(symbol.TypeKind == TypeKind.Enum);
Contract.Assert(symbol.EnumUnderlyingType != null);
var readOperations = new List();
var writeOperations = new List();
writeOperations.Add(
InvocationStatement(
MemberAccessExpression(ReactNativeNames.WriterLocalName, ReactNativeNames.WriteObjectBeginMethodName))
);
// Generates either:
// case "Field1": value.Field1 = reader.ReadValue(); break;
// and/or
// writer.WriteObjectProperty("Field2", value.Field2);
// for each member
foreach (var member in symbol.GetMembers())
{
if (!IsAccessible(member))
{
continue;
}
string name;
string jsonPropertyName;
ISymbol type;
bool emitRead;
bool emitWrite;
if (!member.IsStatic && !member.IsExtern)
{
if (member is IFieldSymbol field)
{
name = field.Name;
jsonPropertyName = TryGetJSNameFromAttribute(field, out var jsName) ? jsName : name;
type = field.Type;
emitRead = !field.IsConst && !field.IsReadOnly;
emitWrite = true;
}
else if
(member is IPropertySymbol property)
{
name = property.Name;
jsonPropertyName = TryGetJSNameFromAttribute(property, out var jsName) ? jsName : name;
type = property.Type;
emitRead = !property.IsReadOnly;
emitWrite = !property.IsWriteOnly;
}
else
{
continue;
}
if (emitRead)
{
readOperations.Add(ReadSwitch(name, jsonPropertyName, type));
}
if (emitWrite)
{
writeOperations.Add(
ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ReactTypes.JSValueWriter.ToTypeSyntax(),
GenericName(ReactNativeNames.WriteObjectPropertyMethodName, type.ToTypeSyntax())),
IdentifierName(ReactNativeNames.WriterLocalName),
LiteralExpression(jsonPropertyName),
MemberAccessExpression(ReactNativeNames.ValueLocalName, Identifier(name)))
)
);
}
}
}
writeOperations.Add(
InvocationStatement(
MemberAccessExpression(ReactNativeNames.WriterLocalName, ReactNativeNames.WriteObjectEndMethodName))
);
// Generates:
// (IJSValueReader reader, out MyType value) =>
// {
// value = new MyType();
// if (reader.ValueType == JSValueType.Object)
// {
// while (reader.GetNextObjectProperty(out string propertyName))
// {
// // For each object field or property
// switch(propertyName)
// {
// ... readOperations ...
// }
// }
// }
// }
registrationCalls.Add(
CodeGenRead(
symbol,
statements: new StatementSyntax[]
{
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentifierName(ReactNativeNames.ValueLocalName),
ObjectCreationExpression(symbol))
),
IfStatement(
BinaryExpression(
SyntaxKind.EqualsExpression,
MemberAccessExpression(ReactNativeNames.ReaderLocalName, ReactNativeNames.ValueTypePropertyName),
MemberAccessExpression(ReactTypes.JSValueType, ReactNativeNames.ObjectEnumMemberName)),
Block(
WhileStatement(
InvocationExpression(
MemberAccessExpression(ReactNativeNames.ReaderLocalName,
ReactNativeNames.GetNextObjectPropertyMethodName),
new[]
{
Argument(
DeclarationExpression(
PredefinedType(
Token(SyntaxKind.StringKeyword)),
SingleVariableDesignation(
ReactNativeNames.PropertyNameLocalName)))
.WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword))
}),
Block(
SwitchStatement(
IdentifierName(ReactNativeNames.PropertyNameLocalName),
new SyntaxList(
readOperations
)))
)
)
)
})
);
// Generates:
// (IJSValueWriter writer, Type value) =>
// {
// if (value != null) // we do not check it for structs
// {
// writer.WriteObjectBegin();
// writer.WriteObjectProperty("Field1", value.Field1);
// writer.WriteObjectProperty("Field2", value.Field2);
// writer.WriteObjectProperty("Prop1", value.Prop1);
// writer.WriteObjectProperty("Prop2", value.Prop2);
// writer.WriteObjectEnd();
// }
// else
// {
// writer.WriteNull();
// }
// }
if (symbol.TypeKind == TypeKind.Struct)
{
registrationCalls.Add(
CodeGenWrite(
symbol,
statements: writeOperations.ToArray())
);
}
else
{
registrationCalls.Add(
CodeGenWrite(
symbol,
statements: new[]
{
IfStatement(
BinaryExpression(
SyntaxKind.NotEqualsExpression,
IdentifierName(ReactNativeNames.ValueLocalName),
LiteralExpression(SyntaxKind.NullLiteralExpression)),
Block(
writeOperations
),
ElseClause(
Block(
InvocationStatement(
MemberAccessExpression(ReactNativeNames.WriterLocalName, ReactNativeNames.WriteNullMethodName)
)
)
)
)
})
);
}
}
return MethodDeclaration(
PredefinedType(Token(SyntaxKind.VoidKeyword)),
ReactNativeNames.CreateObjectSerializers)
.AddModifiers(
Token(SyntaxKind.InternalKeyword))
.WithBody(
Block(
registrationCalls));
}
private bool IsAccessible(ISymbol symbol)
{
return symbol.DeclaredAccessibility == Accessibility.Public ||
symbol.DeclaredAccessibility == Accessibility.Internal;
}
private bool TryGetJSNameFromAttribute(ISymbol member, [NotNullWhen(returnValue: true)] out string? jsName)
{
jsName = null;
AttributeData? attr = member.GetAttributes().FirstOrDefault(
a => a.AttributeClass != null
&& a.AttributeClass.Equals(ReactTypes.ReactPropertyAttribute, SymbolEqualityComparer.Default));
if (attr != null)
{
if (attr.ConstructorArguments.Length > 0)
{
jsName = attr.ConstructorArguments[0].Value as string;
}
foreach (var namedArgument in attr.NamedArguments)
{
if (namedArgument.Key == nameof(ReactPropertyAttribute.PropertyName))
{
jsName = namedArgument.Value.Value as string;
}
}
}
return jsName != null;
}
private SwitchSectionSyntax ReadSwitch(string fieldName, string jsonPropertyName, ISymbol fieldType)
{
return SwitchSection(
new SyntaxList(
CaseSwitchLabel(LiteralExpression(jsonPropertyName))
),
new SyntaxList(
new StatementSyntax[]
{
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(ReactNativeNames.ValueLocalName),
IdentifierName(fieldName)),
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ReactTypes.JSValueReader.ToTypeSyntax(),
GenericName(ReactNativeNames.ReadValueMethodName, fieldType.ToTypeSyntax())
),
new[]
{
Argument(IdentifierName(ReactNativeNames.ReaderLocalName)),
}
)
)),
BreakStatement()
})
);
}
private StatementSyntax CodeGenRead(ITypeSymbol symbol, ExpressionSyntax? expression = null,
IEnumerable? statements = null)
{
Contract.Requires(expression != null ^ statements != null, "Only one of the args can be null.");
// Generates:
// JSValueReaderOf.ReadValue = (reader, out value) => value = ...readExpression...;
return
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
MemberAccessExpression(
ReactTypes.JSValueReaderOf.Construct(symbol),
ReactNativeNames.ReadValueMethodName),
ParenthesizedLambdaExpression(
parameterList: ParameterList(
Parameter(ReactNativeNames.ReaderLocalName)
.WithType(ReactTypes.IJSValueReader.ToTypeSyntax())
,
Parameter(ReactNativeNames.ValueLocalName)
.WithModifiers(SyntaxTokenList.Create(Token(SyntaxKind.OutKeyword)))
.WithType(symbol.ToTypeSyntax())
),
block: statements == null
? null
: Block(statements),
expressionBody: expression == null
? null
: AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentifierName(ReactNativeNames.ValueLocalName),
expression)
)
)
);
}
private StatementSyntax CodeGenWrite(
ITypeSymbol symbol,
ExpressionSyntax? expression = null,
IEnumerable? statements = null)
{
Contract.Requires(expression != null ^ statements != null, "Only one of the args can be null.");
// Generates:
// JSValueWriterOf.WriteValue = (writer, value) => ...writeExpression...;
return
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
MemberAccessExpression(
ReactTypes.JSValueWriterOf.Construct(symbol),
ReactNativeNames.WriteValueMethodName),
ParenthesizedLambdaExpression(
parameterList: ParameterList(
Parameter(ReactNativeNames.WriterLocalName),
Parameter(ReactNativeNames.ValueLocalName)
),
block: statements == null
? null
: Block(statements),
expressionBody: expression
)
)
);
}
internal MemberDeclarationSyntax RegisterExtensionReaders(IDictionary jsReaderFunctions)
{
var nonGeneric = new List();
var generic = new List();
foreach ((var type, var method) in jsReaderFunctions)
{
if (method.IsGenericMethod)
{
generic.Add(InvocationStatement(
MemberAccessExpression(
ReactTypes.JSValueReaderGenerator,
ReactNativeNames.RegisterCodeGeneratorGenericExtensionMethod),
TypeOfExpression(type.ToTypeSyntax())
));
}
else
{
nonGeneric.Add(ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
MemberAccessExpression(
ReactTypes.JSValueReaderOf.Construct(type),
ReactNativeNames.ReadValueMethodName),
MemberAccessExpression(
method.ContainingType,
Identifier(method.Name)
))));
}
}
return MethodDeclaration(
PredefinedType(Token(SyntaxKind.VoidKeyword)),
ReactNativeNames.RegisterExtensionReaders)
.AddModifiers(
Token(SyntaxKind.InternalKeyword))
.WithBody(
Block(
Block(nonGeneric),
Block(generic)));
}
internal MemberDeclarationSyntax RegisterExtensionWriter(IDictionary jsWriterFunctions)
{
var nonGeneric = new List();
var generic = new List();
foreach ((var type, var method) in jsWriterFunctions)
{
if (method.IsGenericMethod)
{
generic.Add(InvocationStatement(
MemberAccessExpression(
ReactTypes.JSValueWriterGenerator,
ReactNativeNames.RegisterCodeGeneratorGenericExtensionMethod),
TypeOfExpression(type.ToTypeSyntax())
));
}
else
{
nonGeneric.Add(ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
MemberAccessExpression(
ReactTypes.JSValueWriterOf.Construct(type),
ReactNativeNames.WriteValueMethodName),
MemberAccessExpression(
method.ContainingType,
Identifier(method.Name)
))));
}
}
return MethodDeclaration(
PredefinedType(Token(SyntaxKind.VoidKeyword)),
ReactNativeNames.RegisterExtensionWriter)
.AddModifiers(
Token(SyntaxKind.InternalKeyword))
.WithBody(
Block(
Block(nonGeneric),
Block(generic)));
}
public void CreateSerializationRegistration(
ICollection separateRegistrationCalls,
ICollection classMembers)
{
//// Generates
//var assembly = typeof(T).Assembly;
//JSValueReaderGenerator.RegisterAssembly(assembly);
//JSValueWriterGenerator.RegisterAssembly(assembly);
// To ensure we haven't missed any types, still register things for reflection lookup
// When we have confidence we have 100% we can remove the following lines.
separateRegistrationCalls.Add(
LocalDeclarationStatement(
ReactNativeNames.AssemblyLocalName,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
ThisExpression(),
IdentifierName(ReactNativeNames.GetTypeMethodName))),
IdentifierName(ReactNativeNames.AssemblyPropertyName))));
separateRegistrationCalls.Add(
InvocationStatement(
MemberAccessExpression(ReactTypes.JSValueReaderGenerator, ReactNativeNames.RegisterAssemblyMethodName),
IdentifierName(ReactNativeNames.AssemblyLocalName)));
separateRegistrationCalls.Add(
InvocationStatement(
MemberAccessExpression(ReactTypes.JSValueWriterGenerator, ReactNativeNames.RegisterAssemblyMethodName),
IdentifierName(ReactNativeNames.AssemblyLocalName)));
}
}
}