Report diagnostics error when no matching property found.
This commit is contained in:
parent
df34907d69
commit
da6fb3e589
|
@ -6,15 +6,18 @@ namespace MapTo
|
||||||
{
|
{
|
||||||
private const string UsageCategory = "Usage";
|
private const string UsageCategory = "Usage";
|
||||||
|
|
||||||
internal static Diagnostic SymbolNotFound(Location location, string syntaxName) =>
|
internal static Diagnostic SymbolNotFoundError(Location location, string syntaxName) =>
|
||||||
Create("MT0001", "Symbol not found.", $"Unable to find any symbols for {syntaxName}", location);
|
Create("MT0001", "Symbol not found.", $"Unable to find any symbols for {syntaxName}", location);
|
||||||
|
|
||||||
internal static Diagnostic MapFromAttributeNotFound(Location location) =>
|
internal static Diagnostic MapFromAttributeNotFoundError(Location location) =>
|
||||||
Create("MT0002", "Attribute Not Available", $"Unable to find {SourceBuilder.MapFromAttributeName} type.", location);
|
Create("MT0002", "Attribute Not Available", $"Unable to find {SourceBuilder.MapFromAttributeName} type.", location);
|
||||||
|
|
||||||
internal static Diagnostic ClassMappingsGenerated(Location location, string typeName) =>
|
internal static Diagnostic ClassMappingsGenerated(Location location, string typeName) =>
|
||||||
Create("MT1001", "Mapped Type", $"Generated mappings for {typeName}", location, DiagnosticSeverity.Info);
|
Create("MT1001", "Mapped Type", $"Generated mappings for {typeName}", location, DiagnosticSeverity.Info);
|
||||||
|
|
||||||
|
internal static Diagnostic NoMatchingPropertyFoundError(Location location, string className, string sourceTypeName) =>
|
||||||
|
Create("MT2001", "Property Not Found", $"No matching properties found between '{className}' and '{sourceTypeName}' types.", location);
|
||||||
|
|
||||||
private static Diagnostic Create(string id, string title, string message, Location location, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
|
private static Diagnostic Create(string id, string title, string message, Location location, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
|
||||||
Diagnostic.Create(new DiagnosticDescriptor(id, title, message, UsageCategory, severity, true), location);
|
Diagnostic.Create(new DiagnosticDescriptor(id, title, message, UsageCategory, severity, true), location);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,12 @@
|
||||||
<RootNamespace>MapTo</RootNamespace>
|
<RootNamespace>MapTo</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
|
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
|
||||||
|
</AssemblyAttribute>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MapTo.Extensions;
|
using MapTo.Extensions;
|
||||||
using MapTo.Models;
|
using MapTo.Models;
|
||||||
|
@ -31,20 +32,12 @@ namespace MapTo
|
||||||
{
|
{
|
||||||
foreach (var classSyntax in candidateClasses)
|
foreach (var classSyntax in candidateClasses)
|
||||||
{
|
{
|
||||||
var root = classSyntax.GetCompilationUnit();
|
var model = CreateModel(context, classSyntax);
|
||||||
var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree);
|
if (model is null)
|
||||||
var classSymbol = classSemanticModel.GetDeclaredSymbol(classSyntax) as INamedTypeSymbol;
|
|
||||||
var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel);
|
|
||||||
|
|
||||||
var (isValid, diagnostics) = Verify(root, classSyntax, classSemanticModel, classSymbol, sourceTypeSymbol);
|
|
||||||
if (!isValid)
|
|
||||||
{
|
{
|
||||||
diagnostics.ForEach(context.ReportDiagnostic);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var model = new MapModel(root, classSyntax, classSymbol!, sourceTypeSymbol!);
|
|
||||||
|
|
||||||
var (source, hintName) = SourceBuilder.GenerateSource(model);
|
var (source, hintName) = SourceBuilder.GenerateSource(model);
|
||||||
context.AddSource(hintName, source);
|
context.AddSource(hintName, source);
|
||||||
context.ReportDiagnostic(Diagnostics.ClassMappingsGenerated(classSyntax.GetLocation(), model.ClassName));
|
context.ReportDiagnostic(Diagnostics.ClassMappingsGenerated(classSyntax.GetLocation(), model.ClassName));
|
||||||
|
@ -68,21 +61,51 @@ namespace MapTo
|
||||||
return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
|
return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type as INamedTypeSymbol : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (bool isValid, IEnumerable<Diagnostic> diagnostics) Verify(CompilationUnitSyntax root, ClassDeclarationSyntax classSyntax, SemanticModel classSemanticModel, INamedTypeSymbol? classSymbol, INamedTypeSymbol? sourceTypeSymbol)
|
private static MapModel? CreateModel(GeneratorExecutionContext context, ClassDeclarationSyntax classSyntax)
|
||||||
{
|
{
|
||||||
var diagnostics = new List<Diagnostic>();
|
var root = classSyntax.GetCompilationUnit();
|
||||||
|
var classSemanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree);
|
||||||
|
|
||||||
if (classSymbol is null)
|
if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol))
|
||||||
{
|
{
|
||||||
diagnostics.Add(Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
|
context.ReportDiagnostic(Diagnostics.SymbolNotFoundError(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel);
|
||||||
if (sourceTypeSymbol is null)
|
if (sourceTypeSymbol is null)
|
||||||
{
|
{
|
||||||
diagnostics.Add(Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
|
context.ReportDiagnostic(Diagnostics.MapFromAttributeNotFoundError(classSyntax.GetLocation()));
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (!diagnostics.Any(), diagnostics);
|
var className = classSyntax.GetClassName();
|
||||||
|
var sourceClassName = sourceTypeSymbol.Name;
|
||||||
|
|
||||||
|
var mappedProperties = GetMappedProperties(classSymbol, sourceTypeSymbol);
|
||||||
|
if (!mappedProperties.Any())
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostics.NoMatchingPropertyFoundError(classSyntax.GetLocation(), className, sourceClassName));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MapModel(
|
||||||
|
root.GetNamespace(),
|
||||||
|
classSyntax.Modifiers,
|
||||||
|
className,
|
||||||
|
sourceTypeSymbol.ContainingNamespace.ToString(),
|
||||||
|
sourceClassName,
|
||||||
|
sourceTypeSymbol.ToString(),
|
||||||
|
mappedProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableArray<string> GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
|
||||||
|
{
|
||||||
|
return sourceTypeSymbol
|
||||||
|
.GetAllMembersOfType<IPropertySymbol>()
|
||||||
|
.Select(p => p.Name)
|
||||||
|
.Intersect(classSymbol.GetAllMembersOfType<IPropertySymbol>().Select(p => p.Name))
|
||||||
|
.ToImmutableArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,57 +1,40 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Immutable;
|
||||||
using MapTo.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
|
|
||||||
namespace MapTo.Models
|
namespace MapTo.Models
|
||||||
{
|
{
|
||||||
public class MapModel
|
public class MapModel
|
||||||
{
|
{
|
||||||
private MapModel(
|
internal MapModel(
|
||||||
string? ns,
|
string? ns,
|
||||||
SyntaxTokenList classModifiers,
|
SyntaxTokenList classModifiers,
|
||||||
string className,
|
string className,
|
||||||
IEnumerable<IPropertySymbol> properties,
|
|
||||||
string sourceNamespace,
|
string sourceNamespace,
|
||||||
string sourceClassName,
|
string sourceClassName,
|
||||||
string sourceClassFullName,
|
string sourceClassFullName,
|
||||||
IEnumerable<IPropertySymbol> sourceTypeProperties)
|
ImmutableArray<string> mappedProperties)
|
||||||
{
|
{
|
||||||
Namespace = ns;
|
Namespace = ns;
|
||||||
ClassModifiers = classModifiers;
|
ClassModifiers = classModifiers;
|
||||||
ClassName = className;
|
ClassName = className;
|
||||||
Properties = properties;
|
|
||||||
SourceNamespace = sourceNamespace;
|
SourceNamespace = sourceNamespace;
|
||||||
SourceClassName = sourceClassName;
|
SourceClassName = sourceClassName;
|
||||||
SourceClassFullName = sourceClassFullName;
|
SourceClassFullName = sourceClassFullName;
|
||||||
SourceTypeProperties = sourceTypeProperties;
|
MappedProperties = mappedProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal MapModel(CompilationUnitSyntax root, ClassDeclarationSyntax classSyntax, ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
|
|
||||||
: this(
|
|
||||||
root.GetNamespace(),
|
|
||||||
classSyntax.Modifiers,
|
|
||||||
classSyntax.GetClassName(),
|
|
||||||
classSymbol.GetAllMembersOfType<IPropertySymbol>(),
|
|
||||||
sourceTypeSymbol.ContainingNamespace.ToString(),
|
|
||||||
sourceTypeSymbol.Name,
|
|
||||||
sourceTypeSymbol.ToString(),
|
|
||||||
sourceTypeSymbol.GetAllMembersOfType<IPropertySymbol>()) { }
|
|
||||||
|
|
||||||
public string? Namespace { get; }
|
public string? Namespace { get; }
|
||||||
|
|
||||||
public SyntaxTokenList ClassModifiers { get; }
|
public SyntaxTokenList ClassModifiers { get; }
|
||||||
|
|
||||||
public string ClassName { get; }
|
public string ClassName { get; }
|
||||||
|
|
||||||
public IEnumerable<IPropertySymbol> Properties { get; }
|
|
||||||
|
|
||||||
public string SourceNamespace { get; }
|
public string SourceNamespace { get; }
|
||||||
|
|
||||||
public string SourceClassName { get; }
|
public string SourceClassName { get; }
|
||||||
|
|
||||||
public string SourceClassFullName { get; }
|
public string SourceClassFullName { get; }
|
||||||
|
|
||||||
public IEnumerable<IPropertySymbol> SourceTypeProperties { get; }
|
public ImmutableArray<string> MappedProperties { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -57,7 +57,7 @@ namespace MapTo
|
||||||
.AppendOpeningBracket(Indent1)
|
.AppendOpeningBracket(Indent1)
|
||||||
|
|
||||||
// Class body
|
// Class body
|
||||||
.GenerateConstructor(model, out var mappedProperties)
|
.GenerateConstructor(model)
|
||||||
.AppendLine()
|
.AppendLine()
|
||||||
.GenerateFactoryMethod(model)
|
.GenerateFactoryMethod(model)
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ namespace MapTo
|
||||||
return builder.AppendLine();
|
return builder.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StringBuilder GenerateConstructor(this StringBuilder builder, MapModel model, out List<IPropertySymbol> mappedProperties)
|
private static StringBuilder GenerateConstructor(this StringBuilder builder, MapModel model)
|
||||||
{
|
{
|
||||||
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
|
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
|
||||||
|
|
||||||
|
@ -98,25 +98,15 @@ namespace MapTo
|
||||||
.AppendFormat("public {0}({1} {2})", model.ClassName, model.SourceClassFullName, sourceClassParameterName)
|
.AppendFormat("public {0}({1} {2})", model.ClassName, model.SourceClassFullName, sourceClassParameterName)
|
||||||
.AppendOpeningBracket(Indent2)
|
.AppendOpeningBracket(Indent2)
|
||||||
.PadLeft(Indent3)
|
.PadLeft(Indent3)
|
||||||
.AppendFormat("if ({0} == null) throw new ArgumentNullException(nameof({0}));", sourceClassParameterName)
|
.AppendFormat("if ({0} == null) throw new ArgumentNullException(nameof({0}));", sourceClassParameterName).AppendLine()
|
||||||
.AppendLine();
|
.AppendLine();
|
||||||
|
|
||||||
mappedProperties = new List<IPropertySymbol>();
|
foreach (var property in model.MappedProperties)
|
||||||
|
|
||||||
if (model.SourceTypeProperties.Any())
|
|
||||||
{
|
{
|
||||||
builder.AppendLine();
|
builder
|
||||||
|
.PadLeft(Indent3)
|
||||||
foreach (var propertySymbol in model.SourceTypeProperties)
|
.AppendFormat("{0} = {1}.{2};", property, sourceClassParameterName, property)
|
||||||
{
|
.AppendLine();
|
||||||
if (model.Properties.Any(p => p.Name == propertySymbol.Name))
|
|
||||||
{
|
|
||||||
mappedProperties.Add(propertySymbol);
|
|
||||||
builder
|
|
||||||
.PadLeft(Indent3)
|
|
||||||
.AppendFormat("{0} = {1}.{2};{3}", propertySymbol.Name, sourceClassParameterName, propertySymbol.Name, Environment.NewLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// End constructor declaration
|
// End constructor declaration
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using MapToTests;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace MapToTests
|
namespace MapTo.Tests
|
||||||
{
|
{
|
||||||
public class Tests
|
public class Tests
|
||||||
{
|
{
|
||||||
|
@ -93,14 +95,12 @@ namespace Test
|
||||||
[MapFrom(typeof(Baz))]
|
[MapFrom(typeof(Baz))]
|
||||||
public partial class Foo
|
public partial class Foo
|
||||||
{
|
{
|
||||||
|
public int Prop1 { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Baz
|
public class Baz
|
||||||
{
|
{
|
||||||
public int Prop1 { get; set; }
|
public int Prop1 { get; set; }
|
||||||
public int Prop2 { get; }
|
|
||||||
public int Prop3 { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
@ -116,6 +116,8 @@ namespace Test
|
||||||
public Foo(Test.Baz baz)
|
public Foo(Test.Baz baz)
|
||||||
{
|
{
|
||||||
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
||||||
|
|
||||||
|
Prop1 = baz.Prop1;
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
|
@ -128,6 +130,31 @@ namespace Test
|
||||||
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
|
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void When_MapToAttributeFoundWithoutMatchingProperties_Should_ReportError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedDiagnostic = Diagnostics.NoMatchingPropertyFoundError(Location.None, "Foo", "Baz");
|
||||||
|
const string source = @"
|
||||||
|
using MapTo;
|
||||||
|
|
||||||
|
namespace Test
|
||||||
|
{
|
||||||
|
[MapFrom(typeof(Baz))]
|
||||||
|
public partial class Foo { }
|
||||||
|
|
||||||
|
public class Baz { public int Prop1 { get; set; } }
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (_, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var error = diagnostics.FirstOrDefault(d => d.Id == expectedDiagnostic.Id);
|
||||||
|
error.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass()
|
public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass()
|
||||||
{
|
{
|
||||||
|
@ -136,15 +163,9 @@ namespace Test
|
||||||
namespace Test
|
namespace Test
|
||||||
{
|
{
|
||||||
[MapTo.MapFrom(typeof(Baz))]
|
[MapTo.MapFrom(typeof(Baz))]
|
||||||
public partial class Foo
|
public partial class Foo { public int Prop1 { get; set; } }
|
||||||
{
|
|
||||||
|
|
||||||
}
|
public class Baz { public int Prop1 { get; set; } }
|
||||||
|
|
||||||
public class Baz
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
|
@ -159,6 +180,8 @@ namespace Test
|
||||||
public Foo(Test.Baz baz)
|
public Foo(Test.Baz baz)
|
||||||
{
|
{
|
||||||
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
||||||
|
|
||||||
|
Prop1 = baz.Prop1;
|
||||||
}
|
}
|
||||||
";
|
";
|
||||||
|
|
||||||
|
@ -187,7 +210,7 @@ namespace Test
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void When_SourceTypeHasDifferentNamespace_Should_AddToUsings()
|
public void When_SourceTypeHasDifferentNamespace_Should_NotAddToUsings()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var source = GetSourceText(sourceClassNamespace: "Bazaar");
|
var source = GetSourceText(sourceClassNamespace: "Bazaar");
|
||||||
|
@ -195,7 +218,8 @@ namespace Test
|
||||||
const string expectedResult = @"
|
const string expectedResult = @"
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Bazaar;
|
|
||||||
|
namespace Test
|
||||||
";
|
";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
@ -219,6 +243,7 @@ using Bazaar;
|
||||||
public Foo(Test.Models.Baz baz)
|
public Foo(Test.Models.Baz baz)
|
||||||
{
|
{
|
||||||
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
||||||
|
|
||||||
Prop1 = baz.Prop1;
|
Prop1 = baz.Prop1;
|
||||||
Prop2 = baz.Prop2;
|
Prop2 = baz.Prop2;
|
||||||
Prop3 = baz.Prop3;
|
Prop3 = baz.Prop3;
|
||||||
|
@ -263,7 +288,7 @@ using Bazaar;
|
||||||
var source = GetSourceText();
|
var source = GetSourceText();
|
||||||
|
|
||||||
const string expectedResult = @"
|
const string expectedResult = @"
|
||||||
public static partial class BazExtensions
|
public static partial class BazToFooExtensions
|
||||||
{
|
{
|
||||||
public static Foo ToFoo(this Test.Models.Baz baz)
|
public static Foo ToFoo(this Test.Models.Baz baz)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue