Skip to content

Commit

Permalink
MA0020 suggests Exists and TrueForAll (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
meziantou authored Mar 18, 2023
1 parent 46f9027 commit dc1c9b0
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 5 deletions.
66 changes: 66 additions & 0 deletions src/Meziantou.Analyzer/Rules/OptimizeLinqUsageAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ public void AnalyzeInvocation(OperationAnalysisContext context)
return;

UseFindInsteadOfFirstOrDefault(context, operation);
UseTrueForAllInsteadOfAll(context, operation);
UseExistsInsteadOfAny(context, operation);
UseCountPropertyInsteadOfMethod(context, operation);
UseIndexerInsteadOfElementAt(context, operation);
CombineWhereWithNextMethod(context, operation);
Expand Down Expand Up @@ -287,6 +289,70 @@ private void UseFindInsteadOfFirstOrDefault(OperationAnalysisContext context, II
context.ReportDiagnostic(s_listMethodsRule, properties, operation, DiagnosticReportOptions.ReportOnMethodName, "Find()", operation.TargetMethod.Name);
}
}

private void UseTrueForAllInsteadOfAll(OperationAnalysisContext context, IInvocationOperation operation)
{
if (operation.TargetMethod.Name != nameof(Enumerable.All))
return;

if (operation.Arguments.Length != 2)
return;

var firstArgumentType = operation.Arguments[0].Value.GetActualType();
if (firstArgumentType == null)
return;

if (firstArgumentType.OriginalDefinition.IsEqualTo(ListOfTSymbol))
{
ImmutableDictionary<string, string?> properties;
var predicateArgument = operation.Arguments[1].Value;
if (predicateArgument is IDelegateCreationOperation)
{
properties = CreateProperties(OptimizeLinqUsageData.UseTrueForAllMethod);
}
else
{
if (!context.Options.GetConfigurationValue(operation, s_listMethodsRule.Id + ".report_when_conversion_needed", defaultValue: false))
return;

properties = CreateProperties(OptimizeLinqUsageData.UseTrueForAllMethodWithConversion);
}

context.ReportDiagnostic(s_listMethodsRule, properties, operation, DiagnosticReportOptions.ReportOnMethodName, "TrueForAll()", operation.TargetMethod.Name);
}
}

private void UseExistsInsteadOfAny(OperationAnalysisContext context, IInvocationOperation operation)
{
if (operation.TargetMethod.Name != nameof(Enumerable.Any))
return;

if (operation.Arguments.Length != 2)
return;

var firstArgumentType = operation.Arguments[0].Value.GetActualType();
if (firstArgumentType == null)
return;

if (firstArgumentType.OriginalDefinition.IsEqualTo(ListOfTSymbol))
{
ImmutableDictionary<string, string?> properties;
var predicateArgument = operation.Arguments[1].Value;
if (predicateArgument is IDelegateCreationOperation)
{
properties = CreateProperties(OptimizeLinqUsageData.UseExistsMethod);
}
else
{
if (!context.Options.GetConfigurationValue(operation, s_listMethodsRule.Id + ".report_when_conversion_needed", defaultValue: false))
return;

properties = CreateProperties(OptimizeLinqUsageData.UseExistsMethodWithConversion);
}

context.ReportDiagnostic(s_listMethodsRule, properties, operation, DiagnosticReportOptions.ReportOnMethodName, "Exists()", operation.TargetMethod.Name);
}
}

private void UseIndexerInsteadOfElementAt(OperationAnalysisContext context, IInvocationOperation operation)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Meziantou.Analyzer/Rules/OptimizeLinqUsageData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ internal enum OptimizeLinqUsageData
UseSkipAndNotAny,
UseSkipAndAny,
UseCastInsteadOfSelect,
UseTrueForAllMethod,
UseTrueForAllMethodWithConversion,
UseExistsMethod,
UseExistsMethodWithConversion,
}
30 changes: 25 additions & 5 deletions src/Meziantou.Analyzer/Rules/OptimizeLinqUsageFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,27 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
break;

