Skip to content

Commit

Permalink
Fix S1905 FP: Nullability context and array of anonymous types (#6753)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim-Pohlmann committed Mar 14, 2023
1 parent d5f0949 commit 8e33179
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 164 deletions.
240 changes: 108 additions & 132 deletions analyzers/src/SonarAnalyzer.CSharp/Rules/RedundantCast.cs
Expand Up @@ -18,161 +18,137 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarAnalyzer.Rules.CSharp
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RedundantCast : SonarDiagnosticAnalyzer
{
internal const string DiagnosticId = "S1905";
private const string MessageFormat = "Remove this unnecessary cast to '{0}'.";

private static readonly DiagnosticDescriptor rule =
DescriptorFactory.Create(DiagnosticId, MessageFormat);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(rule);
namespace SonarAnalyzer.Rules.CSharp;

private static readonly ISet<string> CastIEnumerableMethods = new HashSet<string> { "Cast", "OfType" };

protected override void Initialize(SonarAnalysisContext context)
{
context.RegisterNodeAction(
c =>
{
var castExpression = (CastExpressionSyntax)c.Node;
CheckCastExpression(c, castExpression.Expression, castExpression.Type, castExpression.Type.GetLocation());
},
SyntaxKind.CastExpression);
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RedundantCast : SonarDiagnosticAnalyzer
{
internal const string DiagnosticId = "S1905";
private const string MessageFormat = "Remove this unnecessary cast to '{0}'.";

context.RegisterNodeAction(
c =>
{
var castExpression = (BinaryExpressionSyntax)c.Node;
CheckCastExpression(c, castExpression.Left, castExpression.Right,
castExpression.OperatorToken.CreateLocation(castExpression.Right));
},
SyntaxKind.AsExpression);

context.RegisterNodeAction(
CheckExtensionMethodInvocation,
SyntaxKind.InvocationExpression);
}
private static readonly DiagnosticDescriptor Rule =
DescriptorFactory.Create(DiagnosticId, MessageFormat);

private static void CheckCastExpression(SonarSyntaxNodeReportingContext context, ExpressionSyntax expression, ExpressionSyntax type, Location location)
{
if (expression.IsKind(SyntaxKindEx.DefaultLiteralExpression))
{
return;
}
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

var expressionType = context.SemanticModel.GetTypeInfo(expression).Type;
if (expressionType == null)
{
return;
}
private static readonly ISet<string> CastIEnumerableMethods = new HashSet<string> { "Cast", "OfType" };

var castType = context.SemanticModel.GetTypeInfo(type).Type;
if (castType == null)
protected override void Initialize(SonarAnalysisContext context)
{
context.RegisterNodeAction(
c =>
{
return;
}
var castExpression = (CastExpressionSyntax)c.Node;
CheckCastExpression(c, castExpression.Expression, castExpression.Type, castExpression.Type.GetLocation());
},
SyntaxKind.CastExpression);

if (expressionType.Equals(castType))
context.RegisterNodeAction(
c =>
{
context.ReportIssue(Diagnostic.Create(rule, location,
castType.ToMinimalDisplayString(context.SemanticModel, expression.SpanStart)));
}
}
var castExpression = (BinaryExpressionSyntax)c.Node;
CheckCastExpression(c, castExpression.Left, castExpression.Right,
castExpression.OperatorToken.CreateLocation(castExpression.Right));
},
SyntaxKind.AsExpression);

context.RegisterNodeAction(
CheckExtensionMethodInvocation,
SyntaxKind.InvocationExpression);
}

private static void CheckExtensionMethodInvocation(SonarSyntaxNodeReportingContext context)
private static void CheckCastExpression(SonarSyntaxNodeReportingContext context, ExpressionSyntax expression, ExpressionSyntax type, Location location)
{
if (!expression.IsKind(SyntaxKindEx.DefaultLiteralExpression)
&& context.SemanticModel.GetTypeInfo(expression) is { Type: { } expressionType } expressionTypeInfo
&& context.SemanticModel.GetTypeInfo(type) is { Type: { } castType }
&& expressionType.Equals(castType)
&& FlowStateEquals(expressionTypeInfo, type))
{
var invocation = (InvocationExpressionSyntax)context.Node;
if (GetEnumerableExtensionSymbol(invocation, context.SemanticModel) is { } methodSymbol)
{
var returnType = methodSymbol.ReturnType;
if (GetGenericTypeArgument(returnType) is { } castType)
{
if (methodSymbol.Name == "OfType" && CanHaveNullValue(castType))
{
// OfType() filters 'null' values from enumerables
return;
}

var elementType = GetElementType(invocation, methodSymbol, context.SemanticModel);
// Generic types {T} and {T?} are equal and there is no way to access NullableAnnotation field right now
// See https://github.com/SonarSource/sonar-dotnet/issues/3273
if (elementType != null && elementType.Equals(castType) && string.Equals(elementType.ToString(), castType.ToString(), System.StringComparison.Ordinal))
{
var methodCalledAsStatic = methodSymbol.MethodKind == MethodKind.Ordinary;
context.ReportIssue(Diagnostic.Create(rule, GetReportLocation(invocation, methodCalledAsStatic),
returnType.ToMinimalDisplayString(context.SemanticModel, invocation.SpanStart)));
}
}
}
ReportIssue(context, expression, castType, location);
}
}

/// If the invocation one of the <see cref="CastIEnumerableMethods"/> extensions, returns the method symbol.
private static IMethodSymbol GetEnumerableExtensionSymbol(InvocationExpressionSyntax invocation, SemanticModel semanticModel) =>
invocation.GetMethodCallIdentifier() is { } methodName
&& CastIEnumerableMethods.Contains(methodName.ValueText)
&& semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol methodSymbol
&& methodSymbol.IsExtensionOn(KnownType.System_Collections_IEnumerable)
? methodSymbol
: null;

private static ITypeSymbol GetGenericTypeArgument(ITypeSymbol type) =>
type is INamedTypeSymbol returnType && returnType.Is(KnownType.System_Collections_Generic_IEnumerable_T)
? returnType.TypeArguments.Single()
: null;

private static bool CanHaveNullValue(ITypeSymbol type) => type.IsReferenceType || type.Name == "Nullable";

private static Location GetReportLocation(InvocationExpressionSyntax invocation, bool methodCalledAsStatic)
private static bool FlowStateEquals(TypeInfo expressionTypeInfo, ExpressionSyntax type)
{
var castingToNullable = type.IsKind(SyntaxKind.NullableType);
return expressionTypeInfo.Nullability().FlowState switch
{
if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess))
{
return invocation.Expression.GetLocation();
}

return methodCalledAsStatic
? memberAccess.GetLocation()
: memberAccess.OperatorToken.CreateLocation(invocation);
}
NullableFlowState.None => true,
NullableFlowState.MaybeNull => castingToNullable,
NullableFlowState.NotNull => !castingToNullable,
_ => true,
};
}

