-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis.CodeStyle; | ||
using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Roslyn.Utilities; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp.UseParameterNullChecking | ||
{ | ||
[DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
internal class CSharpUseParameterNullCheckingDiagnosticAnalyzer | ||
: AbstractBuiltInCodeStyleDiagnosticAnalyzer | ||
{ | ||
private const string ArgumentNullExceptionName = $"{nameof(System)}.{nameof(ArgumentNullException)}"; | ||
|
||
public CSharpUseParameterNullCheckingDiagnosticAnalyzer() | ||
: base(IDEDiagnosticIds.UseParameterNullCheckingId, | ||
EnforceOnBuildValues.UseParameterNullChecking, | ||
CodeStyleOptions2.PreferParameterNullChecking, | ||
CSharpAnalyzersResources.Use_parameter_null_checking, | ||
new LocalizableResourceString(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))) | ||
{ | ||
} | ||
|
||
public override DiagnosticAnalyzerCategory GetAnalyzerCategory() | ||
=> DiagnosticAnalyzerCategory.SemanticSpanAnalysis; | ||
|
||
protected override void InitializeWorker(AnalysisContext context) | ||
=> context.RegisterCompilationStartAction(context => | ||
{ | ||
if (((CSharpCompilation)context.Compilation).LanguageVersion < LanguageVersionExtensions.CSharpNext) | ||
{ | ||
return; | ||
} | ||
|
||
var compilation = context.Compilation; | ||
var argumentNullException = compilation.GetTypeByMetadataName(ArgumentNullExceptionName); | ||
if (argumentNullException is null) | ||
{ | ||
return; | ||
} | ||
|
||
var argumentNullExceptionConstructor = argumentNullException.InstanceConstructors.FirstOrDefault(m => | ||
m.DeclaredAccessibility == Accessibility.Public | ||
&& m.Parameters.Length == 1 | ||
&& m.Parameters[0].Type.SpecialType == SpecialType.System_String); | ||
if (argumentNullExceptionConstructor is null) | ||
{ | ||
return; | ||
} | ||
|
||
var objectType = compilation.GetSpecialType(SpecialType.System_Object); | ||
var referenceEqualsMethod = objectType | ||
.GetMembers(nameof(ReferenceEquals)) | ||
.OfType<IMethodSymbol>() | ||
.FirstOrDefault(m => m.DeclaredAccessibility == Accessibility.Public && m.Parameters.Length == 2); | ||
// This seems sufficiently rare that it's not a big deal to just not perform analysis in this case. | ||
if (referenceEqualsMethod is null) | ||
{ | ||
return; | ||
} | ||
|
||
context.RegisterSyntaxNodeAction(context => AnalyzeSyntax(context, argumentNullExceptionConstructor, referenceEqualsMethod), SyntaxKind.MethodDeclaration, SyntaxKind.LocalFunctionStatement); | ||
}); | ||
|
||
private void AnalyzeSyntax(SyntaxNodeAnalysisContext context, IMethodSymbol argumentNullExceptionConstructor, IMethodSymbol referenceEqualsMethod) | ||
{ | ||
var cancellationToken = context.CancellationToken; | ||
|
||
var semanticModel = context.SemanticModel; | ||
var syntaxTree = semanticModel.SyntaxTree; | ||
|
||
var option = context.Options.GetOption(CodeStyleOptions2.PreferParameterNullChecking, semanticModel.Language, syntaxTree, cancellationToken); | ||
if (!option.Value) | ||
{ | ||
return; | ||
} | ||
|
||
var node = context.Node; | ||
var block = node switch | ||
{ | ||
MethodDeclarationSyntax methodDecl => methodDecl.Body, | ||
LocalFunctionStatementSyntax localFunctionStatement => localFunctionStatement.Body, | ||
_ => throw ExceptionUtilities.UnexpectedValue(node) | ||
}; | ||
if (block is null) | ||
{ | ||
return; | ||
} | ||
|
||
var methodSymbol = semanticModel.GetDeclaredSymbol(node, cancellationToken); | ||
if (methodSymbol is null) | ||
{ | ||
return; | ||
} | ||
|
||
foreach (var statement in block.Statements) | ||
{ | ||
switch (statement) | ||
{ | ||
// if (param == null) { throw new ArgumentNullException(nameof(param)); } | ||
// if (param is null) { throw new ArgumentNullException(nameof(param)); } | ||
// if (object.ReferenceEquals(param, null)) { throw new ArgumentNullException(nameof(param)); } | ||
case IfStatementSyntax ifStatement: | ||
ExpressionSyntax left, right; | ||
switch (ifStatement) | ||
{ | ||
case { Condition: BinaryExpressionSyntax { OperatorToken.RawKind: (int)SyntaxKind.EqualsEqualsToken } binary }: | ||
left = binary.Left; | ||
right = binary.Right; | ||
break; | ||
case { Condition: IsPatternExpressionSyntax { Expression: var patternInput, Pattern: ConstantPatternSyntax { Expression: var patternExpression } } }: | ||
left = patternInput; | ||
right = patternExpression; | ||
break; | ||
case { Condition: InvocationExpressionSyntax { Expression: var receiver, ArgumentList.Arguments: { Count: 2 } arguments } }: | ||
if (!referenceEqualsMethod.Equals(semanticModel.GetSymbolInfo(receiver, cancellationToken).Symbol)) | ||
{ | ||
continue; | ||
} | ||
|
||
left = arguments[0].Expression; | ||
right = arguments[1].Expression; | ||
break; | ||
|
||
default: | ||
continue; | ||
} | ||
|
||
IParameterSymbol? parameter; | ||
if (!areOperandsApplicable(left, right, out parameter) | ||
&& !areOperandsApplicable(right, left, out parameter)) | ||
{ | ||
continue; | ||
} | ||
|
||
var throwStatement = ifStatement.Statement switch | ||
{ | ||
ThrowStatementSyntax @throw => @throw, | ||
BlockSyntax { Statements: { Count: 1 } statements } => statements[0] as ThrowStatementSyntax, | ||
_ => null | ||
}; | ||
|
||
if (throwStatement is null | ||
|| throwStatement.Expression is not ObjectCreationExpressionSyntax thrownInIf | ||
|| !isConstructorApplicable(thrownInIf, parameter)) | ||
{ | ||
continue; | ||
} | ||
|
||
break; | ||
|
||
// this.field = param ?? throw new ArgumentNullException(nameof(param)); | ||
case ExpressionStatementSyntax | ||
{ | ||
Expression: AssignmentExpressionSyntax | ||
{ | ||
Right: BinaryExpressionSyntax | ||
{ | ||
OperatorToken.RawKind: (int)SyntaxKind.QuestionQuestionToken, | ||
Left: ExpressionSyntax maybeParameter, | ||
Right: ThrowExpressionSyntax { Expression: ObjectCreationExpressionSyntax thrownInNullCoalescing } | ||
} | ||
} | ||
}: | ||
if (!isParameter(maybeParameter, out var parameterInNullCoalescing) || !isConstructorApplicable(thrownInNullCoalescing, parameterInNullCoalescing)) | ||
{ | ||
continue; | ||
} | ||
|
||
break; | ||
|
||
default: | ||
continue; | ||
} | ||
|
||
context.ReportDiagnostic(DiagnosticHelper.Create(Descriptor, statement.GetLocation(), option.Notification.Severity, additionalLocations: null, properties: null)); | ||
} | ||
|
||
bool areOperandsApplicable(ExpressionSyntax maybeParameter, ExpressionSyntax maybeNullLiteral, [NotNullWhen(true)] out IParameterSymbol? parameter) | ||
{ | ||
if (!maybeNullLiteral.IsKind(SyntaxKind.NullLiteralExpression)) | ||
{ | ||
parameter = null; | ||
return false; | ||
} | ||
|
||
return isParameter(maybeParameter, out parameter); | ||
} | ||
|
||
bool isParameter(ExpressionSyntax maybeParameter, [NotNullWhen(true)] out IParameterSymbol? parameter) | ||
{ | ||
if (maybeParameter is CastExpressionSyntax { Type: var type, Expression: var operand }) | ||
{ | ||
if (semanticModel.GetTypeInfo(type).Type?.SpecialType != SpecialType.System_Object) | ||
{ | ||
parameter = null; | ||
return false; | ||
} | ||
|
||
maybeParameter = operand; | ||
} | ||
|
||
if (semanticModel.GetSymbolInfo(maybeParameter).Symbol is not IParameterSymbol { ContainingSymbol: { } containingSymbol } parameterSymbol || !containingSymbol.Equals(methodSymbol)) | ||
{ | ||
parameter = null; | ||
return false; | ||
} | ||
|
||
parameter = parameterSymbol; | ||
return true; | ||
} | ||
|
||
bool isConstructorApplicable(ObjectCreationExpressionSyntax exceptionCreation, IParameterSymbol parameterSymbol) | ||
{ | ||
if (exceptionCreation.ArgumentList is null | ||
|| exceptionCreation.ArgumentList.Arguments[0] is not { } argument) | ||
{ | ||
return false; | ||
} | ||
var constantValue = semanticModel.GetConstantValue(argument.Expression, cancellationToken); | ||
if (!constantValue.HasValue || constantValue.Value is not string constantString || !string.Equals(constantString, parameterSymbol.Name, StringComparison.Ordinal)) | ||
{ | ||
return false; | ||
} | ||
|
||
if (!argumentNullExceptionConstructor.Equals(semanticModel.GetSymbolInfo(exceptionCreation, cancellationToken).Symbol)) | ||
{ | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.