From 4775b86f95a1baffede51d399ef2c35044b015b5 Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 2 Jun 2022 08:03:21 +0200 Subject: [PATCH] Flatting complex model (#13) * Add Flatting Complex diagnostic with tests * Add fix with tests * Update README --- README.md | 14 +- .../FlattingComplexModelCodeFixProvider.cs | 86 +++++++ .../NullSubstituteCodeFixProvider.cs | 2 +- .../FlattingComplexModelAnalyzer.cs | 53 ++++ .../ForMemberAnalyzer.cs | 2 +- .../RulesResources.Designer.cs | 29 ++- .../RulesResources.resx | 229 +++++++++++++----- .../ClassSourceCode.cs | 4 + .../FlattingComplexModelTests.cs | 35 +++ .../MapBaseAnalyzerTests.cs | 16 +- 10 files changed, 396 insertions(+), 74 deletions(-) create mode 100644 src/AutoMapper.Analyzers.Common.CodeFixes/FlattingComplexModelCodeFixProvider.cs create mode 100644 src/AutoMapper.Analyzers.Common/FlattingComplexModelAnalyzer.cs create mode 100644 tests/AutoMapper.Analyzers.Common.Tests/FlattingComplexModelTests.cs diff --git a/README.md b/README.md index 7d30a91..c97d601 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The project already contains: Common smells - Available + Available AMA0001 Warrning Profile doesn't contain maps @@ -54,6 +54,11 @@ The project already contains: Manual checking that src is not null Available for next checking: "??", "== null", "!= null" + + AMA0005 + Manual flattening of complex model + Available + AMA0006 Manual flattening of naming similar complex model @@ -65,16 +70,11 @@ The project already contains: Available - In Plans + In Plans AMA0004 ForMember ignore for all left properties ... - - AMA0005 - Manual flattening of complex model - ... - ## Installation diff --git a/src/AutoMapper.Analyzers.Common.CodeFixes/FlattingComplexModelCodeFixProvider.cs b/src/AutoMapper.Analyzers.Common.CodeFixes/FlattingComplexModelCodeFixProvider.cs new file mode 100644 index 0000000..39f417a --- /dev/null +++ b/src/AutoMapper.Analyzers.Common.CodeFixes/FlattingComplexModelCodeFixProvider.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace AutoMapper.Analyzers.Common; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FlattingComplexModelCodeFixProvider)), Shared] +public class FlattingComplexModelCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(FlattingComplexModelAnalyzer.DiagnosticId); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + var diagnostic = context.Diagnostics.First(); + var diagnosticSpans = new List { diagnostic.Location.SourceSpan }; + diagnosticSpans.AddRange(diagnostic.AdditionalLocations.Select(al => al.SourceSpan)); + + var declarations = diagnosticSpans.Select(s => + root.FindToken(s.Start).Parent.Ancestors().OfType().First()); + + context.RegisterCodeFix( + CodeAction.Create("Replace manual complex flatting by IncludeMembers call", + c => UseIncludeMembers(context.Document, declarations, c), "FlattingComplexModelFixTitle"), diagnostic); + } + + private async Task UseIncludeMembers(Document document, + IEnumerable declarations, CancellationToken cancellationToken) + { + var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + syntaxRoot = syntaxRoot.ReplaceNodes(declarations.Select(d => syntaxRoot.FindNode(d.Span)), + (_, syntaxNode) => syntaxNode.DescendantNodes().OfType().FirstOrDefault()); + + var lambda = BuildLambdaExpression(declarations); + var includeMembers = GetIncludeMembersInvocation(declarations, ref syntaxRoot); + + var argumentList = new List(includeMembers.ArgumentList.Arguments) { SyntaxFactory.Argument(lambda) }; + + var includeCall = SyntaxFactory.InvocationExpression(includeMembers.Expression, + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(argumentList))); + + return document.WithSyntaxRoot(syntaxRoot.ReplaceNode(includeMembers, includeCall).NormalizeWhitespace()); + } + + private InvocationExpressionSyntax GetIncludeMembersInvocation(IEnumerable declarations, ref SyntaxNode? syntaxRoot) + { + var invocationExpressions = syntaxRoot.FindToken(declarations.ElementAt(0).SpanStart).Parent.Ancestors().OfType(); + var includeMembersName = nameof(IMappingExpression.IncludeMembers); + var includeMembers = invocationExpressions.FirstOrDefault(i => i.ToString().Contains(includeMembersName)); + if (includeMembers == null) + { + SyntaxNode createMap = invocationExpressions.Where(i => i.Expression is GenericNameSyntax).FirstOrDefault(i => i.ToString().StartsWith(nameof(Profile.CreateMap))); + var newCreateMapString = createMap.Parent.ToFullString().Replace(createMap.ToFullString().Trim(),$"{createMap.ToFullString()}.{includeMembersName}()"); + var newCreateMap = SyntaxFactory.ParseExpression(newCreateMapString); + if (createMap.Parent is MemberAccessExpressionSyntax) + { + createMap = createMap.Parent; + } + syntaxRoot = syntaxRoot.ReplaceNode(createMap, newCreateMap); + includeMembers = syntaxRoot.FindToken(declarations.ElementAt(0).SpanStart).Parent.Ancestors().OfType() + .First(i => i.ToString().Contains(includeMembersName)); + } + + return includeMembers; + } + + private static SimpleLambdaExpressionSyntax BuildLambdaExpression(IEnumerable declarations) + { + var srcProperty = + ForMemberAnalyzer.GetLambdaExpressions(declarations.ElementAt(0)).srcExpression as SimpleLambdaExpressionSyntax; + var srcCall = srcProperty.ExpressionBody.ToFullString(); + srcCall = srcCall.Substring(0, srcCall.LastIndexOf('.')); + var srcName = srcProperty.Parameter.Identifier.Text; + + return SyntaxFactory.SimpleLambdaExpression(SyntaxFactory.Parameter(SyntaxFactory.ParseToken(srcName)), + SyntaxFactory.ParseExpression(srcCall)); + } +} \ No newline at end of file diff --git a/src/AutoMapper.Analyzers.Common.CodeFixes/NullSubstituteCodeFixProvider.cs b/src/AutoMapper.Analyzers.Common.CodeFixes/NullSubstituteCodeFixProvider.cs index 6a9fe78..8a70494 100644 --- a/src/AutoMapper.Analyzers.Common.CodeFixes/NullSubstituteCodeFixProvider.cs +++ b/src/AutoMapper.Analyzers.Common.CodeFixes/NullSubstituteCodeFixProvider.cs @@ -75,7 +75,7 @@ private bool IsComplexMapping(InvocationExpressionSyntax invocation, out string srcProperty = string.Empty; var syntaxNode = invocation.DescendantNodes() - .FirstOrDefault(n => n is ConditionalExpressionSyntax || n is BinaryExpressionSyntax); + .FirstOrDefault(n => n is ConditionalExpressionSyntax or BinaryExpressionSyntax); if (syntaxNode is ConditionalExpressionSyntax { Condition: BinaryExpressionSyntax binaryCondition } conditional) { var srcMember = binaryCondition.Left.ToString(); diff --git a/src/AutoMapper.Analyzers.Common/FlattingComplexModelAnalyzer.cs b/src/AutoMapper.Analyzers.Common/FlattingComplexModelAnalyzer.cs new file mode 100644 index 0000000..655b0c4 --- /dev/null +++ b/src/AutoMapper.Analyzers.Common/FlattingComplexModelAnalyzer.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace AutoMapper.Analyzers.Common; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class FlattingComplexModelAnalyzer : ForMemberAnalyzer +{ + public const string DiagnosticId = "AMA0005"; + + protected override string InternalDiagnosticId => DiagnosticId; + + protected override Diagnostic AnalyzeMapFrom(LambdaExpressionSyntax destExpression, + LambdaExpressionSyntax srcExpression) + { + if (TryGetExpressionMemberName(srcExpression, out IdentifierNameSyntax srcName) && + TryGetExpressionMemberName(destExpression, out string destName) && + srcName.ToString().Equals(destName) && srcName.Parent is MemberAccessExpressionSyntax srcMember) + { + var srcMemberCall = srcMember.Expression.ToFullString(); + var nextMembers = ForMember.DescendantNodes().OfType().Where(IsForMember).ToList(); + var prevMembers = ForMember.Ancestors().OfType().Where(IsForMember); + if (!prevMembers.Any(s => IsSameComplexFlatting(s, srcMemberCall)) && + nextMembers.Any(s => IsSameComplexFlatting(s, srcMemberCall))) + { + return Diagnostic.Create(Rule, ForMember.ArgumentList.GetLocation(), nextMembers.Select(m => m.ArgumentList.GetLocation()).ToList(),ProfileName, MapName); + } + } + + return base.AnalyzeMapFrom(destExpression, srcExpression); + } + + private bool IsSameComplexFlatting(InvocationExpressionSyntax syntax, string smelledMemberCall) => + TryGetExpressionMemberName(GetLambdaExpressions(syntax).srcExpression, + out IdentifierNameSyntax srcSyntax) + && srcSyntax.Parent is MemberAccessExpressionSyntax srcAccess && + srcAccess.Expression.ToFullString().Equals(smelledMemberCall); + + private bool IsForMember(InvocationExpressionSyntax syntax) + { + if (syntax.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name.Identifier.Text.Equals(nameof(IMappingExpression.ForMember))) + { + var (descExpression, srcExpression) = GetLambdaExpressions(syntax); + return TryGetExpressionMemberName(descExpression, out string destName) && + TryGetExpressionMemberName(srcExpression, out string srcName) + && destName.Equals(srcName); + } + + return false; + } +} \ No newline at end of file diff --git a/src/AutoMapper.Analyzers.Common/ForMemberAnalyzer.cs b/src/AutoMapper.Analyzers.Common/ForMemberAnalyzer.cs index 5923149..63f2bbc 100644 --- a/src/AutoMapper.Analyzers.Common/ForMemberAnalyzer.cs +++ b/src/AutoMapper.Analyzers.Common/ForMemberAnalyzer.cs @@ -29,7 +29,7 @@ protected override Diagnostic AnalyzeInvocationOperation(IInvocationOperation in return base.AnalyzeInvocationOperation(invocationOperation); } - protected (LambdaExpressionSyntax descExpression, LambdaExpressionSyntax srcExpression) GetLambdaExpressions(InvocationExpressionSyntax forMember) + public static (LambdaExpressionSyntax descExpression, LambdaExpressionSyntax srcExpression) GetLambdaExpressions(InvocationExpressionSyntax forMember) { var destExpression = forMember.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax; var optExpression = forMember.ArgumentList.Arguments[1].Expression as LambdaExpressionSyntax; diff --git a/src/AutoMapper.Analyzers.Common/RulesResources.Designer.cs b/src/AutoMapper.Analyzers.Common/RulesResources.Designer.cs index 46d9a43..2f71b91 100644 --- a/src/AutoMapper.Analyzers.Common/RulesResources.Designer.cs +++ b/src/AutoMapper.Analyzers.Common/RulesResources.Designer.cs @@ -19,7 +19,7 @@ namespace AutoMapper.Analyzers.Common { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class RulesResources { @@ -141,6 +141,33 @@ internal static string AMA0003Title { } } + /// + /// Looks up a localized string similar to Flattening of complex model with naming similar internal properties should be done by `IncludeMember` call.. + /// + internal static string AMA0005Description { + get { + return ResourceManager.GetString("AMA0005Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Profile '{0}' contain map '{1}' with manual flattening of internal naming similar complex model.. + /// + internal static string AMA0005Message { + get { + return ResourceManager.GetString("AMA0005Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Manual flattening of internal naming similar complex model.. + /// + internal static string AMA0005Title { + get { + return ResourceManager.GetString("AMA0005Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to Flattening of naming similar complex model will be done automatically.. /// diff --git a/src/AutoMapper.Analyzers.Common/RulesResources.resx b/src/AutoMapper.Analyzers.Common/RulesResources.resx index 1cfec2e..7ff562d 100644 --- a/src/AutoMapper.Analyzers.Common/RulesResources.resx +++ b/src/AutoMapper.Analyzers.Common/RulesResources.resx @@ -1,65 +1,174 @@  - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Profile class must contain maps. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Profile class must contain maps. + + + Profile '{0}' doesn't contain any map. + + + Profile doesn't contain maps. + + + Properties with identical names should not be mapped manually. + + + Profile '{0}' contain map '{1}' with identical properties mapping. + + + Identical names properties manual mapped. + + + Null checking for properties with identical names should be done by NullSubstitute call. + + + Profile '{0}' contain map '{1}' with manual null checking identical source property. + + + Manual checking that source property is null. + + + Flattening of complex model with naming similar internal properties should be done by `IncludeMember` call. + + + Profile '{0}' contain map '{1}' with manual flattening of internal naming similar complex model. + + + Manual flattening of internal naming similar complex model. + + + Flattening of naming similar complex model will be done automatically. + + + Profile '{0}' contain map '{1}' with manual flattening of naming similar complex model. + + + Manual flattening of naming similar complex model. + + + CreateMap into Profile should not be covered by try-catch/finally. + + + Profile '{0}' contain map '{1}' covered by {2}. + + + Cover CreateMap by try-catch/finally. + \ No newline at end of file diff --git a/tests/AutoMapper.Analyzers.Common.Tests/ClassSourceCode.cs b/tests/AutoMapper.Analyzers.Common.Tests/ClassSourceCode.cs index a6ffadd..48f70d6 100644 --- a/tests/AutoMapper.Analyzers.Common.Tests/ClassSourceCode.cs +++ b/tests/AutoMapper.Analyzers.Common.Tests/ClassSourceCode.cs @@ -32,6 +32,8 @@ public class User public string Name { get; set; } public string Surname { get; set; } + + public string Address { get; set; } } public class OutputObject @@ -43,6 +45,8 @@ public class OutputObject public string Name { get; set; } public string Surname { get; set; } + + public string Address { get; set; } public string UserName { get; set; } } diff --git a/tests/AutoMapper.Analyzers.Common.Tests/FlattingComplexModelTests.cs b/tests/AutoMapper.Analyzers.Common.Tests/FlattingComplexModelTests.cs new file mode 100644 index 0000000..2eab108 --- /dev/null +++ b/tests/AutoMapper.Analyzers.Common.Tests/FlattingComplexModelTests.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; + +namespace AutoMapper.Analyzers.Common.Tests; + +public class FlattingComplexModelTests : MapBaseAnalyzerTests +{ + [TestCase("()\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", 2, TestName = "Two mappings from complex model")] + [TestCase("()\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name))\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", 2, TestName = "Two mappings from complex model with additional ForMember")] + [TestCase("()\n\r.ForMember{|#2:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#1:(dest => dest.Address, opt => opt.MapFrom(src => src.User.Address))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", 3, TestName = "Three mappings from complex model")] + public async Task FlatteningComplexModelAnalyzerRaiseDiagnostic(string forMembers, int locationCount) + { + await VerifyExpectedAsync(FlattingComplexModelAnalyzer.DiagnosticId, forMembers, locationCount); + } + + [TestCase("()\n\r.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User.Name));", TestName = "Usual flattening should not be diagnosed")] + [TestCase("()\n\r.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name));", TestName = "Only one complex flattening should not be diagnosed")] + [TestCase("()\n\r.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id));", TestName = "Similar properties names should not be diagnosed")] + public async Task FlatteningComplexModelAnalyzerSuccess(string forMembers) + { + var goodMapFrom = string.Format(CreateMapCode, forMembers); + await VerifyAnalyzerAsync(goodMapFrom); + } + + [TestCase("()\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", "()\n\r.IncludeMembers(src => src.User);", 2, TestName = "Two mappings from complex model should be replaced by IncludeMembers call")] + [TestCase("()\n\r.IncludeMembers(source => source.Device)\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", "()\n\r.IncludeMembers(source => source.Device, src => src.User);", 2, TestName = "Two mappings from complex model should be added into available IncludeMembers call")] + [TestCase("()\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name))\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", "()\n\r.IncludeMembers(src => src.User)\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name));", 2, TestName = "Two mappings from complex model with additional ForMember before should be replaced by IncludeMembers call")] + [TestCase("()\n\r.IncludeMembers(src => src.Device)\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name))\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", "()\n\r.IncludeMembers(src => src.Device, src => src.User)\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name));", 2, TestName = "Two mappings from complex model with additional ForMember before should be added into available at first place IncludeMembers call")] + [TestCase("()\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name))\n\r.IncludeMembers(src => src.Device)\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", "().ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name))\n\r.IncludeMembers(src => src.Device, src => src.User);", 2, TestName = "Two mappings from complex model with additional ForMember before should be added into available at last place IncludeMembers call")] + [TestCase("()\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|}\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name));", "()\n\r.IncludeMembers(src => src.User)\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name));", 2, TestName = "Two mappings from complex model with additional ForMember after should be replaced by IncludeMembers call")] + [TestCase("()\n\r.ForMember{|#1:(dest => dest.Name, opt => opt.MapFrom(src => src.User.Name))|}\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name))\n\r.ForMember{|#0:(dest => dest.Surname, opt => opt.MapFrom(src => src.User.Surname))|};", "()\n\r.IncludeMembers(src => src.User)\n\r.ForMember(dest => dest.Device, opt => opt.MapFrom(src => src.Device.Name));", 2, TestName = "Two mappings from complex model with additional ForMember between should be replaced by IncludeMembers call")] + public async Task FlatteningComplexModelFixCheck(string forMembers, string includedMember, int locationCount) + { + await VerifyCodeFixAsync(FlattingComplexModelAnalyzer.DiagnosticId, forMembers, includedMember, locationCount); + } +} \ No newline at end of file diff --git a/tests/AutoMapper.Analyzers.Common.Tests/MapBaseAnalyzerTests.cs b/tests/AutoMapper.Analyzers.Common.Tests/MapBaseAnalyzerTests.cs index 1649862..4cb52aa 100644 --- a/tests/AutoMapper.Analyzers.Common.Tests/MapBaseAnalyzerTests.cs +++ b/tests/AutoMapper.Analyzers.Common.Tests/MapBaseAnalyzerTests.cs @@ -10,10 +10,14 @@ public class MapBaseAnalyzerTests : BaseAnalyzerTests private static string MapName => string.Format(CreateMapCode, string.Empty); - protected async Task VerifyExpectedAsync(string diagnosticId, string forMembers) + protected async Task VerifyExpectedAsync(string diagnosticId, string forMembers, int locationCount = 1) { var badMapFrom = string.Format(CreateMapCode, forMembers); - var expected = AnalyzerVerifier.Diagnostic(diagnosticId).WithArguments("TestProfile", MapName).WithLocation(0); + var expected = AnalyzerVerifier.Diagnostic(diagnosticId).WithArguments("TestProfile", MapName); + for (int i = 0; i < locationCount; i++) + { + expected = expected.WithLocation(i); + } await VerifyAnalyzerAsync(badMapFrom, expected); } @@ -22,12 +26,16 @@ protected async Task VerifyAnalyzerAsync(string mapFrom, params DiagnosticResult await AnalyzerVerifier.VerifyAnalyzerAsync(ClassSourceCode(mapFrom), expected); } - protected async Task VerifyCodeFixAsync(string diagnosticId, string forMember, string forMemberFix) + protected async Task VerifyCodeFixAsync(string diagnosticId, string forMember, string forMemberFix, int locationCount = 1) where TCodeFix : CodeFixProvider, new() { var mapFrom = string.Format(CreateMapCode, forMember); var fixMapFrom = string.Format(CreateMapCode, forMemberFix); - var expected = AnalyzerVerifier.Diagnostic(diagnosticId).WithArguments("TestProfile", MapName).WithLocation(0); + var expected = AnalyzerVerifier.Diagnostic(diagnosticId).WithArguments("TestProfile", MapName); + for (int i = 0; i < locationCount; i++) + { + expected = expected.WithLocation(i); + } await AnalyzerVerifier.VerifyCodeFixAsync(ClassSourceCode(mapFrom), expected, ClassSourceCode(fixMapFrom, true)); } } \ No newline at end of file