Skip to content

Commit

Permalink
Detect ArgumentNullException.ThrowIfNull (#974)
Browse files Browse the repository at this point in the history
  • Loading branch information
josefpihrt committed Oct 29, 2022
1 parent 967440f commit 77f4e4a
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 46 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Do not simplify 'default' expression if the type is inferred ([RCS1244](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1244.md)) ([#966](https://github.com/josefpihrt/roslynator/pull/966).
- Use explicit type from lambda expression ([RCS1008](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1008.md)) ([#967](https://github.com/josefpihrt/roslynator/pull/967).
- Do not remove constructor if it is decorated with 'UsedImplicitlyAttribute' ([RCS1074](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1074.md)) ([#968](https://github.com/josefpihrt/roslynator/pull/968).
- Detect argument null check in the form of `ArgumentNullException.ThrowIfNull` ([RR0025](https://github.com/JosefPihrt/Roslynator/blob/main/docs/refactorings/RR0025.md), [RCS1227](https://github.com/JosefPihrt/Roslynator/blob/main/docs/analyzers/RCS1227.md)) ([#974](https://github.com/josefpihrt/roslynator/pull/974).

-----
<!-- Content below does not adhere to 'Keep a Changelog' format -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -58,7 +57,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
int index = -1;
for (int i = 0; i < statementCount; i++)
{
if (IsNullCheck(statements[i]))
if (ArgumentNullCheckAnalysis.IsArgumentNullCheck(statements[i], context.SemanticModel, context.CancellationToken))
{
index++;
}
Expand Down Expand Up @@ -100,11 +99,5 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
DiagnosticRules.ValidateArgumentsCorrectly,
Location.Create(body.SyntaxTree, new TextSpan(statements[index + 1].SpanStart, 0)));
}

private static bool IsNullCheck(StatementSyntax statement)
{
return statement.IsKind(SyntaxKind.IfStatement)
&& ((IfStatementSyntax)statement).SingleNonBlockStatementOrDefault().IsKind(SyntaxKind.ThrowStatement);
}
}
}
123 changes: 123 additions & 0 deletions src/Common/ArgumentNullCheckAnalysis.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Roslynator.CSharp.Syntax;

namespace Roslynator.CSharp
{
internal readonly struct ArgumentNullCheckAnalysis
{
private ArgumentNullCheckAnalysis(ArgumentNullCheckStyle style, string name, bool success)
{
Style = style;
Name = name;
Success = success;
}

public ArgumentNullCheckStyle Style { get; }

public string Name { get; }

public bool Success { get; }

public static ArgumentNullCheckAnalysis Create(
StatementSyntax statement,
SemanticModel semanticModel,
CancellationToken cancellationToken = default)
{
return Create(statement, semanticModel, name: null, cancellationToken);
}

public static ArgumentNullCheckAnalysis Create(
StatementSyntax statement,
SemanticModel semanticModel,
string name,
CancellationToken cancellationToken = default)
{
var style = ArgumentNullCheckStyle.None;
string identifier = null;
var success = false;

if (statement is IfStatementSyntax ifStatement)
{
if (ifStatement.SingleNonBlockStatementOrDefault() is ThrowStatementSyntax throwStatement
&& throwStatement.Expression is ObjectCreationExpressionSyntax objectCreation)
{
NullCheckExpressionInfo nullCheck = SyntaxInfo.NullCheckExpressionInfo(
ifStatement.Condition,
semanticModel,
NullCheckStyles.EqualsToNull | NullCheckStyles.IsNull,
cancellationToken: cancellationToken);

if (nullCheck.Success)
{
style = ArgumentNullCheckStyle.IfStatement;

if (nullCheck.Expression is IdentifierNameSyntax identifierName)
{
identifier = identifierName.Identifier.ValueText;

if (name is null
|| string.Equals(name, identifier, StringComparison.Ordinal))
{
if (semanticModel
.GetSymbol(objectCreation, cancellationToken)?
.ContainingType?
.HasMetadataName(MetadataNames.System_ArgumentNullException) == true)
{
success = true;
}
}
}
}

return new ArgumentNullCheckAnalysis(style, identifier, success);
}
}
else if (statement is ExpressionStatementSyntax expressionStatement)
{
SimpleMemberInvocationStatementInfo invocationInfo = SyntaxInfo.SimpleMemberInvocationStatementInfo(expressionStatement);

if (invocationInfo.Success
&& string.Equals(invocationInfo.NameText, "ThrowIfNull", StringComparison.Ordinal)
&& semanticModel
.GetSymbol(invocationInfo.InvocationExpression, cancellationToken)?
.ContainingType?
.HasMetadataName(MetadataNames.System_ArgumentNullException) == true)
{
style = ArgumentNullCheckStyle.ThrowIfNullMethod;

if (invocationInfo.Arguments.SingleOrDefault(shouldThrow: false)?.Expression is IdentifierNameSyntax identifierName)
{
identifier = identifierName.Identifier.ValueText;

if (string.Equals(name, identifier, StringComparison.Ordinal))
success = true;
}
}
}

return new ArgumentNullCheckAnalysis(style, identifier, success);
}

public static bool IsArgumentNullCheck(
StatementSyntax statement,
SemanticModel semanticModel,
CancellationToken cancellationToken = default)
{
return IsArgumentNullCheck(statement, semanticModel, name: null, cancellationToken);
}

public static bool IsArgumentNullCheck(
StatementSyntax statement,
SemanticModel semanticModel,
string name,
CancellationToken cancellationToken = default)
{
return Create(statement, semanticModel, name, cancellationToken).Success;
}
}
}
11 changes: 11 additions & 0 deletions src/Common/ArgumentNullCheckStyle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Josef Pihrt and Contributors. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace Roslynator.CSharp
{
internal enum ArgumentNullCheckStyle
{
None,
IfStatement,
ThrowIfNullMethod,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ private static void RegisterRefactoring(RefactoringContext context, ParameterSyn

foreach (StatementSyntax statement in body.Statements)
{
NullCheckExpressionInfo nullCheck = GetNullCheckExpressionInfo(statement, semanticModel, cancellationToken);
ArgumentNullCheckAnalysis nullCheck = ArgumentNullCheckAnalysis.Create(statement, semanticModel, parameter.Identifier.ValueText, cancellationToken);

if (nullCheck.Success)
if (nullCheck.Style != ArgumentNullCheckStyle.None)
{
if (string.Equals(((IdentifierNameSyntax)nullCheck.Expression).Identifier.ValueText, parameter.Identifier.ValueText, StringComparison.Ordinal))
if (nullCheck.Success)
return false;
}
else
Expand All @@ -132,7 +132,7 @@ private static void RegisterRefactoring(RefactoringContext context, ParameterSyn
SyntaxList<StatementSyntax> statements = body.Statements;

int count = statements
.TakeWhile(f => GetNullCheckExpressionInfo(f, semanticModel, cancellationToken).Success)
.TakeWhile(f => ArgumentNullCheckAnalysis.IsArgumentNullCheck(f, semanticModel, cancellationToken))
.Count();

List<IfStatementSyntax> ifStatements = CreateNullChecks(parameters);
Expand Down Expand Up @@ -200,40 +200,6 @@ private static List<IfStatementSyntax> CreateNullChecks(ImmutableArray<Parameter
return ifStatements;
}

private static NullCheckExpressionInfo GetNullCheckExpressionInfo(
StatementSyntax statement,
SemanticModel semanticModel,
CancellationToken cancellationToken = default)
{
if (statement is not IfStatementSyntax ifStatement)
return default;

NullCheckExpressionInfo nullCheck = SyntaxInfo.NullCheckExpressionInfo(ifStatement.Condition, NullCheckStyles.EqualsToNull | NullCheckStyles.IsNull);

if (!nullCheck.Success)
return default;

if (nullCheck.Expression.Kind() != SyntaxKind.IdentifierName)
return default;

var throwStatement = ifStatement.SingleNonBlockStatementOrDefault() as ThrowStatementSyntax;

if (throwStatement?.Expression?.Kind() != SyntaxKind.ObjectCreationExpression)
return default;

var objectCreation = (ObjectCreationExpressionSyntax)throwStatement.Expression;

ISymbol type = semanticModel.GetSymbol(objectCreation.Type, cancellationToken);

if (!string.Equals(type?.Name, "ArgumentNullException", StringComparison.Ordinal))
return default;

if (!type.HasMetadataName(MetadataNames.System_ArgumentNullException))
return default;

return nullCheck;
}

private static BlockSyntax GetBody(ParameterSyntax parameter)
{
SyntaxNode parent = parameter.Parent;
Expand Down

0 comments on commit 77f4e4a

Please sign in to comment.