Add nullable reference types support.

This commit is contained in:
Mohammadreza Taikandi 2021-01-27 07:53:29 +00:00
parent cfb0debf82
commit f7a755332e
11 changed files with 146 additions and 39 deletions

View File

@ -1,20 +1,11 @@
using System.Collections.Immutable;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
namespace MapTo
{
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context) => new(
context.GetBuildGlobalOption<AccessModifier>(nameof(ConstructorAccessModifier)),
context.GetBuildGlobalOption<AccessModifier>(nameof(GeneratedMethodsAccessModifier)),
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true)
);
}
internal record SourceCode(string Text, string HintName);
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
@ -28,4 +19,25 @@ namespace MapTo
string SourceClassFullName,
ImmutableArray<MappedProperty> MappedProperties
);
internal record SourceGenerationOptions(
AccessModifier ConstructorAccessModifier,
AccessModifier GeneratedMethodsAccessModifier,
bool GenerateXmlDocument,
bool SupportNullableReferenceTypes)
{
internal static SourceGenerationOptions From(GeneratorExecutionContext context)
{
var compilationOptions = (context.Compilation as CSharpCompilation)?.Options;
return new(
context.GetBuildGlobalOption<AccessModifier>(nameof(ConstructorAccessModifier)),
context.GetBuildGlobalOption<AccessModifier>(nameof(GeneratedMethodsAccessModifier)),
context.GetBuildGlobalOption(nameof(GenerateXmlDocument), true),
compilationOptions is not null && (compilationOptions.NullableContextOptions == NullableContextOptions.Warnings || compilationOptions.NullableContextOptions == NullableContextOptions.Enable)
);
}
public string NullableReferenceSyntax => SupportNullableReferenceTypes ? "?" : string.Empty;
}
}

View File

@ -14,6 +14,7 @@ namespace MapTo.Sources
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
.WriteOpeningBracket();
@ -44,7 +45,7 @@ namespace MapTo.Sources
}
builder
.WriteLine("TDestination Convert(TSource source, object[] converterParameters);")
.WriteLine($"TDestination Convert(TSource source, object[]{options.NullableReferenceSyntax} converterParameters);")
.WriteClosingBracket()
.WriteClosingBracket();

View File

@ -9,6 +9,8 @@ namespace MapTo.Sources
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(model.Options.SupportNullableReferenceTypes)
.WriteLine()
.WriteUsings(model)
.WriteLine()
@ -90,7 +92,7 @@ namespace MapTo.Sources
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName} {sourceClassParameterName})")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} From({model.SourceClassFullName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
.WriteClosingBracket();
@ -127,7 +129,7 @@ namespace MapTo.Sources
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName} {sourceClassParameterName})")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName} To{model.ClassName}(this {model.SourceClassFullName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
.WriteClosingBracket();

View File

@ -12,9 +12,11 @@ namespace MapTo.Sources
internal const string ConverterParametersPropertyName = "ConverterParameters";
internal static SourceCode Generate(SourceGenerationOptions options)
{
{
using var builder = new SourceBuilder()
.WriteLine(GeneratedFilesHeader)
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
.WriteLine()
.WriteLine("using System;")
.WriteLine()
.WriteLine($"namespace {RootNamespace}")
@ -44,7 +46,7 @@ namespace MapTo.Sources
}
builder
.WriteLine($"public {AttributeClassName}(Type converter, object[] converterParameters = null)")
.WriteLine($"public {AttributeClassName}(Type converter, object[]{options.NullableReferenceSyntax} converterParameters = null)")
.WriteOpeningBracket()
.WriteLine($"{ConverterPropertyName} = converter;")
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
@ -72,7 +74,7 @@ namespace MapTo.Sources
}
builder
.WriteLine($"public object[] {ConverterParametersPropertyName} {{ get; }}")
.WriteLine($"public object[]{options.NullableReferenceSyntax} {ConverterParametersPropertyName} {{ get; }}")
.WriteClosingBracket()
.WriteClosingBracket();

View File

