Skip to content

Commit 4775b86

Browse files
authored
Flatting complex model (#13)
* Add Flatting Complex diagnostic with tests * Add fix with tests * Update README
1 parent 191bf2e commit 4775b86

File tree

10 files changed

+396
-74
lines changed

10 files changed

+396
-74
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The project already contains:
3838
</tr>
3939
<tr>
4040
<td rowspan="7">Common smells</td>
41-
<td rowspan="5">Available</td>
41+
<td rowspan="6">Available</td>
4242
<td><b>AMA0001</b></td>
4343
<td rowspan="7">Warrning</td>
4444
<td>Profile doesn't contain maps</td>
@@ -54,6 +54,11 @@ The project already contains:
5454
<td>Manual checking that src is not null</td>
5555
<td>Available for next checking: "??", "== null", "!= null"</td>
5656
</tr>
57+
<tr>
58+
<td><b>AMA0005</b></td>
59+
<td>Manual flattening of complex model</td>
60+
<td>Available</td>
61+
</tr>
5762
<tr>
5863
<td><b>AMA0006</b></td>
5964
<td>Manual flattening of naming similar complex model</td>
@@ -65,16 +70,11 @@ The project already contains:
6570
<td>Available</td>
6671
</tr>
6772
<tr>
68-
<td rowspan="2">In Plans</td>
73+
<td>In Plans</td>
6974
<td><b>AMA0004</b></td>
7075
<td>ForMember ignore for all left properties</td>
7176
<td>...</td>
7277
</tr>
73-
<tr>
74-
<td><b>AMA0005</b></td>
75-
<td>Manual flattening of complex model</td>
76-
<td>...</td>
77-
</tr>
7878
</table>
7979

8080
## Installation
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Collections.Immutable;
2+
using System.Composition;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CodeActions;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
namespace AutoMapper.Analyzers.Common;
11+
12+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FlattingComplexModelCodeFixProvider)), Shared]
13+
public class FlattingComplexModelCodeFixProvider : CodeFixProvider
14+
{
15+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(FlattingComplexModelAnalyzer.DiagnosticId);
16+
17+
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
18+
19+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
20+
{
21+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
22+
23+
var diagnostic = context.Diagnostics.First();
24+
var diagnosticSpans = new List<TextSpan> { diagnostic.Location.SourceSpan };
25+
diagnosticSpans.AddRange(diagnostic.AdditionalLocations.Select(al => al.SourceSpan));
26+
27+
var declarations = diagnosticSpans.Select(s =>
28+
root.FindToken(s.Start).Parent.Ancestors().OfType<InvocationExpressionSyntax>().First());
29+
30+
context.RegisterCodeFix(
31+
CodeAction.Create("Replace manual complex flatting by IncludeMembers call",
32+
c => UseIncludeMembers(context.Document, declarations, c), "FlattingComplexModelFixTitle"), diagnostic);
33+
}
34+
35+
private async Task<Document> UseIncludeMembers(Document document,
36+
IEnumerable<InvocationExpressionSyntax> declarations, CancellationToken cancellationToken)
37+
{
38+
var syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
39+
syntaxRoot = syntaxRoot.ReplaceNodes(declarations.Select(d => syntaxRoot.FindNode(d.Span)),
40+
(_, syntaxNode) => syntaxNode.DescendantNodes().OfType<InvocationExpressionSyntax>().FirstOrDefault());
41+
42+
var lambda = BuildLambdaExpression(declarations);
43+
var includeMembers = GetIncludeMembersInvocation(declarations, ref syntaxRoot);
44+
45+
var argumentList = new List<ArgumentSyntax>(includeMembers.ArgumentList.Arguments) { SyntaxFactory.Argument(lambda) };
46+
47+
var includeCall = SyntaxFactory.InvocationExpression(includeMembers.Expression,
48+
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(argumentList)));
49+
50+
return document.WithSyntaxRoot(syntaxRoot.ReplaceNode(includeMembers, includeCall).NormalizeWhitespace());
51+
}
52+
53+
private InvocationExpressionSyntax GetIncludeMembersInvocation(IEnumerable<InvocationExpressionSyntax> declarations, ref SyntaxNode? syntaxRoot)
54+
{
55+
var invocationExpressions = syntaxRoot.FindToken(declarations.ElementAt(0).SpanStart).Parent.Ancestors().OfType<InvocationExpressionSyntax>();
56+
var includeMembersName = nameof(IMappingExpression.IncludeMembers);
57+
var includeMembers = invocationExpressions.FirstOrDefault(i => i.ToString().Contains(includeMembersName));
58+
if (includeMembers == null)
59+
{
60+
SyntaxNode createMap = invocationExpressions.Where(i => i.Expression is GenericNameSyntax).FirstOrDefault(i => i.ToString().StartsWith(nameof(Profile.CreateMap)));
61+
var newCreateMapString = createMap.Parent.ToFullString().Replace(createMap.ToFullString().Trim(),$"{createMap.ToFullString()}.{includeMembersName}()");
62+
var newCreateMap = SyntaxFactory.ParseExpression(newCreateMapString);
63+
if (createMap.Parent is MemberAccessExpressionSyntax)
64+
{
65+
createMap = createMap.Parent;
66+
}
67+
syntaxRoot = syntaxRoot.ReplaceNode(createMap, newCreateMap);
68+
includeMembers = syntaxRoot.FindToken(declarations.ElementAt(0).SpanStart).Parent.Ancestors().OfType<InvocationExpressionSyntax>()
69+
.First(i => i.ToString().Contains(includeMembersName));
70+
}
71+
72+
return includeMembers;
73+
}
74+
75+
private static SimpleLambdaExpressionSyntax BuildLambdaExpression(IEnumerable<InvocationExpressionSyntax> declarations)
76+
{
77+
var srcProperty =
78+
ForMemberAnalyzer.GetLambdaExpressions(declarations.ElementAt(0)).srcExpression as SimpleLambdaExpressionSyntax;
79+
var srcCall = srcProperty.ExpressionBody.ToFullString();
80+
srcCall = srcCall.Substring(0, srcCall.LastIndexOf('.'));
81+
var srcName = srcProperty.Parameter.Identifier.Text;
82+
83+
return SyntaxFactory.SimpleLambdaExpression(SyntaxFactory.Parameter(SyntaxFactory.ParseToken(srcName)),
84+
SyntaxFactory.ParseExpression(srcCall));
85+
}
86+
}

