Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional compilation for Nullable ref types #2146

Merged
merged 12 commits into from
Jan 17, 2023
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Adds support for nullable reference types in dotnet for projects running Netstandard 2.1/Net 6.0 and above []()

### Changed

- Updated the client constructor to set the baseUrl path parameter from RequestAdapter's baseUrl(PHP) [#2129](https://github.com/microsoft/kiota/issues/2129)
Expand Down
5 changes: 3 additions & 2 deletions src/Kiota.Builder/Writers/CSharp/CSharpConventionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public class CSharpConventionService : CommonLanguageConventionService {
public override string VoidTypeName => "void";
public override string DocCommentPrefix => "/// ";
private static readonly HashSet<string> NullableTypes = new(StringComparer.OrdinalIgnoreCase) { "int", "bool", "float", "double", "decimal", "long", "Guid", "DateTimeOffset", "TimeSpan", "Date","Time", "sbyte", "byte" };
public static readonly char NullableMarker = '?';
public const char NullableMarker = '?';
public const string NullableEnableDirective = "NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER";
public static string NullableMarkerAsString => "?";
public override string ParseNodeInterfaceName => "IParseNode";
public override void WriteShortDescription(string description, LanguageWriter writer) {
Expand Down Expand Up @@ -57,7 +58,7 @@ internal void AddParametersAssignment(LanguageWriter writer, CodeTypeBase pathPa
).ToArray());
}
#pragma warning restore CA1822 // Method should be static
internal bool ShouldTypeHaveNullableMarker(CodeTypeBase propType, string propTypeName) {
private static bool ShouldTypeHaveNullableMarker(CodeTypeBase propType, string propTypeName) {
return propType.IsNullable && (NullableTypes.Contains(propTypeName) || (propType is CodeType codeType && codeType.TypeDefinition is CodeEnum));
}
private static HashSet<string> _namespaceSegmentsNames;
Expand Down
31 changes: 28 additions & 3 deletions src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,8 @@ private void WriteSerializerBodyForInheritedModel(bool shouldHide, CodeMethod me
.Where(static x => !x.ExistsInBaseType && !x.ReadOnly)
.OrderBy(static x => x.Name))
{
writer.WriteLine($"writer.{GetSerializationMethodName(otherProp.Type, method)}(\"{otherProp.SerializationName ?? otherProp.Name}\", {otherProp.Name.ToFirstCharacterUpperCase()});");
var serializationMethodName = GetSerializationMethodName(otherProp.Type, method);
writer.WriteLine($"writer.{serializationMethodName}(\"{otherProp.SerializationName ?? otherProp.Name}\", {otherProp.Name.ToFirstCharacterUpperCase()});");
}
}
private void WriteSerializerBodyForUnionModel(CodeMethod method, CodeClass parentClass, LanguageWriter writer)
Expand Down Expand Up @@ -554,9 +555,33 @@ private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string
baseSuffix = " : base()";
var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p => conventions.GetParameterSignature(p, code)).ToList());
var methodName = isConstructor ? code.Parent.Name.ToFirstCharacterUpperCase() : code.Name.ToFirstCharacterUpperCase();
var includeNullableReferenceType = code.Parameters.Any(static codeParameter => codeParameter.IsOfKind(CodeParameterKind.RequestConfiguration));
if (includeNullableReferenceType)
{
var completeReturnTypeWithNullable = string.IsNullOrEmpty(genericTypeSuffix) ? completeReturnType : $"{completeReturnType[..^2]}?{genericTypeSuffix} ";
var nullableParameters = string.Join(", ", code.Parameters.OrderBy(static x => x, parameterOrderComparer)
.Select(p => p.IsOfKind(CodeParameterKind.RequestConfiguration) ?
GetParameterSignatureWithNullableRefType(p,code):
conventions.GetParameterSignature(p, code))
.ToList());
writer.WriteLine($"#if {CSharpConventionService.NullableEnableDirective}",false);
writer.WriteLine($"{conventions.GetAccessModifier(code.Access)} {staticModifier}{hideModifier}{completeReturnTypeWithNullable}{methodName}({nullableParameters}){baseSuffix} {{");
writer.WriteLine("#else",false);
}

writer.WriteLine($"{conventions.GetAccessModifier(code.Access)} {staticModifier}{hideModifier}{completeReturnType}{methodName}({parameters}){baseSuffix} {{");

if (includeNullableReferenceType)
writer.WriteLine("#endif",false);

}

private string GetParameterSignatureWithNullableRefType(CodeParameter parameter, CodeElement targetElement)
{
var signatureSegments = conventions.GetParameterSignature(parameter, targetElement).Split(" ", StringSplitOptions.RemoveEmptyEntries);
return $"{signatureSegments[0]}? {string.Join(" ",signatureSegments[1..])}";
}
private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method)
private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method, bool includeNullableRef = false)
{
var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None;
var propertyType = conventions.GetTypeString(propType, method, false);
Expand All @@ -577,7 +602,7 @@ private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod meth
{
"byte[]" => "WriteByteArrayValue",
_ when conventions.IsPrimitiveType(propertyType) => $"Write{propertyType.TrimEnd(CSharpConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value",
_ => $"WriteObjectValue<{propertyType.ToFirstCharacterUpperCase()}>",
_ => $"WriteObjectValue<{propertyType.ToFirstCharacterUpperCase()}{(includeNullableRef ? "?": string.Empty)}>",
};
}
}
28 changes: 22 additions & 6 deletions src/Kiota.Builder/Writers/CSharp/CodePropertyWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,39 @@ public class CodePropertyWriter : BaseElementWriter<CodeProperty, CSharpConventi
{
public CodePropertyWriter(CSharpConventionService conventionService): base(conventionService) { }
public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter writer)
{
var propertyType = conventions.GetTypeString(codeElement.Type, codeElement);
var isNullableReferenceType = !propertyType.EndsWith("?")
&& codeElement.IsOfKind(CodePropertyKind.Custom,CodePropertyKind.QueryParameter);// Other property types are apropriately constructor initialized
conventions.WriteShortDescription(codeElement.Documentation.Description, writer);
if (isNullableReferenceType)
{
writer.WriteLine($"#if {CSharpConventionService.NullableEnableDirective}",false);
WritePropertyInternal(codeElement, writer, $"{propertyType}?");
writer.WriteLine("#else",false);
}

WritePropertyInternal(codeElement, writer, propertyType);// Always write the normal way

if (isNullableReferenceType)
writer.WriteLine("#endif",false);
}

