Add nullable reference types support.
This commit is contained in:
parent
cfb0debf82
commit
f7a755332e
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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("{");
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
namespace MapTo.Sources
|
||||
{
|
||||
internal record SourceCode(string Text, string HintName);
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue