From 8b46a417179892238743408ae7ed7d37203054dd Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Aug 2023 22:13:03 +0000 Subject: [PATCH 01/11] orderByMinMax --- .../OptimizeLinqMethodCallCodeFixProvider.cs | 11 ++++++- .../Analysis/InvocationExpressionAnalyzer.cs | 2 +- .../OptimizeLinqMethodCallAnalysis.cs | 31 ++++++++++++++++++- src/Core/SymbolUtility.cs | 14 +++++++++ .../RCS1077OptimizeLinqMethodCallTests.cs | 23 ++++++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs index aad0fe3874..9d9957a253 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs @@ -332,8 +332,17 @@ private static Task SimplifyLinqMethodChainAsync( InvocationExpressionSyntax invocation = invocationInfo.InvocationExpression; InvocationExpressionSyntax invocation2 = invocationInfo2.InvocationExpression; + SimpleNameSyntax name = (invocationInfo2.NameText, invocationInfo.NameText) switch + { + ("OrderBy", "Min") => (SimpleNameSyntax)ParseName("MinBy"), + ("OrderBy", "Max") => (SimpleNameSyntax)ParseName("MaxBy"), + ("OrderByDescending", "Min") => (SimpleNameSyntax)ParseName("MaxBy"), + ("OrderByDescending", "Max") => (SimpleNameSyntax)ParseName("MinBy"), + _ => invocationInfo.Name + }; + InvocationExpressionSyntax newNode = invocation2.WithExpression( - invocationInfo2.MemberAccessExpression.WithName(invocationInfo.Name.WithTriviaFrom(invocationInfo2.Name))); + invocationInfo2.MemberAccessExpression.WithName(name.WithTriviaFrom(invocationInfo2.Name))); IEnumerable trivia = invocation.DescendantTrivia(TextSpan.FromBounds(invocation2.Span.End, invocation.Span.End)); diff --git a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs index 076223ee0f..3a2768da9a 100644 --- a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs @@ -137,7 +137,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex case "Min": { if (DiagnosticRules.OptimizeLinqMethodCall.IsEffective(context)) - OptimizeLinqMethodCallAnalysis.AnalyzeSelectAndMinOrMax(context, invocationInfo); + OptimizeLinqMethodCallAnalysis.AnalyzeMinOrMax(context, invocationInfo); break; } diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index e17c798c4a..4b7ff489cf 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -105,7 +105,9 @@ public static void AnalyzeWhere(SyntaxNodeAnalysisContext context, in SimpleMemb } // items.Select(selector).Min/Max() >>> items.Min/Max(selector) - public static void AnalyzeSelectAndMinOrMax( + // items.OrderBy(selector).Min/Max() >>> items.MinBy/MaxBy(selector) + // items.OrderByDescending(selector).Min/Max() >>> items.MaxBy/MinBy(selector) + public static void AnalyzeMinOrMax( SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { @@ -114,6 +116,19 @@ public static void AnalyzeSelectAndMinOrMax( invocationInfo, "Select", Properties.SimplifyLinqMethodChain); + + SimplifyLinqMethodChain( + context, + invocationInfo, + "OrderBy", + Properties.SimplifyLinqMethodChain); + + SimplifyLinqMethodChain( + context, + invocationInfo, + "OrderByDescending", + Properties.SimplifyLinqMethodChain); + } // list.Select(selector).ToList() >>> list.ConvertAll(selector) @@ -187,6 +202,20 @@ private static void SimplifyLinqMethodChain( break; } + case "OrderBy": + { + if (!SymbolUtility.IsLinqOrderBy(methodSymbol2, allowImmutableArrayExtension: true)) + return; + + break; + } + case "OrderByDescending": + { + if (!SymbolUtility.IsLinqOrderByDescending(methodSymbol2, allowImmutableArrayExtension: true)) + return; + + break; + } default: { Debug.Fail(methodName); diff --git a/src/Core/SymbolUtility.cs b/src/Core/SymbolUtility.cs index ffb097f0d2..eca6a3b0b5 100644 --- a/src/Core/SymbolUtility.cs +++ b/src/Core/SymbolUtility.cs @@ -305,6 +305,20 @@ internal static bool IsLinqWhere( return IsLinqExtensionOfIEnumerableOfTWithPredicate(methodSymbol, "Where", parameterCount: 2, allowImmutableArrayExtension: allowImmutableArrayExtension); } + internal static bool IsLinqOrderBy( + IMethodSymbol methodSymbol, + bool allowImmutableArrayExtension = false) + { + return IsLinqExtensionOfIEnumerableOfT(methodSymbol, "OrderBy", parameterCount: 2, allowImmutableArrayExtension: allowImmutableArrayExtension); + } + + internal static bool IsLinqOrderByDescending( + IMethodSymbol methodSymbol, + bool allowImmutableArrayExtension = false) + { + return IsLinqExtensionOfIEnumerableOfT(methodSymbol, "OrderByDescending", parameterCount: 2, allowImmutableArrayExtension: allowImmutableArrayExtension); + } + internal static bool IsLinqWhereWithIndex(IMethodSymbol methodSymbol) { if (!IsLinqExtensionOfIEnumerableOfT(methodSymbol, "Where", parameterCount: 2, allowImmutableArrayExtension: false)) diff --git a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs index 463fee4830..58664b91a1 100644 --- a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs @@ -282,6 +282,29 @@ void M() ", source, expected); } + [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] + [InlineData(@"OrderBy(f => f.Length).Max()", @"MaxBy(f => f.Length)")] + [InlineData(@"OrderBy(f => f.Length).Min()", @"MinBy(f => f.Length)")] + [InlineData(@"OrderByDescending(f => f.Length).Max()", @"MinBy(f => f.Length)")] + [InlineData(@"OrderByDescending(f => f.Length).Min()", @"MaxBy(f => f.Length)")] + public async Task Test_CombineOrderByMinMax(string source, string expected) + { + await VerifyDiagnosticAndFixAsync(@" +using System.Collections.Generic; +using System.Linq; + +class C +{ + string M() + { + var items = new List(); + + return items.[||]; + } +} +", source, expected); + } + [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] [InlineData(@"Where(f => f.StartsWith(""a"")).Any(f => f.StartsWith(""b""))", @"Any(f => f.StartsWith(""a"") && f.StartsWith(""b""))")] [InlineData(@"Where((f) => f.StartsWith(""a"")).Any(f => f.StartsWith(""b""))", @"Any((f) => f.StartsWith(""a"") && f.StartsWith(""b""))")] From 04180e7cb4edcfbdad083c4149d117911c1c1391 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Aug 2023 22:48:02 +0000 Subject: [PATCH 02/11] support more cases for not all to any --- .../SimplifyLogicalNegationCodeFixProvider.cs | 6 +- .../OptimizeLinqMethodCallAnalysis.cs | 24 +++---- .../SimplifyLogicalNegationAnalyzer.cs | 4 +- .../RCS1068SimplifyLogicalNegationTests2.cs | 68 +++++++++++++++++++ 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs index 1536aa5aa3..9f8191ac04 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs @@ -97,9 +97,11 @@ private static ExpressionSyntax GetNewNode(PrefixUnaryExpressionSyntax logicalNo SingleParameterLambdaExpressionInfo lambdaInfo = SyntaxInfo.SingleParameterLambdaExpressionInfo(lambdaExpression); - var logicalNot2 = (PrefixUnaryExpressionSyntax)SimplifyLogicalNegationAnalyzer.GetReturnExpression(lambdaInfo.Body).WalkDownParentheses(); + var logicalNot2 = SimplifyLogicalNegationAnalyzer.GetReturnExpression(lambdaInfo.Body).WalkDownParentheses(); - InvocationExpressionSyntax newNode = invocationExpression.ReplaceNode(logicalNot2, logicalNot2.Operand.WithTriviaFrom(logicalNot2)); + var invertedExperssion = SyntaxLogicalInverter.GetInstance(document).LogicallyInvert(logicalNot2); + + InvocationExpressionSyntax newNode = invocationExpression.ReplaceNode(logicalNot2, invertedExperssion.WithTriviaFrom(logicalNot2)); return SyntaxRefactorings.ChangeInvokedMethodName(newNode, (memberAccessExpression.Name.Identifier.ValueText == "All") ? "Any" : "All"); } diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index 4b7ff489cf..d54bc1142f 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -203,19 +203,19 @@ private static void SimplifyLinqMethodChain( break; } case "OrderBy": - { - if (!SymbolUtility.IsLinqOrderBy(methodSymbol2, allowImmutableArrayExtension: true)) - return; - - break; - } + { + if (!SymbolUtility.IsLinqOrderBy(methodSymbol2, allowImmutableArrayExtension: true)) + return; + + break; + } case "OrderByDescending": - { - if (!SymbolUtility.IsLinqOrderByDescending(methodSymbol2, allowImmutableArrayExtension: true)) - return; - - break; - } + { + if (!SymbolUtility.IsLinqOrderByDescending(methodSymbol2, allowImmutableArrayExtension: true)) + return; + + break; + } default: { Debug.Fail(methodName); diff --git a/src/Analyzers/CSharp/Analysis/SimplifyLogicalNegationAnalyzer.cs b/src/Analyzers/CSharp/Analysis/SimplifyLogicalNegationAnalyzer.cs index c725eb1beb..76807715c5 100644 --- a/src/Analyzers/CSharp/Analysis/SimplifyLogicalNegationAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/SimplifyLogicalNegationAnalyzer.cs @@ -173,9 +173,7 @@ public static void Analyze(SyntaxNodeAnalysisContext context, in SimpleMemberInv if (!lambdaInfo.Success) return; - ExpressionSyntax expression = GetReturnExpression(lambdaInfo.Body)?.WalkDownParentheses(); - - if (expression?.IsKind(SyntaxKind.LogicalNotExpression) != true) + if (GetReturnExpression(lambdaInfo.Body) is null) return; IMethodSymbol methodSymbol = context.SemanticModel.GetReducedExtensionMethodInfo(invocationInfo.InvocationExpression, context.CancellationToken).Symbol; diff --git a/src/Tests/Analyzers.Tests/RCS1068SimplifyLogicalNegationTests2.cs b/src/Tests/Analyzers.Tests/RCS1068SimplifyLogicalNegationTests2.cs index 21b0f43fb2..44f2078c77 100644 --- a/src/Tests/Analyzers.Tests/RCS1068SimplifyLogicalNegationTests2.cs +++ b/src/Tests/Analyzers.Tests/RCS1068SimplifyLogicalNegationTests2.cs @@ -120,6 +120,40 @@ void M() "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.SimplifyLogicalNegation)] + public async Task Test_NotAny4() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Linq; +using System.Collections.Generic; + +class C +{ + void M() + { + bool f1 = false; + var items = new List(); + + f1 = [|!items.Any(i => i % 2 == 0)|]; + } +} +", @" +using System.Linq; +using System.Collections.Generic; + +class C +{ + void M() + { + bool f1 = false; + var items = new List(); + + f1 = items.All(i => i % 2 != 0); + } +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.SimplifyLogicalNegation)] public async Task Test_NotAll() { @@ -225,6 +259,40 @@ void M() f1 = items.Any(s => s.Equals(s)); } } +"); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.SimplifyLogicalNegation)] + public async Task Test_NotAll4() + { + await VerifyDiagnosticAndFixAsync(@" +using System.Linq; +using System.Collections.Generic; + +class C +{ + void M() + { + bool f1 = false; + var items = new List(); + + f1 = [|!items.All(i => i % 2 == 0)|]; + } +} +", @" +using System.Linq; +using System.Collections.Generic; + +class C +{ + void M() + { + bool f1 = false; + var items = new List(); + + f1 = items.Any(i => i % 2 != 0); + } +} "); } } From 60803c529c975f13e154bbbaac5adab9f9cc2cc0 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Aug 2023 22:56:40 +0000 Subject: [PATCH 03/11] change log --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index c5cf396474..aa422af2c4 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add SECURITY.md ([#1147](https://github.com/josefpihrt/roslynator/pull/1147)) +- Support for more linq optimizations ([#1157](https://github.com/josefpihrt/roslynator/pull/1157)) ### Fixed From e0f6014aba3b1146516661f626c7a06f1d4563a4 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Aug 2023 23:24:12 +0000 Subject: [PATCH 04/11] actually get the logic correct --- .../OptimizeLinqMethodCallCodeFixProvider.cs | 6 ++-- .../Analysis/InvocationExpressionAnalyzer.cs | 5 ++- .../OptimizeLinqMethodCallAnalysis.cs | 35 ++++++++++--------- .../RCS1077OptimizeLinqMethodCallTests.cs | 8 ++--- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs index 9d9957a253..2050870fff 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs @@ -334,10 +334,8 @@ private static Task SimplifyLinqMethodChainAsync( SimpleNameSyntax name = (invocationInfo2.NameText, invocationInfo.NameText) switch { - ("OrderBy", "Min") => (SimpleNameSyntax)ParseName("MinBy"), - ("OrderBy", "Max") => (SimpleNameSyntax)ParseName("MaxBy"), - ("OrderByDescending", "Min") => (SimpleNameSyntax)ParseName("MaxBy"), - ("OrderByDescending", "Max") => (SimpleNameSyntax)ParseName("MinBy"), + ("OrderBy", "FirstOrDefault") => (SimpleNameSyntax)ParseName("MinBy"), + ("OrderByDescending", "FirstOrDefault") => (SimpleNameSyntax)ParseName("MaxBy"), _ => invocationInfo.Name }; diff --git a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs index 3a2768da9a..5651a58fff 100644 --- a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs @@ -137,7 +137,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex case "Min": { if (DiagnosticRules.OptimizeLinqMethodCall.IsEffective(context)) - OptimizeLinqMethodCallAnalysis.AnalyzeMinOrMax(context, invocationInfo); + OptimizeLinqMethodCallAnalysis.AnalyzeSelectAndMinOrMax(context, invocationInfo); break; } @@ -184,6 +184,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex { OptimizeLinqMethodCallAnalysis.AnalyzeWhere(context, invocationInfo); OptimizeLinqMethodCallAnalysis.AnalyzeFirstOrDefault(context, invocationInfo); + OptimizeLinqMethodCallAnalysis.AnalyzerOrderByAndFirstOrDefault(context, invocationInfo); } break; @@ -269,7 +270,9 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex case "FirstOrDefault": { if (DiagnosticRules.OptimizeLinqMethodCall.IsEffective(context)) + { OptimizeLinqMethodCallAnalysis.AnalyzeFirstOrDefault(context, invocationInfo); + } break; } diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index d54bc1142f..9d7a43f59a 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -105,9 +105,7 @@ public static void AnalyzeWhere(SyntaxNodeAnalysisContext context, in SimpleMemb } // items.Select(selector).Min/Max() >>> items.Min/Max(selector) - // items.OrderBy(selector).Min/Max() >>> items.MinBy/MaxBy(selector) - // items.OrderByDescending(selector).Min/Max() >>> items.MaxBy/MinBy(selector) - public static void AnalyzeMinOrMax( + public static void AnalyzeSelectAndMinOrMax( SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { @@ -116,19 +114,6 @@ public static void AnalyzeMinOrMax( invocationInfo, "Select", Properties.SimplifyLinqMethodChain); - - SimplifyLinqMethodChain( - context, - invocationInfo, - "OrderBy", - Properties.SimplifyLinqMethodChain); - - SimplifyLinqMethodChain( - context, - invocationInfo, - "OrderByDescending", - Properties.SimplifyLinqMethodChain); - } // list.Select(selector).ToList() >>> list.ConvertAll(selector) @@ -228,6 +213,24 @@ private static void SimplifyLinqMethodChain( Report(context, invocation, span, checkDirectives: true, properties: properties); } + // items.OrderBy(selector).FirstOrDefault() >>> items.MaxBy(selector) + // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) + public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, + in SimpleMemberInvocationExpressionInfo invocationInfo) + { + SimplifyLinqMethodChain( + context, + invocationInfo, + "OrderBy", + Properties.SimplifyLinqMethodChain); + + SimplifyLinqMethodChain( + context, + invocationInfo, + "OrderByDescending", + Properties.SimplifyLinqMethodChain); + } + public static void AnalyzeFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { InvocationExpressionSyntax invocation = invocationInfo.InvocationExpression; diff --git a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs index 58664b91a1..e24f3af497 100644 --- a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs @@ -283,11 +283,9 @@ void M() } [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] - [InlineData(@"OrderBy(f => f.Length).Max()", @"MaxBy(f => f.Length)")] - [InlineData(@"OrderBy(f => f.Length).Min()", @"MinBy(f => f.Length)")] - [InlineData(@"OrderByDescending(f => f.Length).Max()", @"MinBy(f => f.Length)")] - [InlineData(@"OrderByDescending(f => f.Length).Min()", @"MaxBy(f => f.Length)")] - public async Task Test_CombineOrderByMinMax(string source, string expected) + [InlineData(@"OrderBy(f => f.Length).FirstOrDefault()", @"MinBy(f => f.Length)")] + [InlineData(@"OrderByDescending(f => f.Length).FirstOrDefault()", @"MaxBy(f => f.Length)")] + public async Task Test_CombineOrderByFirstOrDefault(string source, string expected) { await VerifyDiagnosticAndFixAsync(@" using System.Collections.Generic; From 467d1f1c458d0be7880dc220531479899fb3c90c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 14 Aug 2023 23:31:28 +0000 Subject: [PATCH 05/11] formatting --- .../CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs | 4 ++-- src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs | 2 -- .../CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs | 3 +-- .../Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs index 9f8191ac04..e5c4d5c431 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/SimplifyLogicalNegationCodeFixProvider.cs @@ -97,9 +97,9 @@ private static ExpressionSyntax GetNewNode(PrefixUnaryExpressionSyntax logicalNo SingleParameterLambdaExpressionInfo lambdaInfo = SyntaxInfo.SingleParameterLambdaExpressionInfo(lambdaExpression); - var logicalNot2 = SimplifyLogicalNegationAnalyzer.GetReturnExpression(lambdaInfo.Body).WalkDownParentheses(); + ExpressionSyntax logicalNot2 = SimplifyLogicalNegationAnalyzer.GetReturnExpression(lambdaInfo.Body).WalkDownParentheses(); - var invertedExperssion = SyntaxLogicalInverter.GetInstance(document).LogicallyInvert(logicalNot2); + ExpressionSyntax invertedExperssion = SyntaxLogicalInverter.GetInstance(document).LogicallyInvert(logicalNot2); InvocationExpressionSyntax newNode = invocationExpression.ReplaceNode(logicalNot2, invertedExperssion.WithTriviaFrom(logicalNot2)); diff --git a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs index 5651a58fff..3e1cde29bb 100644 --- a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs @@ -270,9 +270,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex case "FirstOrDefault": { if (DiagnosticRules.OptimizeLinqMethodCall.IsEffective(context)) - { OptimizeLinqMethodCallAnalysis.AnalyzeFirstOrDefault(context, invocationInfo); - } break; } diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index 9d7a43f59a..1029fff39f 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -215,8 +215,7 @@ private static void SimplifyLinqMethodChain( // items.OrderBy(selector).FirstOrDefault() >>> items.MaxBy(selector) // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) - public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, - in SimpleMemberInvocationExpressionInfo invocationInfo) + public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { SimplifyLinqMethodChain( context, diff --git a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs index e24f3af497..865231a429 100644 --- a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs @@ -283,8 +283,8 @@ void M() } [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] - [InlineData(@"OrderBy(f => f.Length).FirstOrDefault()", @"MinBy(f => f.Length)")] - [InlineData(@"OrderByDescending(f => f.Length).FirstOrDefault()", @"MaxBy(f => f.Length)")] + [InlineData("OrderBy(f => f.Length).FirstOrDefault()", "MinBy(f => f.Length)")] + [InlineData("OrderByDescending(f => f.Length).FirstOrDefault()", "MaxBy(f => f.Length)")] public async Task Test_CombineOrderByFirstOrDefault(string source, string expected) { await VerifyDiagnosticAndFixAsync(@" From 3653b316c7c58e397b3ea2f06d6bfecf317b5692 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Aug 2023 20:17:32 +0000 Subject: [PATCH 06/11] with net6 or 7 check --- .../OptimizeLinqMethodCallAnalysis.cs | 23 ++++++++++++ .../RCS1077OptimizeLinqMethodCallTests.cs | 36 +++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index 1029fff39f..06e23c004b 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -213,10 +213,33 @@ private static void SimplifyLinqMethodChain( Report(context, invocation, span, checkDirectives: true, properties: properties); } + private static bool IsNet6OrGreater(Compilation compilation) + { + var targetFrameworkAttribute = compilation.GetTypeByMetadataName("System.Runtime.Versioning.TargetFrameworkAttribute"); + + if (targetFrameworkAttribute == null) return false; + + foreach (var attr in compilation.Assembly.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(targetFrameworkAttribute,attr.AttributeClass)) + continue; + + if(attr.ConstructorArguments.FirstOrDefault().Value is not string targetFramework) + continue; + + if (targetFramework is ".NETCoreApp,Version=v6.0" or ".NETCoreApp,Version=v7.0") + return true; + } + + return false; + } + // items.OrderBy(selector).FirstOrDefault() >>> items.MaxBy(selector) // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { + if (!IsNet6OrGreater(context.Compilation)) return; + SimplifyLinqMethodChain( context, invocationInfo, diff --git a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs index 865231a429..0d6ea65336 100644 --- a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs @@ -290,17 +290,41 @@ public async Task Test_CombineOrderByFirstOrDefault(string source, string expect await VerifyDiagnosticAndFixAsync(@" using System.Collections.Generic; using System.Linq; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute("".NETCoreApp,Version=v6.0"", FrameworkDisplayName = "".NET 6.0"")] -class C +namespace N { - string M() + class C { - var items = new List(); + string M() + { + var items = new List(); - return items.[||]; + return items.[||]; + } } -} -", source, expected); +}", source, expected); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] + public async Task Test_DoesntCombineOrderByFirstOrDefaultForNetstandard() + { + await VerifyNoDiagnosticAsync(@" +using System.Collections.Generic; +using System.Linq; + +namespace N +{ + class C + { + string M() + { + var items = new List(); + + return items.OrderBy(f => f.Length).FirstOrDefault(); + } + } +}"); } [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] From 689b19df807e2867ad3e92e19f8475560a467e65 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Aug 2023 20:18:04 +0000 Subject: [PATCH 07/11] formatting --- .../CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index 06e23c004b..e8afa7bdb5 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -221,10 +221,10 @@ private static bool IsNet6OrGreater(Compilation compilation) foreach (var attr in compilation.Assembly.GetAttributes()) { - if (!SymbolEqualityComparer.Default.Equals(targetFrameworkAttribute,attr.AttributeClass)) + if (!SymbolEqualityComparer.Default.Equals(targetFrameworkAttribute, attr.AttributeClass)) continue; - if(attr.ConstructorArguments.FirstOrDefault().Value is not string targetFramework) + if (attr.ConstructorArguments.FirstOrDefault().Value is not string targetFramework) continue; if (targetFramework is ".NETCoreApp,Version=v6.0" or ".NETCoreApp,Version=v7.0") From 7b2d465c858b2cad6f5996392d119dcc5291eb21 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 15 Aug 2023 20:31:04 +0000 Subject: [PATCH 08/11] format --- .../CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index e8afa7bdb5..959176b06a 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -217,7 +217,8 @@ private static bool IsNet6OrGreater(Compilation compilation) { var targetFrameworkAttribute = compilation.GetTypeByMetadataName("System.Runtime.Versioning.TargetFrameworkAttribute"); - if (targetFrameworkAttribute == null) return false; + if (targetFrameworkAttribute == null) + return false; foreach (var attr in compilation.Assembly.GetAttributes()) { @@ -238,7 +239,8 @@ private static bool IsNet6OrGreater(Compilation compilation) // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { - if (!IsNet6OrGreater(context.Compilation)) return; + if (!IsNet6OrGreater(context.Compilation)) + return; SimplifyLinqMethodChain( context, From 29cd3ee958daaba0e11c019a3e7a1b7c3068c16b Mon Sep 17 00:00:00 2001 From: james_hargreaves Date: Tue, 15 Aug 2023 21:39:11 +0100 Subject: [PATCH 09/11] trigger GitHub actions From 7b440c766a287b1507fee7389846fef362e048c5 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 16 Aug 2023 19:18:12 +0000 Subject: [PATCH 10/11] CR comments --- .../OptimizeLinqMethodCallAnalysis.cs | 26 +++---------------- .../RCS1077OptimizeLinqMethodCallTests.cs | 24 +---------------- 2 files changed, 4 insertions(+), 46 deletions(-) diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index 959176b06a..b0e3012a7f 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -213,33 +213,13 @@ private static void SimplifyLinqMethodChain( Report(context, invocation, span, checkDirectives: true, properties: properties); } - private static bool IsNet6OrGreater(Compilation compilation) - { - var targetFrameworkAttribute = compilation.GetTypeByMetadataName("System.Runtime.Versioning.TargetFrameworkAttribute"); - - if (targetFrameworkAttribute == null) - return false; - - foreach (var attr in compilation.Assembly.GetAttributes()) - { - if (!SymbolEqualityComparer.Default.Equals(targetFrameworkAttribute, attr.AttributeClass)) - continue; - - if (attr.ConstructorArguments.FirstOrDefault().Value is not string targetFramework) - continue; - - if (targetFramework is ".NETCoreApp,Version=v6.0" or ".NETCoreApp,Version=v7.0") - return true; - } - - return false; - } - // items.OrderBy(selector).FirstOrDefault() >>> items.MaxBy(selector) // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { - if (!IsNet6OrGreater(context.Compilation)) + INamedTypeSymbol enumerableSymbol = context.Compilation.GetTypeByMetadataName("System.Linq.Enumerable"); + + if (enumerableSymbol.FindMember("MinBy") is null) return; SimplifyLinqMethodChain( diff --git a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs index 0d6ea65336..ba595f18a7 100644 --- a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs @@ -290,7 +290,6 @@ public async Task Test_CombineOrderByFirstOrDefault(string source, string expect await VerifyDiagnosticAndFixAsync(@" using System.Collections.Generic; using System.Linq; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute("".NETCoreApp,Version=v6.0"", FrameworkDisplayName = "".NET 6.0"")] namespace N { @@ -305,28 +304,7 @@ string M() } }", source, expected); } - - [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] - public async Task Test_DoesntCombineOrderByFirstOrDefaultForNetstandard() - { - await VerifyNoDiagnosticAsync(@" -using System.Collections.Generic; -using System.Linq; - -namespace N -{ - class C - { - string M() - { - var items = new List(); - - return items.OrderBy(f => f.Length).FirstOrDefault(); - } - } -}"); - } - + [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] [InlineData(@"Where(f => f.StartsWith(""a"")).Any(f => f.StartsWith(""b""))", @"Any(f => f.StartsWith(""a"") && f.StartsWith(""b""))")] [InlineData(@"Where((f) => f.StartsWith(""a"")).Any(f => f.StartsWith(""b""))", @"Any((f) => f.StartsWith(""a"") && f.StartsWith(""b""))")] From 4ce695792411cba21e4ee2e7090f137f172a937c Mon Sep 17 00:00:00 2001 From: James Date: Wed, 16 Aug 2023 22:09:42 +0000 Subject: [PATCH 11/11] with different behaviour based on TSource --- .../OptimizeLinqMethodCallCodeFixProvider.cs | 2 + .../Analysis/InvocationExpressionAnalyzer.cs | 3 +- .../OptimizeLinqMethodCallAnalysis.cs | 92 ++++++++++++++----- .../RCS1077OptimizeLinqMethodCallTests.cs | 67 +++++++++++++- 4 files changed, 140 insertions(+), 24 deletions(-) diff --git a/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs b/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs index 2050870fff..2855cfc598 100644 --- a/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs +++ b/src/Analyzers.CodeFixes/CSharp/CodeFixes/OptimizeLinqMethodCallCodeFixProvider.cs @@ -336,6 +336,8 @@ private static Task SimplifyLinqMethodChainAsync( { ("OrderBy", "FirstOrDefault") => (SimpleNameSyntax)ParseName("MinBy"), ("OrderByDescending", "FirstOrDefault") => (SimpleNameSyntax)ParseName("MaxBy"), + ("OrderBy", "First") => (SimpleNameSyntax)ParseName("MinBy"), + ("OrderByDescending", "First") => (SimpleNameSyntax)ParseName("MaxBy"), _ => invocationInfo.Name }; diff --git a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs index 3e1cde29bb..a0fd63d5f0 100644 --- a/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/InvocationExpressionAnalyzer.cs @@ -129,6 +129,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex OptimizeLinqMethodCallAnalysis.AnalyzeWhere(context, invocationInfo); OptimizeLinqMethodCallAnalysis.AnalyzeFirst(context, invocationInfo); + OptimizeLinqMethodCallAnalysis.AnalyzerOrderByAndFirst(context, invocationInfo, shouldThrowIfEmpty: true); } break; @@ -184,7 +185,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex { OptimizeLinqMethodCallAnalysis.AnalyzeWhere(context, invocationInfo); OptimizeLinqMethodCallAnalysis.AnalyzeFirstOrDefault(context, invocationInfo); - OptimizeLinqMethodCallAnalysis.AnalyzerOrderByAndFirstOrDefault(context, invocationInfo); + OptimizeLinqMethodCallAnalysis.AnalyzerOrderByAndFirst(context, invocationInfo, shouldThrowIfEmpty: false); } break; diff --git a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs index b0e3012a7f..9cc3abb86f 100644 --- a/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs +++ b/src/Analyzers/CSharp/Analysis/OptimizeLinqMethodCallAnalysis.cs @@ -187,6 +187,65 @@ private static void SimplifyLinqMethodChain( break; } + default: + { + Debug.Fail(methodName); + return; + } + } + + TextSpan span = TextSpan.FromBounds(invocationInfo2.Name.SpanStart, invocation.Span.End); + + Report(context, invocation, span, checkDirectives: true, properties: properties); + } + + + // for reference types + // items.OrderBy(selector).FirstOrDefault() >>> items.MaxBy(selector) + // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) + // for value types: + // items.OrderBy(selector).First() >>> items.MaxBy(selector) + // items.OrderByDescending(selector).First() >>> items.MaxBy(selector) + public static void AnalyzerOrderByAndFirst(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo, bool shouldThrowIfEmpty) + { + // MinBy / MaxBy are only supported for net6.0 onwards + INamedTypeSymbol enumerableSymbol = context.Compilation.GetTypeByMetadataName("System.Linq.Enumerable"); + + if (enumerableSymbol.FindMember("MinBy") is null) + return; + + SimpleMemberInvocationExpressionInfo previousInvocationInfo = SyntaxInfo.SimpleMemberInvocationExpressionInfo(invocationInfo.Expression); + + if (!previousInvocationInfo.Success) + return; + + if (previousInvocationInfo.Arguments.Count != 1) + return; + + if (previousInvocationInfo.NameText != "OrderBy" && previousInvocationInfo.NameText != "OrderByDescending") + return; + + InvocationExpressionSyntax invocation = invocationInfo.InvocationExpression; + + SemanticModel semanticModel = context.SemanticModel; + CancellationToken cancellationToken = context.CancellationToken; + + IMethodSymbol methodSymbol = semanticModel.GetExtensionMethodInfo(invocation, cancellationToken).Symbol; + + if (methodSymbol is null) + return; + + if (!SymbolUtility.IsLinqExtensionOfIEnumerableOfTWithoutParameters(methodSymbol, invocationInfo.NameText)) + return; + + IMethodSymbol methodSymbol2 = semanticModel.GetExtensionMethodInfo(previousInvocationInfo.InvocationExpression, cancellationToken).Symbol; + + if (methodSymbol2 is null) + return; + + + switch (previousInvocationInfo.NameText) + { case "OrderBy": { if (!SymbolUtility.IsLinqOrderBy(methodSymbol2, allowImmutableArrayExtension: true)) @@ -203,38 +262,27 @@ private static void SimplifyLinqMethodChain( } default: { - Debug.Fail(methodName); - return; + throw new InvalidOperationException(); } } - TextSpan span = TextSpan.FromBounds(invocationInfo2.Name.SpanStart, invocation.Span.End); - - Report(context, invocation, span, checkDirectives: true, properties: properties); - } + // First throws if no values found. MaxBy/MinBy match this behaviour if TSource is a not reference type. + var lambda = previousInvocationInfo.InvocationExpression.ArgumentList.Arguments[0].Expression; + var delegateType = semanticModel.GetTypeInfo(lambda).ConvertedType; + if (delegateType is not INamedTypeSymbol { TypeKind: TypeKind.Delegate } namedDelegateType) + return; - // items.OrderBy(selector).FirstOrDefault() >>> items.MaxBy(selector) - // items.OrderByDescending(selector).FirstOrDefault() >>> items.MaxBy(selector) - public static void AnalyzerOrderByAndFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) - { - INamedTypeSymbol enumerableSymbol = context.Compilation.GetTypeByMetadataName("System.Linq.Enumerable"); + var tSource = namedDelegateType.TypeArguments.First(); - if (enumerableSymbol.FindMember("MinBy") is null) + if (tSource.IsReferenceType == shouldThrowIfEmpty) return; - SimplifyLinqMethodChain( - context, - invocationInfo, - "OrderBy", - Properties.SimplifyLinqMethodChain); + TextSpan span = TextSpan.FromBounds(previousInvocationInfo.Name.SpanStart, invocation.Span.End); - SimplifyLinqMethodChain( - context, - invocationInfo, - "OrderByDescending", - Properties.SimplifyLinqMethodChain); + Report(context, invocation, span, checkDirectives: true, properties: Properties.SimplifyLinqMethodChain); } + public static void AnalyzeFirstOrDefault(SyntaxNodeAnalysisContext context, in SimpleMemberInvocationExpressionInfo invocationInfo) { InvocationExpressionSyntax invocation = invocationInfo.InvocationExpression; diff --git a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs index ba595f18a7..28433c8638 100644 --- a/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1077OptimizeLinqMethodCallTests.cs @@ -304,7 +304,72 @@ string M() } }", source, expected); } - + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] + public async Task Test_CombineOrderByFirstOrDefault_NoDiagnosticIfTsourceIsValueType() + { + await VerifyNoDiagnosticAsync(@" +using System.Collections.Generic; +using System.Linq; + +namespace N +{ + class C + { + void M() + { + var items = new List(); + + var y = items.OrderBy(x=>x).FirstOrDefault(); + } + } +}"); + } + + [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] + [InlineData("OrderBy(f => f).First()", "MinBy(f => f)")] + [InlineData("OrderByDescending(f => f).First()", "MaxBy(f => f)")] + public async Task Test_CombineOrderByFirst(string source, string expected) + { + await VerifyDiagnosticAndFixAsync(@" +using System.Collections.Generic; +using System.Linq; + +namespace N +{ + class C + { + int M() + { + var items = new List(); + + return items.[||]; + } + } +}", source, expected); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] + public async Task Test_CombineOrderByFirst_NoDiagnosticIfTsourceIsReferenceType() + { + await VerifyNoDiagnosticAsync(@" +using System.Collections.Generic; +using System.Linq; + +namespace N +{ + class C + { + void M() + { + var items = new List(); + + var y = items.OrderBy(x=>x.Length).First(); + } + } +}"); + } + [Theory, Trait(Traits.Analyzer, DiagnosticIdentifiers.OptimizeLinqMethodCall)] [InlineData(@"Where(f => f.StartsWith(""a"")).Any(f => f.StartsWith(""b""))", @"Any(f => f.StartsWith(""a"") && f.StartsWith(""b""))")] [InlineData(@"Where((f) => f.StartsWith(""a"")).Any(f => f.StartsWith(""b""))", @"Any((f) => f.StartsWith(""a"") && f.StartsWith(""b""))")]