private void WritePropertyInternal(CodeProperty codeElement, LanguageWriter writer, string propertyType)
{
var parentClass = codeElement.Parent as CodeClass;
var backingStoreProperty = parentClass.GetBackingStoreProperty();
var setterAccessModifier = codeElement.ReadOnly && codeElement.Access > AccessModifier.Private ? "private " : string.Empty;
var simpleBody = $"get; {setterAccessModifier}set;";
var propertyType = conventions.GetTypeString(codeElement.Type, codeElement);
var defaultValue = string.Empty;
conventions.WriteShortDescription(codeElement.Documentation.Description, writer);
switch(codeElement.Kind) {
case CodePropertyKind.RequestBuilder:
writer.WriteLine($"{conventions.GetAccessModifier(codeElement.Access)} {propertyType} {codeElement.Name.ToFirstCharacterUpperCase()} {{ get =>");
writer.IncreaseIndent();
conventions.AddRequestBuilderBody(parentClass, propertyType, writer);
writer.DecreaseIndent();
writer.WriteLine("}");
break;
break;
case CodePropertyKind.AdditionalData when backingStoreProperty != null:
case CodePropertyKind.Custom when backingStoreProperty != null:
var backingStoreKey = codeElement.SerializationName ?? codeElement.Name.ToFirstCharacterLowerCase();
Expand All @@ -31,7 +48,7 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w
writer.WriteLine($"set {{ {backingStoreProperty.Name.ToFirstCharacterUpperCase()}?.Set(\"{backingStoreKey}\", value); }}");
writer.DecreaseIndent();
writer.WriteLine("}");
break;
break;
case CodePropertyKind.QueryParameter when codeElement.IsNameEscaped:
writer.WriteLine($"[QueryParameter(\"{codeElement.SerializationName}\")]");
goto default;
Expand All @@ -40,8 +57,7 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w
goto default;
default:
writer.WriteLine($"{conventions.GetAccessModifier(codeElement.Access)} {propertyType} {codeElement.Name.ToFirstCharacterUpperCase()} {{ {simpleBody} }}{defaultValue}");
break;
break;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ public void WritesRequestExecutorBody() {
Assert.Contains(AsyncKeyword, result);
Assert.Contains("await", result);
Assert.Contains("cancellationToken", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void WritesRequestExecutorBodyForCollection() {
Expand All @@ -402,7 +402,7 @@ public void WritesRequestExecutorBodyForCollection() {
Assert.Contains("SendCollectionAsync", result);
Assert.Contains("return collectionResult.ToList()", result);
Assert.Contains($"{ReturnTypeName}.CreateFromDiscriminatorValue", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void DoesntCreateDictionaryOnEmptyErrorMapping() {
Expand All @@ -413,7 +413,7 @@ public void DoesntCreateDictionaryOnEmptyErrorMapping() {
var result = tw.ToString();
Assert.DoesNotContain("var errorMapping = new Dictionary<string, Func<IParsable>>", result);
Assert.Contains("default", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void WritesModelFactoryBodyForUnionModels() {
Expand Down Expand Up @@ -647,7 +647,7 @@ public void WritesRequestExecutorBodyForCollections() {
var result = tw.ToString();
Assert.Contains("SendCollectionAsync", result);
Assert.Contains("cancellationToken", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void WritesRequestGeneratorBodyForScalar() {
Expand All @@ -671,7 +671,7 @@ public void WritesRequestGeneratorBodyForScalar() {
Assert.Contains("requestInfo.AddRequestOptions(requestConfig.O)", result);
Assert.Contains("SetContentFromScalar", result);
Assert.Contains("return requestInfo;", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void WritesRequestGeneratorBodyForParsable() {
Expand All @@ -695,7 +695,7 @@ public void WritesRequestGeneratorBodyForParsable() {
Assert.Contains("requestInfo.AddRequestOptions(requestConfig.O)", result);
Assert.Contains("SetContentFromParsable", result);
Assert.Contains("return requestInfo;", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void WritesRequestGeneratorBodyForScalarCollection() {
Expand All @@ -711,7 +711,7 @@ public void WritesRequestGeneratorBodyForScalarCollection() {
writer.Write(method);
var result = tw.ToString();
Assert.Contains("SetContentFromScalarCollection", result);
AssertExtensions.CurlyBracesAreClosed(result);
AssertExtensions.CurlyBracesAreClosed(result,1);
}
[Fact]
public void WritesInheritedDeSerializerBody() {
Expand Down