-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
745 additions
and
45 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
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 |
---|---|---|
|
@@ -5,4 +5,4 @@ | |
<package pattern="*" /> | ||
</packageSource> | ||
</packageSourceMapping> | ||
</configuration> | ||
</configuration> |
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
5 changes: 3 additions & 2 deletions
5
.../EC86_GCCollectShouldNotBeCalled.Fixer.cs → .../EC86.GCCollectShouldNotBeCalled.Fixer.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
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
125 changes: 125 additions & 0 deletions
125
src/EcoCode.Core/Analyzers/EC87.UseListIndexer.Fixer.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,125 @@ | ||
using System.Linq; | ||
using System.Reflection; | ||
|
||
namespace EcoCode.Analyzers; | ||
|
||
/// <summary>EC87 fixer: Use list indexer.</summary> | ||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseListIndexerFixer)), Shared] | ||
public sealed class UseListIndexerFixer : CodeFixProvider | ||
{ | ||
/// <inheritdoc/> | ||
public override ImmutableArray<string> FixableDiagnosticIds => _fixableDiagnosticIds; | ||
private static readonly ImmutableArray<string> _fixableDiagnosticIds = [UseListIndexer.Descriptor.Id]; | ||
|
||
/// <inheritdoc/> | ||
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; | ||
|
||
/// <inheritdoc/> | ||
public override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
if (root is null) return; | ||
|
||
foreach (var diagnostic in context.Diagnostics) | ||
{ | ||
var parent = root.FindToken(diagnostic.Location.SourceSpan.Start).Parent; | ||
if (parent is null) continue; | ||
|
||
foreach (var node in parent.AncestorsAndSelf()) | ||
{ | ||
if (node is not InvocationExpressionSyntax invocation || invocation.Expression is not MemberAccessExpressionSyntax memberAccess) | ||
continue; | ||
|
||
var refactorFunc = memberAccess.Name.Identifier.ValueText switch | ||
{ | ||
nameof(Enumerable.First) => RefactorFirstAsync, | ||
nameof(Enumerable.Last) => | ||
context.Document.GetLanguageVersion() >= LanguageVersion.CSharp8 | ||
? RefactorLastWithIndexAsync | ||
: RefactorLastWithCountOrLengthAsync, | ||
nameof(Enumerable.ElementAt) => RefactorElementAtAsync, | ||
_ => default(Func<Document, InvocationExpressionSyntax, CancellationToken, Task<Document>>), | ||
}; | ||
|
||
if (refactorFunc is null) continue; | ||
context.RegisterCodeFix( | ||
CodeAction.Create( | ||
title: "Use collection indexer", | ||
createChangedDocument: token => refactorFunc(context.Document, invocation, token), | ||
equivalenceKey: "Use collection indexer"), | ||
diagnostic); | ||
break; | ||
} | ||
} | ||
} | ||
|
||
private static async Task<Document> UpdateDocument(Document document, InvocationExpressionSyntax invocation, ExpressionSyntax indexExpr, CancellationToken token) | ||
{ | ||
if (await document.GetSyntaxRootAsync(token) is not SyntaxNode root) | ||
return document; | ||
|
||
var elementAccess = SyntaxFactory.ElementAccessExpression( | ||
((MemberAccessExpressionSyntax)invocation.Expression).Expression, | ||
SyntaxFactory.BracketedArgumentList( | ||
SyntaxFactory.SingletonSeparatedList( | ||
SyntaxFactory.Argument(indexExpr)))); | ||
|
||
return document.WithSyntaxRoot(root.ReplaceNode(invocation, elementAccess)); | ||
} | ||
|
||
private static Task<Document> RefactorFirstAsync(Document document, InvocationExpressionSyntax invocationExpr, CancellationToken token) => | ||
UpdateDocument(document, invocationExpr, SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0)), token); | ||
|
||
private static Task<Document> RefactorLastWithIndexAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken token) => | ||
UpdateDocument(document, invocation, | ||
SyntaxFactory.PrefixUnaryExpression(SyntaxKind.IndexExpression, | ||
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(1))), | ||
token); | ||
|
||
private static async Task<Document> RefactorLastWithCountOrLengthAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken token) | ||
{ | ||
if (await document.GetSemanticModelAsync(token).ConfigureAwait(false) is not { } semanticModel) | ||
return document; | ||
|
||
var memberAccess = (MemberAccessExpressionSyntax)invocation.Expression; | ||
if (semanticModel.GetTypeInfo(memberAccess.Expression, token).Type is not { } memberType || | ||
GetCountOrLength(memberType, invocation.SpanStart, semanticModel) is not { } countOrLength) | ||
{ | ||
return document; | ||
} | ||
|
||
var property = SyntaxFactory.MemberAccessExpression( | ||
SyntaxKind.SimpleMemberAccessExpression, | ||
memberAccess.Expression, | ||
SyntaxFactory.IdentifierName(countOrLength.Name)); | ||
|
||
var oneLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(1)); | ||
|
||
var indexExpression = SyntaxFactory.BinaryExpression(SyntaxKind.SubtractExpression, property, oneLiteral); | ||
|
||
return await UpdateDocument(document, invocation, indexExpression, token); | ||
|
||
static ISymbol? GetCountOrLength(ITypeSymbol type, int position, SemanticModel semanticModel) | ||
{ | ||
do | ||
{ | ||
foreach (var member in type.GetMembers()) | ||
{ | ||
if (member.Name is nameof(IReadOnlyList<int>.Count) or nameof(Array.Length) && | ||
member is IPropertySymbol prop && | ||
prop.Type.IsPrimitiveNumber() && | ||
semanticModel.IsAccessible(position, prop)) | ||
{ | ||
return prop; | ||
} | ||
} | ||
type = type.BaseType!; | ||
} while (type is not null); | ||
|
||
return null; | ||
} | ||
} | ||
|
||
private static Task<Document> RefactorElementAtAsync(Document document, InvocationExpressionSyntax invocation, CancellationToken token) => | ||
UpdateDocument(document, invocation, invocation.ArgumentList.Arguments[0].Expression, token); | ||
} |
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,81 @@ | ||
using System.Collections; | ||
using System.Linq; | ||
|
||
namespace EcoCode.Analyzers; | ||
|
||
/// <summary>EC87: Use list indexer.</summary> | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
public sealed class UseListIndexer : DiagnosticAnalyzer | ||
{ | ||
private static readonly ImmutableArray<SyntaxKind> SyntaxKinds = [SyntaxKind.InvocationExpression]; | ||
|
||
/// <summary>The diagnostic descriptor.</summary> | ||
public static DiagnosticDescriptor Descriptor { get; } = new( | ||
Rule.Ids.EC87_UseCollectionIndexer, | ||
title: "Use list indexer", | ||
messageFormat: "A list indexer should be used instead of a Linq method", | ||
Rule.Categories.Performance, | ||
DiagnosticSeverity.Warning, | ||
isEnabledByDefault: true, | ||
description: "Collections that implement IList, IList<T> or IReadOnlyList<T>, should use their indexers instead of Linq methods for improved performance.", | ||
helpLinkUri: Rule.GetHelpUri(Rule.Ids.EC87_UseCollectionIndexer)); | ||
|
||
/// <inheritdoc/> | ||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => _supportedDiagnostics; | ||
private static readonly ImmutableArray<DiagnosticDescriptor> _supportedDiagnostics = [Descriptor]; | ||
|
||
/// <inheritdoc/> | ||
public override void Initialize(AnalysisContext context) | ||
{ | ||
context.EnableConcurrentExecution(); | ||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
context.RegisterSyntaxNodeAction(static context => AnalyzeInvocationExpression(context), SyntaxKinds); | ||
} | ||
|
||
// TODO: analysis can be improved by including scenarios with method chains | ||
// For example: myList.Skip(5).First() should be refactored to myList[5] | ||
|
||
private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext context) | ||
{ | ||
var invocationExpr = (InvocationExpressionSyntax)context.Node; | ||
var memberAccess = (MemberAccessExpressionSyntax)invocationExpr.Expression; | ||
|
||
if (context.SemanticModel.GetSymbolInfo(invocationExpr).Symbol is not IMethodSymbol method || | ||
!method.IsExtensionMethod || | ||
!SymbolEqualityComparer.Default.Equals(method.ContainingType, context.Compilation.GetTypeByMetadataName(typeof(Enumerable).FullName))) | ||
{ | ||
return; | ||
} | ||
|
||
bool report = method.Name switch | ||
{ | ||
nameof(Enumerable.First) => method.Parameters.Length == 0, | ||
nameof(Enumerable.Last) => method.Parameters.Length == 0, | ||
nameof(Enumerable.ElementAt) => method.Parameters.Length == 1, | ||
_ => false, | ||
}; | ||
|
||
if (report && IsList(context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type, context.Compilation)) | ||
context.ReportDiagnostic(Diagnostic.Create(Descriptor, memberAccess.GetLocation())); | ||
|
||
static bool IsList(ITypeSymbol? type, Compilation compilation) | ||
{ | ||
if (type is null) return false; | ||
|
||
var iReadOnlyListT = compilation.GetTypeByMetadataName(typeof(IReadOnlyList<>).FullName); | ||
var iListT = compilation.GetTypeByMetadataName(typeof(IList<>).FullName); | ||
var iList = compilation.GetTypeByMetadataName(typeof(IList).FullName); | ||
|
||
foreach (var iface in type.AllInterfaces) | ||
{ | ||
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iReadOnlyListT) || | ||
SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iListT) || | ||
SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iList)) | ||
{ | ||
return true; | ||
} | ||
} | ||
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
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,11 @@ | ||
namespace EcoCode.Extensions; | ||
|
||
/// <summary>Extension methods for <see cref="Document"/>.</summary> | ||
public static class DocumentExtensions | ||
{ | ||
/// <summary>Returns the language version of the document.</summary> | ||
/// <param name="document">The document.</param> | ||
/// <returns>The language version.</returns> | ||
public static LanguageVersion GetLanguageVersion(this Document document) => | ||
document.Project.ParseOptions is CSharpParseOptions options ? options.LanguageVersion : LanguageVersion.Latest; | ||
} |
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,21 @@ | ||
namespace EcoCode.Extensions; | ||
|
||
/// <summary>Extensions methods for <see cref="IMethodSymbol"/>.</summary> | ||
public static class MethodSymbolExtensions | ||
{ | ||
/// <summary>Returns whether the method is a LINQ extension method.</summary> | ||
/// <param name="methodSymbol">The method symbol.</param> | ||
/// <param name="compilation">The compilation.</param> | ||
/// <returns>True if the method is a LINQ extension method, false otherwise.</returns> | ||
public static bool IsLinqMethod(this IMethodSymbol methodSymbol, Compilation compilation) => | ||
methodSymbol.IsExtensionMethod && | ||
SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, compilation.GetLinqEnumerableSymbol()); | ||
|
||
/// <summary>Returns whether the method is a LINQ extension method.</summary> | ||
/// <param name="methodSymbol">The method symbol.</param> | ||
/// <param name="linqEnumerableSymbol">The LINQ Enumerable symbol.</param> | ||
/// <returns>True if the method is a LINQ extension method, false otherwise.</returns> | ||
public static bool IsLinqMethod(this IMethodSymbol methodSymbol, INamedTypeSymbol? linqEnumerableSymbol) => | ||
methodSymbol.IsExtensionMethod && | ||
SymbolEqualityComparer.Default.Equals(methodSymbol.ContainingType, linqEnumerableSymbol); | ||
} |
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,17 @@ | ||
namespace EcoCode.Extensions; | ||
|
||
internal static class TypeSymbolExtensions | ||
{ | ||
/// <summary>Returns whether the type is a primitive number type.</summary> | ||
/// <param name="type">The type.</param> | ||
/// <returns>True if the type is a primitive number type, false otherwise.</returns> | ||
public static bool IsPrimitiveNumber(this ITypeSymbol type) => type.SpecialType is | ||
SpecialType.System_Int32 or | ||
SpecialType.System_Int64 or | ||
SpecialType.System_UInt32 or | ||
SpecialType.System_UInt64 or | ||
SpecialType.System_Int16 or | ||
SpecialType.System_UInt16 or | ||
SpecialType.System_Byte or | ||
SpecialType.System_SByte; | ||
} |
Oops, something went wrong.