case OptimizeLinqUsageData.UseFindMethod:
context.RegisterCodeFix(CodeAction.Create(title, ct => UseFindMethod(context.Document, nodeToFix, convertPredicate: false, ct), equivalenceKey: title), context.Diagnostics);
context.RegisterCodeFix(CodeAction.Create(title, ct => UseListMethod(context.Document, nodeToFix, "Find", convertPredicate: false, ct), equivalenceKey: title), context.Diagnostics);
break;

case OptimizeLinqUsageData.UseFindMethodWithConversion:
context.RegisterCodeFix(CodeAction.Create(title, ct => UseFindMethod(context.Document, nodeToFix, convertPredicate: true, ct), equivalenceKey: title), context.Diagnostics);
context.RegisterCodeFix(CodeAction.Create(title, ct => UseListMethod(context.Document, nodeToFix, "Find", convertPredicate: true, ct), equivalenceKey: title), context.Diagnostics);
break;

case OptimizeLinqUsageData.UseTrueForAllMethod:
context.RegisterCodeFix(CodeAction.Create(title, ct => UseListMethod(context.Document, nodeToFix, "TrueForAll", convertPredicate: false, ct), equivalenceKey: title), context.Diagnostics);
break;

case OptimizeLinqUsageData.UseTrueForAllMethodWithConversion:
context.RegisterCodeFix(CodeAction.Create(title, ct => UseListMethod(context.Document, nodeToFix, "TrueForAll", convertPredicate: true, ct), equivalenceKey: title), context.Diagnostics);
break;

case OptimizeLinqUsageData.UseExistsMethod:
context.RegisterCodeFix(CodeAction.Create(title, ct => UseListMethod(context.Document, nodeToFix, "Exists", convertPredicate: false, ct), equivalenceKey: title), context.Diagnostics);
break;

case OptimizeLinqUsageData.UseExistsMethodWithConversion:
context.RegisterCodeFix(CodeAction.Create(title, ct => UseListMethod(context.Document, nodeToFix, "Exists", convertPredicate: true, ct), equivalenceKey: title), context.Diagnostics);
break;

case OptimizeLinqUsageData.UseIndexer:
Expand Down Expand Up @@ -389,20 +405,24 @@ private async static Task<Document> UseCountProperty(Document document, SyntaxNo
return editor.GetChangedDocument();
}