src/AutoMapper.Analyzers.Common.CodeFixes/NullSubstituteCodeFixProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private bool IsComplexMapping(InvocationExpressionSyntax invocation, out string
7575
srcProperty = string.Empty;
7676

7777
var syntaxNode = invocation.DescendantNodes()
78-
.FirstOrDefault(n => n is ConditionalExpressionSyntax || n is BinaryExpressionSyntax);
78+
.FirstOrDefault(n => n is ConditionalExpressionSyntax or BinaryExpressionSyntax);
7979
if (syntaxNode is ConditionalExpressionSyntax { Condition: BinaryExpressionSyntax binaryCondition } conditional)
8080
{
8181
var srcMember = binaryCondition.Left.ToString();
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Linq;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace AutoMapper.Analyzers.Common;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public class FlattingComplexModelAnalyzer : ForMemberAnalyzer
10+
{
11+
public const string DiagnosticId = "AMA0005";
12+
13+
protected override string InternalDiagnosticId => DiagnosticId;
14+
15+
protected override Diagnostic AnalyzeMapFrom(LambdaExpressionSyntax destExpression,
16+
LambdaExpressionSyntax srcExpression)
17+
{
18+
if (TryGetExpressionMemberName(srcExpression, out IdentifierNameSyntax srcName) &&
19+
TryGetExpressionMemberName(destExpression, out string destName) &&
20+
srcName.ToString().Equals(destName) && srcName.Parent is MemberAccessExpressionSyntax srcMember)
21+
{
22+
var srcMemberCall = srcMember.Expression.ToFullString();
23+
var nextMembers = ForMember.DescendantNodes().OfType<InvocationExpressionSyntax>().Where(IsForMember).ToList();
24+
var prevMembers = ForMember.Ancestors().OfType<InvocationExpressionSyntax>().Where(IsForMember);
25+
if (!prevMembers.Any(s => IsSameComplexFlatting(s, srcMemberCall)) &&
26+
nextMembers.Any(s => IsSameComplexFlatting(s, srcMemberCall)))
27+
{
28+
return Diagnostic.Create(Rule, ForMember.ArgumentList.GetLocation(), nextMembers.Select(m => m.ArgumentList.GetLocation()).ToList(),ProfileName, MapName);
29+
}
30+
}
31+
32+
return base.AnalyzeMapFrom(destExpression, srcExpression);
33+
}
34+
35+
private bool IsSameComplexFlatting(InvocationExpressionSyntax syntax, string smelledMemberCall) =>
36+
TryGetExpressionMemberName(GetLambdaExpressions(syntax).srcExpression,
37+
out IdentifierNameSyntax srcSyntax)
38+
&& srcSyntax.Parent is MemberAccessExpressionSyntax srcAccess &&
39+
srcAccess.Expression.ToFullString().Equals(smelledMemberCall);
40+
41+
private bool IsForMember(InvocationExpressionSyntax syntax)
42+
{
43+
if (syntax.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name.Identifier.Text.Equals(nameof(IMappingExpression.ForMember)))
44+
{
45+
var (descExpression, srcExpression) = GetLambdaExpressions(syntax);
46+
return TryGetExpressionMemberName(descExpression, out string destName) &&
47+
TryGetExpressionMemberName(srcExpression, out string srcName)
48+
&& destName.Equals(srcName);
49+
}
50+
51+
return false;
52+
}
53+
}

src/AutoMapper.Analyzers.Common/ForMemberAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected override Diagnostic AnalyzeInvocationOperation(IInvocationOperation in
2929
return base.AnalyzeInvocationOperation(invocationOperation);
3030
}
3131

32-
protected (LambdaExpressionSyntax descExpression, LambdaExpressionSyntax srcExpression) GetLambdaExpressions(InvocationExpressionSyntax forMember)
32+
public static (LambdaExpressionSyntax descExpression, LambdaExpressionSyntax srcExpression) GetLambdaExpressions(InvocationExpressionSyntax forMember)
3333
{
3434
var destExpression = forMember.ArgumentList.Arguments[0].Expression as LambdaExpressionSyntax;
3535
var optExpression = forMember.ArgumentList.Arguments[1].Expression as LambdaExpressionSyntax;

src/AutoMapper.Analyzers.Common/RulesResources.Designer.cs

Lines changed: 28 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)