-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add Flatting Complex diagnostic with tests * Add fix with tests * Update README
- Loading branch information
Showing
10 changed files
with
396 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
src/AutoMapper.Analyzers.Common.CodeFixes/FlattingComplexModelCodeFixProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string> 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<TextSpan> { diagnostic.Location.SourceSpan }; | ||
diagnosticSpans.AddRange(diagnostic.AdditionalLocations.Select(al => al.SourceSpan)); | ||
|
||
var declarations = diagnosticSpans.Select(s => | ||
root.FindToken(s.Start).Parent.Ancestors().OfType<InvocationExpressionSyntax>().First()); | ||
|
||
context.RegisterCodeFix( | ||
CodeAction.Create("Replace manual complex flatting by IncludeMembers call", | ||
c => UseIncludeMembers(context.Document, declarations, c), "FlattingComplexModelFixTitle"), diagnostic); | ||
} | ||
|
||
private async Task<Document> UseIncludeMembers(Document document, | ||
IEnumerable<InvocationExpressionSyntax> 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<InvocationExpressionSyntax>().FirstOrDefault()); | ||
|
||
var lambda = BuildLambdaExpression(declarations); | ||
var includeMembers = GetIncludeMembersInvocation(declarations, ref syntaxRoot); | ||
|
||
var argumentList = new List<ArgumentSyntax>(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<InvocationExpressionSyntax> declarations, ref SyntaxNode? syntaxRoot) | ||
{ | ||
var invocationExpressions = syntaxRoot.FindToken(declarations.ElementAt(0).SpanStart).Parent.Ancestors().OfType<InvocationExpressionSyntax>(); | ||
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<InvocationExpressionSyntax>() | ||
.First(i => i.ToString().Contains(includeMembersName)); | ||
} | ||
|
||
return includeMembers; | ||
} | ||
|
||
private static SimpleLambdaExpressionSyntax BuildLambdaExpression(IEnumerable<InvocationExpressionSyntax> 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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
src/AutoMapper.Analyzers.Common/FlattingComplexModelAnalyzer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<InvocationExpressionSyntax>().Where(IsForMember).ToList(); | ||
var prevMembers = ForMember.Ancestors().OfType<InvocationExpressionSyntax>().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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.