Skip to content

Commit

Permalink
CA2262: Prefer generic overload when type is known
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 committed Aug 15, 2023
1 parent 2af4cd8 commit 600b323
Show file tree
Hide file tree
Showing 26 changed files with 3,295 additions and 1 deletion.
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>
/// CA2262: <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>
/// CA2262: <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 @@ -24,6 +24,7 @@ CA1862 | Performance | Info | RecommendCaseInsensitiveStringComparison, [Documen
CA1863 | Performance | Hidden | UseCompositeFormatAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1862)
CA1864 | Performance | Info | PreferDictionaryTryAddValueOverGuardedAddAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1864)
CA2021 | Reliability | Warning | DoNotCallEnumerableCastOrOfTypeWithIncompatibleTypesAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2021)
CA2262 | Usage | Info | PreferGenericOverloadsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2262)

### Removed Rules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,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>
/// CA2262: <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 600b323

Please sign in to comment.