private static ITypeSymbol GetElementType(InvocationExpressionSyntax invocation, IMethodSymbol methodSymbol,
SemanticModel semanticModel)
private static void CheckExtensionMethodInvocation(SonarSyntaxNodeReportingContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;
if (GetEnumerableExtensionSymbol(invocation, context.SemanticModel) is { } methodSymbol)
{
ExpressionSyntax collection;
if (methodSymbol.MethodKind == MethodKind.Ordinary)
var returnType = methodSymbol.ReturnType;
if (GetGenericTypeArgument(returnType) is { } castType)
{
if (!invocation.ArgumentList.Arguments.Any())
if (methodSymbol.Name == "OfType" && CanHaveNullValue(castType))
{
return null;
// OfType() filters 'null' values from enumerables
return;
}
collection = invocation.ArgumentList.Arguments.First().Expression;
}
else
{
if (!(invocation.Expression is MemberAccessExpressionSyntax memberAccess))

var elementType = GetElementType(invocation, methodSymbol, context.SemanticModel);
// Generic types {T} and {T?} are equal and there is no way to access NullableAnnotation field right now
// See https://github.com/SonarSource/sonar-dotnet/issues/3273
if (elementType != null && elementType.Equals(castType) && string.Equals(elementType.ToString(), castType.ToString(), StringComparison.Ordinal))
{
return null;
var methodCalledAsStatic = methodSymbol.MethodKind == MethodKind.Ordinary;
ReportIssue(context, invocation, returnType, GetReportLocation(invocation, methodCalledAsStatic));
}
collection = memberAccess.Expression;
}
}
}