private async static Task<Document> UseFindMethod(Document document, SyntaxNode nodeToFix, bool convertPredicate, CancellationToken cancellationToken)
private async static Task<Document> UseListMethod(Document document, SyntaxNode nodeToFix, string methodName, bool convertPredicate, CancellationToken cancellationToken)
{
var expression = GetMemberAccessExpression(nodeToFix);
if (expression == null)
return document;

var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

var newExpression = expression.WithName(IdentifierName("Find"));
var newExpression = expression.WithName(IdentifierName(methodName));
editor.ReplaceNode(expression, newExpression);
if (convertPredicate)
{
var compilation = editor.SemanticModel.Compilation;
var type = editor.SemanticModel.GetTypeInfo(nodeToFix, cancellationToken).Type;
var symbol = editor.SemanticModel.GetSymbolInfo(nodeToFix, cancellationToken: cancellationToken).Symbol as IMethodSymbol;
if(symbol == null || symbol.TypeArguments.Length != 1)
return document;

var type = symbol.TypeArguments[0];
if (type != null)
{
var predicateType = compilation.GetBestTypeByMetadataName("System.Predicate`1")?.Construct(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,182 @@ await CreateProjectBuilder()
.ShouldFixCodeWith(CodeFix)
.ValidateAsync();
}

[Fact]
public async Task TrueForAll()
{
const string SourceCode = @"using System.Linq;
class Test
{
public Test()
{
var enumerable = System.Linq.Enumerable.Empty<int>();
var list = new System.Collections.Generic.List<int>();
list.[|All|](x => x == 0);
enumerable.All(x => x == 0);
}
}
";
const string CodeFix = @"using System.Linq;
class Test
{
public Test()
{
var enumerable = System.Linq.Enumerable.Empty<int>();
var list = new System.Collections.Generic.List<int>();
list.TrueForAll(x => x == 0);
enumerable.All(x => x == 0);
}
}
";

await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ShouldReportDiagnosticWithMessage("Use 'TrueForAll()' instead of 'All()'")
.ShouldFixCodeWith(CodeFix)
.ValidateAsync();
}

[Fact]
public async Task TrueForAll_Cast()
{
const string SourceCode = @"using System.Linq;
class Test
{
public Test()
{
var list = new System.Collections.Generic.List<int>();
System.Func<int, bool> predicate = _ => true;
list.All(predicate);
}
}
";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task TrueForAll_Cast_ConfigureEnabled()
{
const string SourceCode = @"using System.Linq;
class Test
{
public Test()
{
var list = new System.Collections.Generic.List<int>();
System.Func<int, bool> predicate = _ => true;
list.[|All|](predicate);
}
}
";
const string CodeFix = @"using System.Linq;
class Test
{
public Test()
{
var list = new System.Collections.Generic.List<int>();
System.Func<int, bool> predicate = _ => true;
list.TrueForAll(new System.Predicate<int>(predicate));
}
}
";

await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.AddAnalyzerConfiguration("MA0020.report_when_conversion_needed", "true")
.ShouldReportDiagnosticWithMessage("Use 'TrueForAll()' instead of 'All()'")
.ShouldFixCodeWith(CodeFix)
.ValidateAsync();
}

[Fact]
public async Task Exists()
{
const string SourceCode = @"using System.Linq;
class Test
{
public Test()
{
var enumerable = System.Linq.Enumerable.Empty<int>();
var list = new System.Collections.Generic.List<int>();
list.[|Any|](x => x == 0);
enumerable.Any(x => x == 0);
}
}
";
const string CodeFix = @"using System.Linq;
class Test
{
public Test()
{
var enumerable = System.Linq.Enumerable.Empty<int>();
var list = new System.Collections.Generic.List<int>();
list.Exists(x => x == 0);
enumerable.Any(x => x == 0);
}
}
";

await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ShouldReportDiagnosticWithMessage("Use 'Exists()' instead of 'Any()'")
.ShouldFixCodeWith(CodeFix)
.ValidateAsync();
}

[Fact]
public async Task Exists_Cast()
{
const string SourceCode = @"using System.Linq;
class Test
{
public Test()
{
var list = new System.Collections.Generic.List<int>();
System.Func<int, bool> predicate = _ => true;
list.Any(predicate);
}
}
";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.ValidateAsync();
}

[Fact]
public async Task Exists_Cast_ConfigureEnabled()
{
const string SourceCode = @"using System.Linq;
class Test
{
public Test()
{
var list = new System.Collections.Generic.List<int>();
System.Func<int, bool> predicate = _ => true;
list.[|Any|](predicate);
}
}
";
const string CodeFix = @"using System.Linq;
class Test
{
public Test()
{
var list = new System.Collections.Generic.List<int>();
System.Func<int, bool> predicate = _ => true;
list.Exists(new System.Predicate<int>(predicate));
}
}
";

await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.AddAnalyzerConfiguration("MA0020.report_when_conversion_needed", "true")
.ShouldReportDiagnosticWithMessage("Use 'Exists()' instead of 'Any()'")
.ShouldFixCodeWith(CodeFix)
.ValidateAsync();
}

[Fact]
public async Task Count_IEnumerableAsync()
Expand Down

0 comments on commit dc1c9b0

Please sign in to comment.