@ -22,18 +22,32 @@ namespace MapTo.Sources
_indentedWriter.Dispose();
}
public SourceBuilder WriteLine(string value)
public SourceBuilder WriteLine(string? value = null)
{
_indentedWriter.WriteLine(value);
if (string.IsNullOrWhiteSpace(value))
{
_indentedWriter.WriteLineNoTabs(string.Empty);
}
else
{
_indentedWriter.WriteLine(value);
}
return this;
}
public SourceBuilder WriteLine()
public SourceBuilder WriteLineIf(bool condition, string? value)
{
_indentedWriter.WriteLineNoTabs(string.Empty);
if (condition)
{
WriteLine(value);
}
return this;
}
public SourceBuilder WriteNullableContextOptionIf(bool enabled) => WriteLineIf(enabled, "#nullable enable");
public SourceBuilder WriteOpeningBracket()
{
_indentedWriter.WriteLine("{");

View File

@ -1,4 +0,0 @@
namespace MapTo.Sources
{
internal record SourceCode(string Text, string HintName);
}

View File

@ -11,7 +11,7 @@ namespace MapTo.Tests.Infrastructure
{
internal static class CSharpGenerator
{
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary<string, string> analyzerConfigOptions = null)
internal static (Compilation compilation, ImmutableArray<Diagnostic> diagnostics) GetOutputCompilation(string source, bool assertCompilation = false, IDictionary<string, string> analyzerConfigOptions = null, NullableContextOptions nullableContextOptions = NullableContextOptions.Disable)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var references = AppDomain.CurrentDomain.GetAssemblies()
@ -19,25 +19,29 @@ namespace MapTo.Tests.Infrastructure
.Select(a => MetadataReference.CreateFromFile(a.Location))
.ToList();
var compilation = CSharpCompilation.Create($"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic", new[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var compilation = CSharpCompilation.Create(
$"{typeof(CSharpGenerator).Assembly.GetName().Name}.Dynamic",
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: nullableContextOptions));
if (assertCompilation)
{
// NB: fail tests when the injected program isn't valid _before_ running generators
compilation.GetDiagnostics().ShouldBeSuccessful();
}
var driver = CSharpGeneratorDriver.Create(
new[] { new MapToGenerator() },
optionsProvider: new TestAnalyzerConfigOptionsProvider(analyzerConfigOptions)
);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
var diagnostics = outputCompilation.GetDiagnostics()
.Where(d => d.Severity >= DiagnosticSeverity.Warning)
.Select(c => $"{c.Severity}: {c.Location.GetLineSpan().StartLinePosition} - {c.GetMessage()} [in \"{c.Location.SourceTree?.FilePath}\"]").ToArray();
if (diagnostics.Any())
{
Assert.False(diagnostics.Any(), $@"Failed:

View File

@ -286,6 +286,7 @@ namespace Test
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
@ -355,6 +356,7 @@ namespace Test
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
@ -404,6 +406,7 @@ namespace Test
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
@ -514,6 +517,32 @@ namespace MapTo
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void VerifyTypeConverterInterfaceWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
namespace MapTo
{{
public interface ITypeConverter<in TSource, out TDestination>
{{
TDestination Convert(TSource source, object[]? converterParameters);
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttribute()
{
@ -521,6 +550,7 @@ namespace MapTo
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
using System;
namespace MapTo
@ -548,6 +578,43 @@ namespace MapTo
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
{
// Arrange
const string source = "";
var expectedInterface = $@"
{Constants.GeneratedFilesHeader}
#nullable enable
using System;
namespace MapTo
{{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class MapTypeConverterAttribute : Attribute
{{
public MapTypeConverterAttribute(Type converter, object[]? converterParameters = null)
{{
Converter = converter;
ConverterParameters = converterParameters;
}}
public Type Converter {{ get; }}
public object[]? ConverterParameters {{ get; }}
}}
}}
".Trim();
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: NullableContextOptions.Enable);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
}
[Fact]
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()

View File

@ -1,6 +1,13 @@
using TestConsoleApp.ViewModels;
using VM = TestConsoleApp.ViewModels;
using Data = TestConsoleApp.Data.Models;
var userViewModel = VM.User.From(new Data.User());
var userViewModel2 = new Data.User().ToUserViewModel();
namespace TestConsoleApp
{
internal class Program
{
private static void Main(string[] args)
{
var userViewModel = User.From(new Data.Models.User());
var userViewModel2 = new Data.Models.User().ToUserViewModel();
}
}
}

View File

@ -2,14 +2,16 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>8</LangVersion>
<Nullable>annotations</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\..\src\MapTo\MapTo.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\src\MapTo\MapTo.props"/>
<Import Project="..\..\src\MapTo\MapTo.props" />
<PropertyGroup>
<MapTo_ConstructorAccessModifier>Internal</MapTo_ConstructorAccessModifier>
</PropertyGroup>

View File

@ -16,7 +16,7 @@ namespace TestConsoleApp.ViewModels
private class LastNameConverter : ITypeConverter<long, string>
{
/// <inheritdoc />
public string Convert(long source, object[] converterParameters) => $"{source} :: With Type Converter";
public string Convert(long source, object[]? converterParameters) => $"{source} :: With Type Converter";
}
}
}