var typeInfo = semanticModel.GetTypeInfo(collection);
if (typeInfo.Type is INamedTypeSymbol collectionType &&
collectionType.TypeArguments.Length == 1)
{
return collectionType.TypeArguments.First();
}
private static void ReportIssue(SonarSyntaxNodeReportingContext context, ExpressionSyntax expression, ITypeSymbol castType, Location location) =>
context.ReportIssue(Diagnostic.Create(Rule, location, castType.ToMinimalDisplayString(context.SemanticModel, expression.SpanStart)));

if (typeInfo.Type is IArrayTypeSymbol arrayType &&
arrayType.Rank == 1) // casting is necessary for multidimensional arrays
{
return arrayType.ElementType;
}
/// If the invocation one of the <see cref="CastIEnumerableMethods"/> extensions, returns the method symbol.
private static IMethodSymbol GetEnumerableExtensionSymbol(InvocationExpressionSyntax invocation, SemanticModel semanticModel) =>
invocation.GetMethodCallIdentifier() is { } methodName
&& CastIEnumerableMethods.Contains(methodName.ValueText)
&& semanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol methodSymbol
&& methodSymbol.IsExtensionOn(KnownType.System_Collections_IEnumerable)
? methodSymbol
: null;

return null;
}
private static ITypeSymbol GetGenericTypeArgument(ITypeSymbol type) =>
type is INamedTypeSymbol returnType && returnType.Is(KnownType.System_Collections_Generic_IEnumerable_T)
? returnType.TypeArguments.Single()
: null;

private static bool CanHaveNullValue(ITypeSymbol type) =>
type.IsReferenceType || type.Is(KnownType.System_Nullable_T);

private static Location GetReportLocation(InvocationExpressionSyntax invocation, bool methodCalledAsStatic) =>
methodCalledAsStatic is false && invocation.Expression is MemberAccessExpressionSyntax memberAccess
? memberAccess.OperatorToken.CreateLocation(invocation)
: invocation.Expression.GetLocation();

private static ITypeSymbol GetElementType(InvocationExpressionSyntax invocation, IMethodSymbol methodSymbol, SemanticModel semanticModel)
{
return semanticModel.GetTypeInfo(CollectionExpression(invocation, methodSymbol)).Type switch
{
INamedTypeSymbol { TypeArguments: { Length: 1 } typeArguments } => typeArguments.First(),
IArrayTypeSymbol { Rank: 1 } arrayType => arrayType.ElementType, // casting is necessary for multidimensional arrays
_ => null
};

static ExpressionSyntax CollectionExpression(InvocationExpressionSyntax invocation, IMethodSymbol methodSymbol) =>
methodSymbol.MethodKind is MethodKind.ReducedExtension
? ReducedExtensionExpression(invocation)
: invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression;

static ExpressionSyntax ReducedExtensionExpression(InvocationExpressionSyntax invocation) =>
invocation.Expression is MemberAccessExpressionSyntax { Expression: { } memberAccessExpression }
? memberAccessExpression
: invocation.GetParentConditionalAccessExpression()?.Expression;
}
}

0 comments on commit 8e33179

Please sign in to comment.