Skip to content

Commit

Permalink
Flatting complex model (#13)
Browse files Browse the repository at this point in the history
* Add Flatting Complex diagnostic with tests

* Add fix with tests

* Update README
  • Loading branch information
Scogun authored Jun 2, 2022
1 parent 191bf2e commit 4775b86
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 74 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The project already contains:
</tr>
<tr>
<td rowspan="7">Common smells</td>
<td rowspan="5">Available</td>
<td rowspan="6">Available</td>
<td><b>AMA0001</b></td>
<td rowspan="7">Warrning</td>
<td>Profile doesn't contain maps</td>
Expand All @@ -54,6 +54,11 @@ The project already contains:
<td>Manual checking that src is not null</td>
<td>Available for next checking: "??", "== null", "!= null"</td>
</tr>
<tr>
<td><b>AMA0005</b></td>
<td>Manual flattening of complex model</td>
<td>Available</td>
</tr>
<tr>
<td><b>AMA0006</b></td>
<td>Manual flattening of naming similar complex model</td>
Expand All @@ -65,16 +70,11 @@ The project already contains:
<td>Available</td>
</tr>
<tr>
<td rowspan="2">In Plans</td>
<td>In Plans</td>
<td><b>AMA0004</b></td>
<td>ForMember ignore for all left properties</td>
<td>...</td>
</tr>
<tr>
<td><b>AMA0005</b></td>
<td>Manual flattening of complex model</td>
<td>...</td>
</tr>
</table>

## Installation
Expand Down
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
53 changes: 53 additions & 0 deletions src/AutoMapper.Analyzers.Common/FlattingComplexModelAnalyzer.cs
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;
}
}
2 changes: 1 addition & 1 deletion src/AutoMapper.Analyzers.Common/ForMemberAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 28 additions & 1 deletion src/AutoMapper.Analyzers.Common/RulesResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4775b86

Please sign in to comment.