Skip to content

Commit

Permalink
CA2263: Prefer generic overload when type is known (#6857)
Browse files Browse the repository at this point in the history
This analyzer detects when a `System.Type` overload is called when a
suitable generic overload is available.

To validate if a generic overload is applicable, the arity, parameter
count, containing symbol of the invocation (to avoid endless loops),
return type, argument types and type constraints(using speculative
binding) are checked.

The fixer removes unnecessary casts and parentheses.
  • Loading branch information
mpidash authored Feb 16, 2024
1 parent 35c0109 commit 90721b0
Show file tree
Hide file tree
Showing 26 changed files with 3,296 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.NetCore.Analyzers.Usage;
using static Microsoft.NetCore.Analyzers.Usage.PreferGenericOverloadsAnalyzer;

namespace Microsoft.NetCore.CSharp.Analyzers.Usage
{
/// <summary>
/// CA2263: <inheritdoc cref="NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources.PreferGenericOverloadsTitle"/>
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public sealed class CSharpPreferGenericOverloadsFixer : PreferGenericOverloadsFixer
{
protected override async Task<Document> ReplaceWithGenericCallAsync(Document document, IInvocationOperation invocation, CancellationToken cancellationToken)
{
if (!RuntimeTypeInvocationContext.TryGetContext(invocation, out var invocationContext))
{
return document;
}

var modifiedInvocationSyntax = CSharpPreferGenericOverloadsAnalyzer.GetModifiedInvocationSyntax(invocationContext);

if (modifiedInvocationSyntax is not InvocationExpressionSyntax invocationExpressionSyntax)
{
return document;
}

// Analyzers are not allowed to have a reference to Simplifier, so add the additional annotation here instead.
invocationExpressionSyntax = invocationExpressionSyntax.WithExpression(invocationExpressionSyntax.Expression.WithAdditionalAnnotations(Simplifier.Annotation));

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

if (invocationContext.Parent is IConversionOperation conversionOperation
&& invocationContext.Parent.Syntax is CastExpressionSyntax castExpressionSyntax
&& invocationContext.SemanticModel is not null)
{
var typeInfo = invocationContext.SemanticModel.GetSpeculativeTypeInfo(
invocationContext.Syntax.SpanStart,
invocationExpressionSyntax,
SpeculativeBindingOption.BindAsExpression);

if (typeInfo.ConvertedType.IsAssignableTo(conversionOperation.Type, invocationContext.SemanticModel.Compilation))
{
// Add a simplifier annotation to the parent to remove no longer needed parenthesis.
if (castExpressionSyntax.Parent is ParenthesizedExpressionSyntax parenthesizedExpressionSyntax)
{
editor.ReplaceNode(
parenthesizedExpressionSyntax,
parenthesizedExpressionSyntax
.ReplaceNode(
castExpressionSyntax,
castExpressionSyntax.Expression
.ReplaceNode(invocationContext.Syntax, invocationExpressionSyntax)
.WithTriviaFrom(castExpressionSyntax))
.WithAdditionalAnnotations(Simplifier.Annotation));
}
else
{
editor.ReplaceNode(
castExpressionSyntax,
castExpressionSyntax.Expression
.ReplaceNode(invocationContext.Syntax, invocationExpressionSyntax)
.WithTriviaFrom(castExpressionSyntax));
}
}
else
{
editor.ReplaceNode(invocationContext.Syntax, invocationExpressionSyntax);
}
}
else
{
editor.ReplaceNode(invocationContext.Syntax, invocationExpressionSyntax);
}

return document.WithSyntaxRoot(editor.GetChangedRoot());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.NetCore.Analyzers.Usage;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.CSharp.Analyzers.Usage
{
/// <summary>
/// CA2263: <inheritdoc cref="NetCore.Analyzers.MicrosoftNetCoreAnalyzersResources.PreferGenericOverloadsTitle"/>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CSharpPreferGenericOverloadsAnalyzer : PreferGenericOverloadsAnalyzer
{
protected sealed override bool TryGetModifiedInvocationSyntax(RuntimeTypeInvocationContext invocationContext, [NotNullWhen(true)] out SyntaxNode? modifiedInvocationSyntax)
{
modifiedInvocationSyntax = GetModifiedInvocationSyntax(invocationContext);

return modifiedInvocationSyntax is not null;
}

// Expose as internal static to allow the fixer to also call this method.
internal static SyntaxNode? GetModifiedInvocationSyntax(RuntimeTypeInvocationContext invocationContext)
{
if (invocationContext.Syntax is not InvocationExpressionSyntax invocationSyntax)
{
return null;
}

var typeArgumentsSyntax = invocationContext.TypeArguments.Select(t => SyntaxFactory.ParseTypeName(t.ToDisplayString()));
var otherArgumentsSyntax = invocationContext.OtherArguments
.Where(a => a.ArgumentKind != ArgumentKind.DefaultValue)
.Select(a => a.Syntax)
.OfType<ArgumentSyntax>();
var methodNameSyntax =
SyntaxFactory.GenericName(
SyntaxFactory.Identifier(invocationContext.Method.Name),
SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(typeArgumentsSyntax)));
var modifiedInvocationExpression = invocationSyntax.Expression;

if (modifiedInvocationExpression is MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
modifiedInvocationExpression = memberAccessExpressionSyntax.WithName(methodNameSyntax);
}
else if (modifiedInvocationExpression is IdentifierNameSyntax identifierNameSyntax)
{
modifiedInvocationExpression = methodNameSyntax;
}
else
{
return null;
}

return invocationSyntax
.WithExpression(modifiedInvocationExpression)
.WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(otherArgumentsSyntax)))
.WithTriviaFrom(invocationSyntax);
}
}
}
1 change: 1 addition & 0 deletions src/NetAnalyzers/Core/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
CA1514 | Maintainability | Info | AvoidLengthCheckWhenSlicingToEndAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1514)
CA1515 | Maintainability | Disabled | MakeTypesInternal, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1515)
CA2262 | Usage | Info | ProvideHttpClientHandlerMaxResponseHeaderLengthValueCorrectly, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2262)
CA2263 | Usage | Info | PreferGenericOverloadsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,18 @@ Widening and user defined conversions are not supported with generic types.</val
<data name="ImplementGenericMathInterfacesCorrectlyTitle" xml:space="preserve">
<value>Use correct type parameter</value>
</data>
<data name="PreferGenericOverloadsCodeFixTitle" xml:space="preserve">
<value>Use generic overload</value>
</data>
<data name="PreferGenericOverloadsDescription" xml:space="preserve">
<value>Using a generic overload is preferable to the 'System.Type' overload when the type is known, promoting cleaner and more type-safe code with improved compile-time checks.</value>
</data>
<data name="PreferGenericOverloadsMessage" xml:space="preserve">
<value>Prefer the generic overload '{0}' instead of '{1}'</value>
</data>
<data name="PreferGenericOverloadsTitle" xml:space="preserve">
<value>Prefer generic overload when type is known</value>
</data>
<data name="UseSpanClearInsteadOfFillCodeFixTitle" xml:space="preserve">
<value>Use 'Clear()'</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Analyzer.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Operations;
using static Microsoft.NetCore.Analyzers.Usage.PreferGenericOverloadsAnalyzer;

namespace Microsoft.NetCore.Analyzers.Usage
{
/// <summary>
/// CA2263: <inheritdoc cref="MicrosoftNetCoreAnalyzersResources.PreferGenericOverloadsTitle"/>
/// </summary>
public abstract class PreferGenericOverloadsFixer : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(RuleId);

public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetRequiredSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var node = root.FindNode(context.Span, getInnermostNodeForTie: true);

if (node is null)
{
return;
}

var semanticModel = await context.Document.GetRequiredSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
var operation = semanticModel.GetOperation(node, context.CancellationToken);

if (operation is not IInvocationOperation invocation)
{
return;
}

var codeAction = CodeAction.Create(
MicrosoftNetCoreAnalyzersResources.PreferGenericOverloadsCodeFixTitle,
ct => ReplaceWithGenericCallAsync(context.Document, invocation, ct),
nameof(MicrosoftNetCoreAnalyzersResources.PreferGenericOverloadsCodeFixTitle));

context.RegisterCodeFix(codeAction, context.Diagnostics);
}

protected abstract Task<Document> ReplaceWithGenericCallAsync(Document document, IInvocationOperation invocation, CancellationToken cancellationToken);
}
}
Loading

0 comments on commit 90721b0

Please sign in to comment.