From 7c60002b24f59f88b0dc2fdb350936733afd4ccf Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Tue, 11 Jan 2022 16:58:04 -0800 Subject: [PATCH] Parameter null-checking analyzer and fixer (#58182) --- .../Analyzers/CSharpAnalyzers.projitems | 1 + .../Analyzers/CSharpAnalyzersResources.resx | 3 + ...ParameterNullCheckingDiagnosticAnalyzer.cs | 316 ++++ .../xlf/CSharpAnalyzersResources.cs.xlf | 5 + .../xlf/CSharpAnalyzersResources.de.xlf | 5 + .../xlf/CSharpAnalyzersResources.es.xlf | 5 + .../xlf/CSharpAnalyzersResources.fr.xlf | 5 + .../xlf/CSharpAnalyzersResources.it.xlf | 5 + .../xlf/CSharpAnalyzersResources.ja.xlf | 5 + .../xlf/CSharpAnalyzersResources.ko.xlf | 5 + .../xlf/CSharpAnalyzersResources.pl.xlf | 5 + .../xlf/CSharpAnalyzersResources.pt-BR.xlf | 5 + .../xlf/CSharpAnalyzersResources.ru.xlf | 5 + .../xlf/CSharpAnalyzersResources.tr.xlf | 5 + .../xlf/CSharpAnalyzersResources.zh-Hans.xlf | 5 + .../xlf/CSharpAnalyzersResources.zh-Hant.xlf | 5 + .../CodeFixes/CSharpCodeFixes.projitems | 1 + ...UseParameterNullCheckingCodeFixProvider.cs | 102 ++ .../Tests/CSharpAnalyzers.UnitTests.projitems | 1 + .../UseParameterNullCheckingTests.cs | 1549 +++++++++++++++++ .../Core/Analyzers/EnforceOnBuildValues.cs | 1 + .../Core/Analyzers/IDEDiagnosticIds.cs | 2 + .../PredefinedCodeFixProviderNames.cs | 1 + src/Compilers/Test/Core/Traits/Traits.cs | 1 + .../AddParameterCheckTests.cs | 23 +- .../IDEDiagnosticIDConfigurationTests.cs | 6 + .../CSharp/Impl/CSharpVSResources.resx | 3 + .../CSharpCodeStyleSettingsProvider.cs | 4 + .../AutomationObject.Style.cs | 6 + .../Impl/Options/Formatting/StyleViewModel.cs | 23 + .../CSharp/Impl/xlf/CSharpVSResources.cs.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.de.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.es.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.fr.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.it.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.ja.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.ko.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.pl.xlf | 5 + .../Impl/xlf/CSharpVSResources.pt-BR.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.ru.xlf | 5 + .../CSharp/Impl/xlf/CSharpVSResources.tr.xlf | 5 + .../Impl/xlf/CSharpVSResources.zh-Hans.xlf | 5 + .../Impl/xlf/CSharpVSResources.zh-Hant.xlf | 5 + .../CSharpEditorConfigGeneratorTests.vb | 2 + .../CodeStyle/CSharpCodeStyleOptions.cs | 6 + 45 files changed, 2180 insertions(+), 1 deletion(-) create mode 100644 src/Analyzers/CSharp/Analyzers/UseParameterNullChecking/CSharpUseParameterNullCheckingDiagnosticAnalyzer.cs create mode 100644 src/Analyzers/CSharp/CodeFixes/UseParameterNullChecking/CSharpUseParameterNullCheckingCodeFixProvider.cs create mode 100644 src/Analyzers/CSharp/Tests/UseParameterNullChecking/UseParameterNullCheckingTests.cs diff --git a/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems b/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems index 77239030f8743..64c484b1d0853 100644 --- a/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems +++ b/src/Analyzers/CSharp/Analyzers/CSharpAnalyzers.projitems @@ -103,6 +103,7 @@ + diff --git a/src/Analyzers/CSharp/Analyzers/CSharpAnalyzersResources.resx b/src/Analyzers/CSharp/Analyzers/CSharpAnalyzersResources.resx index 2d3d526546b29..466c18e00e098 100644 --- a/src/Analyzers/CSharp/Analyzers/CSharpAnalyzersResources.resx +++ b/src/Analyzers/CSharp/Analyzers/CSharpAnalyzersResources.resx @@ -340,4 +340,7 @@ Use tuple to swap values + + Use parameter null checking + \ No newline at end of file diff --git a/src/Analyzers/CSharp/Analyzers/UseParameterNullChecking/CSharpUseParameterNullCheckingDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseParameterNullChecking/CSharpUseParameterNullCheckingDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000..db85ff5cdea6a --- /dev/null +++ b/src/Analyzers/CSharp/Analyzers/UseParameterNullChecking/CSharpUseParameterNullCheckingDiagnosticAnalyzer.cs @@ -0,0 +1,316 @@ +// 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.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.UseParameterNullChecking +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal sealed class CSharpUseParameterNullCheckingDiagnosticAnalyzer + : AbstractBuiltInCodeStyleDiagnosticAnalyzer + { + private const string ArgumentNullExceptionName = $"{nameof(System)}.{nameof(ArgumentNullException)}"; + private static readonly LocalizableResourceString s_resourceTitle = new(nameof(AnalyzersResources.Null_check_can_be_simplified), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); + + public CSharpUseParameterNullCheckingDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseParameterNullCheckingId, + EnforceOnBuildValues.UseParameterNullChecking, + CSharpCodeStyleOptions.PreferParameterNullChecking, + LanguageNames.CSharp, + s_resourceTitle, + s_resourceTitle) + { + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + { + var compilation = (CSharpCompilation)context.Compilation; + if (compilation.LanguageVersion < LanguageVersionExtensions.CSharpNext) + { + return; + } + + var argumentNullException = compilation.GetBestTypeByMetadataName(ArgumentNullExceptionName); + if (argumentNullException is null) + { + return; + } + + IMethodSymbol? argumentNullExceptionConstructor = null; + IMethodSymbol? argumentNullExceptionStringConstructor = null; + foreach (var constructor in argumentNullException.InstanceConstructors) + { + if (argumentNullExceptionConstructor is not null && argumentNullExceptionStringConstructor is not null) + { + break; + } + + switch (constructor) + { + case { DeclaredAccessibility: Accessibility.Public, Parameters.Length: 0 }: + argumentNullExceptionConstructor = constructor; + break; + case { DeclaredAccessibility: Accessibility.Public, Parameters.Length: 1 } + when constructor.Parameters[0].Type.SpecialType == SpecialType.System_String: + + argumentNullExceptionStringConstructor = constructor; + break; + } + } + + if (argumentNullExceptionConstructor is null || argumentNullExceptionStringConstructor is null) + { + return; + } + + var objectType = compilation.GetSpecialType(SpecialType.System_Object); + var referenceEqualsMethod = (IMethodSymbol?)objectType + .GetMembers(nameof(ReferenceEquals)) + .FirstOrDefault(m => m is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, Parameters.Length: 2 }); + + // We are potentially interested in any declaration that has parameters. + // However, we avoid indexers specifically because of the complexity of locating and deleting equivalent null checks across multiple accessors. + context.RegisterSyntaxNodeAction( + context => AnalyzeSyntax(context, argumentNullExceptionConstructor, argumentNullExceptionStringConstructor, referenceEqualsMethod), + SyntaxKind.ConstructorDeclaration, + SyntaxKind.MethodDeclaration, + SyntaxKind.LocalFunctionStatement, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.AnonymousMethodExpression, + SyntaxKind.OperatorDeclaration, + SyntaxKind.ConversionOperatorDeclaration); + }); + + private void AnalyzeSyntax( + SyntaxNodeAnalysisContext context, + IMethodSymbol argumentNullExceptionConstructor, + IMethodSymbol argumentNullExceptionStringConstructor, + IMethodSymbol? referenceEqualsMethod) + { + var cancellationToken = context.CancellationToken; + + var semanticModel = context.SemanticModel; + var syntaxTree = semanticModel.SyntaxTree; + + var option = context.Options.GetOption(CSharpCodeStyleOptions.PreferParameterNullChecking, syntaxTree, cancellationToken); + if (!option.Value) + { + return; + } + + var node = context.Node; + var block = node switch + { + MethodDeclarationSyntax methodDecl => methodDecl.Body, + ConstructorDeclarationSyntax constructorDecl => constructorDecl.Body, + LocalFunctionStatementSyntax localFunctionStatement => localFunctionStatement.Body, + AnonymousFunctionExpressionSyntax anonymousFunction => anonymousFunction.Block, + OperatorDeclarationSyntax operatorDecl => operatorDecl.Body, + ConversionOperatorDeclarationSyntax conversionDecl => conversionDecl.Body, + _ => throw ExceptionUtilities.UnexpectedValue(node) + }; + + // More scenarios should be supported eventually: https://github.com/dotnet/roslyn/issues/58699 + if (block is null) + { + return; + } + + var methodSymbol = node is AnonymousFunctionExpressionSyntax + ? (IMethodSymbol?)semanticModel.GetSymbolInfo(node, cancellationToken).Symbol + : (IMethodSymbol?)semanticModel.GetDeclaredSymbol(node, cancellationToken); + if (methodSymbol is null || methodSymbol.Parameters.IsEmpty) + { + return; + } + + foreach (var statement in block.Statements) + { + var parameter = TryGetParameterNullCheckedByStatement(statement); + if (ParameterCanUseNullChecking(parameter) + && parameter.DeclaringSyntaxReferences.FirstOrDefault() is SyntaxReference reference + && reference.SyntaxTree.Equals(statement.SyntaxTree) + && reference.GetSyntax() is ParameterSyntax parameterSyntax) + { + context.ReportDiagnostic(DiagnosticHelper.Create( + Descriptor, + statement.GetLocation(), + option.Notification.Severity, + additionalLocations: new[] { parameterSyntax.GetLocation() }, + properties: null)); + } + } + + return; + + bool ParameterCanUseNullChecking([NotNullWhen(true)] IParameterSymbol? parameter) + { + if (parameter is null) + return false; + + if (parameter.RefKind != RefKind.None) + return false; + + if (parameter.Type.IsValueType) + { + return parameter.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T + || parameter.Type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer; + } + + return true; + } + + IParameterSymbol? TryGetParameterNullCheckedByStatement(StatementSyntax statement) + { + 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 } + // Only suggest the fix on built-in `==` operators where we know we won't change behavior + when semanticModel.GetSymbolInfo(binary).Symbol is IMethodSymbol { MethodKind: MethodKind.BuiltinOperator }: + + 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 } } + when referenceEqualsMethod != null && referenceEqualsMethod.Equals(semanticModel.GetSymbolInfo(receiver, cancellationToken).Symbol): + + left = arguments[0].Expression; + right = arguments[1].Expression; + break; + + default: + return null; + } + + var parameterInBinary = left.IsKind(SyntaxKind.NullLiteralExpression) ? TryGetParameter(right) + : right.IsKind(SyntaxKind.NullLiteralExpression) ? TryGetParameter(left) + : null; + if (parameterInBinary is null) + { + return null; + } + + var throwStatement = ifStatement.Statement switch + { + ThrowStatementSyntax @throw => @throw, + BlockSyntax { Statements: { Count: 1 } statements } => statements[0] as ThrowStatementSyntax, + _ => null + }; + + if (throwStatement?.Expression is not ObjectCreationExpressionSyntax thrownInIf + || !IsConstructorApplicable(thrownInIf, parameterInBinary)) + { + return null; + } + + return parameterInBinary; + + // 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 } + } + } + }: + var coalescedParameter = TryGetParameter(maybeParameter); + if (coalescedParameter is null || !IsConstructorApplicable(thrownInNullCoalescing, coalescedParameter)) + { + return null; + } + + return coalescedParameter; + + default: + return null; + } + } + + IParameterSymbol? TryGetParameter(ExpressionSyntax maybeParameter) + { + // `(object)x == null` is often used to ensure reference equality is used. + // therefore, we specially unwrap casts when the cast is to `object`. + if (maybeParameter is CastExpressionSyntax { Type: var type, Expression: var operand }) + { + if (semanticModel.GetTypeInfo(type).Type?.SpecialType != SpecialType.System_Object) + { + return null; + } + + maybeParameter = operand; + } + + if (semanticModel.GetSymbolInfo(maybeParameter).Symbol is not IParameterSymbol { ContainingSymbol: { } containingSymbol } parameterSymbol || !containingSymbol.Equals(methodSymbol)) + { + return null; + } + + return parameterSymbol; + } + + bool IsConstructorApplicable(ObjectCreationExpressionSyntax exceptionCreation, IParameterSymbol parameterSymbol) + { + if (exceptionCreation.ArgumentList?.Arguments is not { } arguments) + { + return false; + } + + // 'new ArgumentNullException()' + if (argumentNullExceptionConstructor.Equals(semanticModel.GetSymbolInfo(exceptionCreation, cancellationToken).Symbol)) + { + return arguments.Count == 0; + } + + // 'new ArgumentNullException(nameof(param))' (or equivalent) + if (!argumentNullExceptionStringConstructor.Equals(semanticModel.GetSymbolInfo(exceptionCreation, cancellationToken).Symbol)) + { + return false; + } + + if (arguments.Count != 1) + { + return false; + } + + var constantValue = semanticModel.GetConstantValue(arguments[0].Expression, cancellationToken); + if (constantValue.Value is not string constantString || !string.Equals(constantString, parameterSymbol.Name, StringComparison.Ordinal)) + { + return false; + } + + return true; + } + } + } +} diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.cs.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.cs.xlf index be38ae577fa07..d3bf326ebc1a3 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.cs.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.cs.xlf @@ -262,6 +262,11 @@ Použít new(...) {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Použít porovnávání vzorů diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.de.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.de.xlf index de593ee65610b..3e5a10a5c1b01 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.de.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.de.xlf @@ -262,6 +262,11 @@ "new(...)" verwenden {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Musterabgleich verwenden diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.es.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.es.xlf index ad0f16fb97f56..487cb6690ab0d 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.es.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.es.xlf @@ -262,6 +262,11 @@ Usar "new(...)" {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Usar coincidencia de patrones diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.fr.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.fr.xlf index f9cf8172a54d7..3ea7e6285c43f 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.fr.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.fr.xlf @@ -262,6 +262,11 @@ Utiliser 'new(...)' {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Utiliser les critères spéciaux diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.it.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.it.xlf index 2bced89905496..e5166f9b4e1c5 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.it.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.it.xlf @@ -262,6 +262,11 @@ Usa 'new(...)' {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Usa i criteri di ricerca diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ja.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ja.xlf index f36328909a864..06648a84234eb 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ja.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ja.xlf @@ -262,6 +262,11 @@ 'new(...)' を使用する {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching パターン マッチングを使用します diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ko.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ko.xlf index ce043a110e343..c48fe7cd0888d 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ko.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ko.xlf @@ -262,6 +262,11 @@ 'new(...)' 사용 {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching 패턴 일치 사용 diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pl.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pl.xlf index af3991d281da5..92deaef069791 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pl.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pl.xlf @@ -262,6 +262,11 @@ Użyj operatora „new(...)” {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Użyj dopasowywania wzorców diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pt-BR.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pt-BR.xlf index 67408b2147adf..52318280707e5 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pt-BR.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.pt-BR.xlf @@ -262,6 +262,11 @@ Usar 'new(...)' {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Usar a correspondência de padrão diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ru.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ru.xlf index 3e87bac5a4c3b..98bde1d848ccc 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ru.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.ru.xlf @@ -262,6 +262,11 @@ Используйте "new(...)". {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Используйте сопоставление шаблонов diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.tr.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.tr.xlf index f3a2d5a24347d..6627d78551333 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.tr.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.tr.xlf @@ -262,6 +262,11 @@ 'new(...)' kullanın {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching Desen eşleştirme kullan diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hans.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hans.xlf index 7c6167a7e02dc..cf626f8bbc06d 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hans.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hans.xlf @@ -262,6 +262,11 @@ 使用 "new(...)" {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching 使用模式匹配 diff --git a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hant.xlf b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hant.xlf index 25266870695f0..c43a1e4f62e47 100644 --- a/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hant.xlf +++ b/src/Analyzers/CSharp/Analyzers/xlf/CSharpAnalyzersResources.zh-Hant.xlf @@ -262,6 +262,11 @@ 使用 'new(...)' {Locked="new(...)"} This is a C# construct and should not be localized. + + Use parameter null checking + Use parameter null checking + + Use pattern matching 使用模式比對 diff --git a/src/Analyzers/CSharp/CodeFixes/CSharpCodeFixes.projitems b/src/Analyzers/CSharp/CodeFixes/CSharpCodeFixes.projitems index c58db92c96184..d411389a4b28d 100644 --- a/src/Analyzers/CSharp/CodeFixes/CSharpCodeFixes.projitems +++ b/src/Analyzers/CSharp/CodeFixes/CSharpCodeFixes.projitems @@ -51,6 +51,7 @@ + diff --git a/src/Analyzers/CSharp/CodeFixes/UseParameterNullChecking/CSharpUseParameterNullCheckingCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/UseParameterNullChecking/CSharpUseParameterNullCheckingCodeFixProvider.cs new file mode 100644 index 0000000000000..5bc8f3a915a18 --- /dev/null +++ b/src/Analyzers/CSharp/CodeFixes/UseParameterNullChecking/CSharpUseParameterNullCheckingCodeFixProvider.cs @@ -0,0 +1,102 @@ +// 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.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.UseParameterNullChecking +{ + [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseParameterNullChecking), Shared] + internal sealed class CSharpUseParameterNullCheckingCodeFixProvider : SyntaxEditorBasedCodeFixProvider + { + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CSharpUseParameterNullCheckingCodeFixProvider() + { + } + + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(IDEDiagnosticIds.UseParameterNullCheckingId); + + internal sealed override CodeFixCategory CodeFixCategory => CodeFixCategory.CodeStyle; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics[0]; + context.RegisterCodeFix( + new MyCodeAction(c => FixAsync(context.Document, diagnostic, c)), + context.Diagnostics); + return Task.CompletedTask; + } + + protected override Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CancellationToken cancellationToken) + { + // Tracking parameters which have already been fixed by a fix-all operation. + // This avoids crashing the fixer when the same parameter is null-tested multiple times. + using var _ = PooledHashSet.GetInstance(out var fixedParameterLocations); + foreach (var diagnostic in diagnostics) + { + var node = diagnostic.Location.FindNode(getInnermostNodeForTie: true, cancellationToken: cancellationToken); + switch (node) + { + case IfStatementSyntax ifStatement: + editor.RemoveNode(ifStatement); + break; + case ExpressionStatementSyntax expressionStatement: + // this.item = item ?? throw new ArgumentNullException(nameof(item)); + var assignment = (AssignmentExpressionSyntax)expressionStatement.Expression; + var nullCoalescing = (BinaryExpressionSyntax)assignment.Right; + var parameterReferenceSyntax = nullCoalescing.Left; + editor.ReplaceNode(nullCoalescing, parameterReferenceSyntax.WithAppendedTrailingTrivia(SyntaxFactory.ElasticMarker)); + break; + default: + throw ExceptionUtilities.UnexpectedValue(node); + } + + var parameterLocation = diagnostic.AdditionalLocations[0]; + if (fixedParameterLocations.Add(parameterLocation)) + { + var parameterSyntax = (ParameterSyntax)parameterLocation.FindNode(cancellationToken); + if (parameterSyntax.ExclamationExclamationToken.IsKind(SyntaxKind.None)) + { + var identifier = parameterSyntax.Identifier; + var newIdentifier = identifier.WithoutTrailingTrivia(); + var newExclamationExclamationToken = SyntaxFactory.Token(SyntaxTriviaList.Empty, SyntaxKind.ExclamationExclamationToken, identifier.TrailingTrivia); + editor.ReplaceNode(parameterSyntax, parameterSyntax.Update( + parameterSyntax.AttributeLists, + parameterSyntax.Modifiers, + parameterSyntax.Type, + newIdentifier, + newExclamationExclamationToken, + parameterSyntax.Default)); + } + } + } + + return Task.CompletedTask; + } + + private class MyCodeAction : CustomCodeActions.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(CSharpAnalyzersResources.Use_parameter_null_checking, createChangedDocument, nameof(CSharpUseParameterNullCheckingCodeFixProvider)) + { + } + } + } +} diff --git a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems index f084e0292c272..2aa0e218be626 100644 --- a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems +++ b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems @@ -92,6 +92,7 @@ + diff --git a/src/Analyzers/CSharp/Tests/UseParameterNullChecking/UseParameterNullCheckingTests.cs b/src/Analyzers/CSharp/Tests/UseParameterNullChecking/UseParameterNullCheckingTests.cs new file mode 100644 index 0000000000000..bc69220f51600 --- /dev/null +++ b/src/Analyzers/CSharp/Tests/UseParameterNullChecking/UseParameterNullCheckingTests.cs @@ -0,0 +1,1549 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; +using Microsoft.CodeAnalysis.CSharp.UseParameterNullChecking; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics; +using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseParameterNullChecking +{ + using VerifyCS = CSharpCodeFixVerifier; + + public class UseParameterNullCheckingTests + { + [Theory] + [Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + [InlineData("==")] + [InlineData("is")] + public async Task TestNoBraces(string @operator) + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string s) + { + [|if (s " + @operator + @" null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Theory] + [Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + [InlineData("==")] + [InlineData("is")] + public async Task TestWithBraces(string @operator) + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string s) + { + [|if (s " + @operator + @" null) + { + throw new ArgumentNullException(nameof(s)); + }|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Theory] + [Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + [InlineData("==")] + [InlineData("is")] + public async Task TestLocalFunction(string @operator) + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M() + { + local(""""); + void local(string s) + { + [|if (s " + @operator + @" null) + { + throw new ArgumentNullException(nameof(s)); + }|] + } + } +}", + FixedCode = @" +using System; + +class C +{ + void M() + { + local(""""); + void local(string s!!) + { + } + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestEqualitySwapped() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string s) + { + [|if (null == (object)s) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNotEquality() + { + var testCode = @" +using System; + +class C +{ + void M(string s) + { + if ((object)s != null) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNullCoalescingThrow() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + private readonly string s; + public C(string s) + { + [|this.s = s ?? throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @" +using System; + +class C +{ + private readonly string s; + public C(string s!!) + { + this.s = s; + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestOperator() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + public static C operator +(C c, string s) + { + [|if (s is null) + throw new ArgumentNullException(nameof(s));|] + + return new C(); + } +}", + FixedCode = @" +using System; + +class C +{ + public static C operator +(C c, string s!!) + { + return new C(); + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestConversionOperator() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + public static implicit operator C(string s) + { + [|if (s is null) + throw new ArgumentNullException(nameof(s));|] + + return new C(); + } +}", + FixedCode = @" +using System; + +class C +{ + public static implicit operator C(string s!!) + { + return new C(); + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNullCoalescingThrowExpressionBody() + { + // We'd like to support this eventually. https://github.com/dotnet/roslyn/issues/58699 + var testCode = @" +using System; + +class C +{ + private readonly string s; + public C(string s) + => this.s = s ?? throw new ArgumentNullException(nameof(s)); +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNullCoalescingThrowBaseClause() + { + // We'd like to support this eventually. https://github.com/dotnet/roslyn/issues/58699 + var testCode = @" +using System; + +class Base { public Base(string s) { } } +class C : Base +{ + public C(string s) : base(s ?? throw new ArgumentNullException(nameof(s))) { } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestAlreadyNullChecked() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C(string s!!) + { + [|if (s is null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @"using System; + +class C +{ + public C(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestSingleLineNoBraces() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C(string s) + { + [|if (s is null) throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @"using System; + +class C +{ + public C(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestSingleLineWithBraces() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C(string s) + { + [|if (s is null) { throw new ArgumentNullException(nameof(s)); }|] + } +}", + FixedCode = @"using System; + +class C +{ + public C(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestLeadingAndTrailingTrivia1() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C(string s) + { + // comment1 + [|if (s is null) { throw new ArgumentNullException(nameof(s)); }|] // comment2 + } +}", + FixedCode = @"using System; + +class C +{ + public C(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestLeadingAndTrailingTrivia2() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C( + string x // comment + , string y // comment, + , string z // comment + ) + { + [|if (x is null) throw new ArgumentNullException(nameof(x));|] + [|if (y is null) throw new ArgumentNullException(nameof(y));|] + [|if (z is null) throw new ArgumentNullException(nameof(z));|] + } +}", + FixedCode = @"using System; + +class C +{ + public C( + string x!! // comment + , string y!! // comment, + , string z!! // comment + ) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestLeadingAndTrailingTrivia3() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C( + string x /* comment */ !! + ) + { + [|if (x is null) throw new ArgumentNullException(nameof(x));|] + } +}", + FixedCode = @"using System; + +class C +{ + public C( + string x /* comment */ !! + ) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestAssignThenTest() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + public C(string s) + { + s = ""a""; + [|if (s is null) { throw new ArgumentNullException(nameof(s)); }|] + } +}", + FixedCode = @"using System; + +class C +{ + public C(string s!!) + { + s = ""a""; + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestRedundantNullChecks() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + public C(string s) + { + [|if (s is null) + throw new ArgumentNullException(nameof(s));|] + + [|if (s is null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = +@" +using System; + +class C +{ + public C(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Theory, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + [InlineData("ref")] + [InlineData("in")] + [InlineData("out")] + public async Task TestRefParameter(string refKind) + { + // https://github.com/dotnet/roslyn/issues/58699 + // When the implementation changes to permit ref/in parameters, we should also change the fixer. + var testCode = @" +using System; + +class C +{ + public C(" + refKind + @" string s) + { + if (s is null) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + CompilerDiagnostics = Testing.CompilerDiagnostics.None, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Theory, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + [InlineData("object")] + [InlineData("C")] + public async Task TestReferenceEqualsCheck(string className) + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + private readonly string s; + void M1(string s) + { + [|if (" + className + @".ReferenceEquals(s, null)) + { + throw new ArgumentNullException(nameof(s)); + }|] + } + + void M2(string s) + { + [|if (" + className + @".ReferenceEquals(null, s)) + { + throw new ArgumentNullException(nameof(s)); + }|] + } +}", + FixedCode = @" +using System; + +class C +{ + private readonly string s; + void M1(string s!!) + { + } + + void M2(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNotCustomReferenceEqualsCheck() + { + var testCode = @" +using System; + +class C +{ + private readonly string s; + void M(string s) + { + if (ReferenceEquals(s, null)) + { + throw new ArgumentNullException(nameof(s)); + } + } + + bool ReferenceEquals(object o1, object o2) => false; +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNotEqualitySwapped() + { + // https://github.com/dotnet/roslyn/issues/58699 + var testCode = @"using System; + +class C +{ + void M(string s) + { + if (null != (object)s) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestMissingPreCSharp11() + { + var testCode = @" +using System; + +class C +{ + void M(string s) + { + if ((object)s == null) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersion.CSharp10 + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestOnlyForObjectCast() + { + var testCode = @"using System; + +class C +{ + void M(string s) + { + if ((string)s == null) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestFixAll1() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string s1, string s2) + { + [|if ((object)s1 == null) + throw new ArgumentNullException(nameof(s1));|] + + [|if (null == (object)s2) + throw new ArgumentNullException(nameof(s2));|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string s1!!, string s2!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNested1() + { + var testCode = @"using System; + +class C +{ + void M(string s2) + { + if ((object)((object)s2 == null) == null) + throw new ArgumentNullException(nameof(s2)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNestedStatements() + { + var testCode = @"using System; + +class C +{ + void M(string s2) + { + { + if (s2 == null) + throw new ArgumentNullException(nameof(s2)); + } + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestConstrainedTypeParameter() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(T s) where T : class + { + [|if ((object)s == null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(T s!!) where T : class + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestOnStructConstrainedTypeParameter() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M1(T s) where T : struct + { + if ((object)s == null) + throw new ArgumentNullException(nameof(s)); + } + + void M2(T? s) where T : struct + { + [|if ((object)s == null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M1(T s) where T : struct + { + if ((object)s == null) + throw new ArgumentNullException(nameof(s)); + } + + void M2(T? s!!) where T : struct + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestUnconstrainedTypeParameter() + { + await new VerifyCS.Test() + { + TestCode = +@"using System; + +class C +{ + void M(T s) + { + [|if ((object)s == null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = +@"using System; + +class C +{ + void M(T s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestComplexExpr() + { + var testCode = @"using System; + +class C +{ + void M(string[] s) + { + if ((object)s[0] == null) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestComplexExpr2() + { + var testCode = @"using System; + +class C +{ + void M(string s1, string s2) + { + if (s1 == null || s2 == null) + throw new ArgumentNullException(nameof(s1)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNotOnDefault() + { + var testCode = @" +using System; + +class C +{ + void M(string s) + { + if ((object)s == default) + throw new ArgumentNullException(nameof(s)); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestOptionalParameter() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string s = ""a"") + { + [|if ((object)s == null) + throw new ArgumentNullException(nameof(s));|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string s!! = ""a"") + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithStringExceptionArgument() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string s) + { + [|if (s == null) + throw new ArgumentNullException(""s"");|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithStringExceptionArgument2() + { + await new VerifyCS.Test() + { + TestCode = @" +using System; + +class C +{ + void M(string HelloWorld) + { + [|if (HelloWorld == null) + throw new ArgumentNullException(""Hello"" + ""World"");|] + } +}", + FixedCode = @" +using System; + +class C +{ + void M(string HelloWorld!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNotWithUnexpectedExceptionArgument() + { + var testCode = @"using System; + +class C +{ + void M(string s) + { + if (s == null) + throw new ArgumentNullException(""banana""); + } +}"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithExceptionNoArguments() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + void M(string s) + { + [|if (s == null) + throw new ArgumentNullException();|] + } +}", + FixedCode = @"using System; + +class C +{ + void M(string s!!) + { + } +}", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestSimpleLambda() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + Action lambda = x => + { + [|if (x is null) + throw new ArgumentNullException(nameof(x));|] + }; +} +", + FixedCode = @"using System; + +class C +{ + Action lambda = x!! => + { + }; +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestParenthesizedLambdaNoType() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + Action lambda = (x) => + { + [|if (x is null) + throw new ArgumentNullException(nameof(x));|] + }; +} +", + FixedCode = @"using System; + +class C +{ + Action lambda = (x!!) => + { + }; +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestParenthesizedLambdaWithType() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + Action lambda = (string x) => + { + [|if (x is null) + throw new ArgumentNullException(nameof(x));|] + }; +} +", + FixedCode = @"using System; + +class C +{ + Action lambda = (string x!!) => + { + }; +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestAnonymousMethod() + { + await new VerifyCS.Test() + { + TestCode = @"using System; + +class C +{ + Action lambda = delegate (string x) + { + [|if (x is null) + throw new ArgumentNullException(nameof(x));|] + }; +} +", + FixedCode = @"using System; + +class C +{ + Action lambda = delegate (string x!!) + { + }; +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestLocalFunctionWithinAccessor() + { + var testCode = @"using System; +class C +{ + private int _p; + + public int P + { + get => _p; + set + { + local(); + + void local() + { + if (value == null) throw new ArgumentNullException(nameof(value)); + _p = value; + } + } + } +} +"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestLocalFunctionCapturingParameter() + { + var testCode = @"using System; +class C +{ + void M(string param) + { + local(); + + void local() + { + if (param == null) + throw new ArgumentNullException(nameof(param)); + } + } +} +"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestNotWithUserDefinedOperator() + { + var testCode = @"using System; +class C +{ + public static bool operator ==(C c1, C c2) => true; + public static bool operator !=(C c1, C c2) => false; + + static void M(C c) + { + if (c == null) + throw new ArgumentNullException(nameof(c)); + } +} +"; + await new VerifyCS.Test() + { + TestCode = testCode, + FixedCode = testCode, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithUnusedUserDefinedOperator() + { + await new VerifyCS.Test() + { + TestCode = @"using System; +class C +{ + public static bool operator ==(C c1, C c2) => true; + public static bool operator !=(C c1, C c2) => false; + + static void M(C c) + { + [|if ((object)c == null) + throw new ArgumentNullException(nameof(c));|] + } +} +", + FixedCode = @"using System; +class C +{ + public static bool operator ==(C c1, C c2) => true; + public static bool operator !=(C c1, C c2) => false; + + static void M(C c!!) + { + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestIsWithUnusedUserDefinedOperator() + { + await new VerifyCS.Test() + { + TestCode = @"using System; +class C +{ + public static bool operator ==(C c1, C c2) => true; + public static bool operator !=(C c1, C c2) => false; + + static void M(C c) + { + [|if (c is null) + throw new ArgumentNullException(nameof(c));|] + } +} +", + FixedCode = @"using System; +class C +{ + public static bool operator ==(C c1, C c2) => true; + public static bool operator !=(C c1, C c2) => false; + + static void M(C c!!) + { + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithPreprocessorDirective() + { + await new VerifyCS.Test() + { + TestCode = @"using System; +class C +{ + static void M(C c) + { +#nullable enable + [|if ((object)c == null) + throw new ArgumentNullException(nameof(c));|] +#nullable disable + } +} +", + FixedCode = @"using System; +class C +{ + static void M(C c!!) + { +#nullable disable + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithIfPreprocessorDirective() + { + await new VerifyCS.Test() + { + TestCode = @"#define DEBUG +using System; +class C +{ + static void M(C c) + { +#if DEBUG + [|if ((object)c == null) + throw new ArgumentNullException(nameof(c));|] +#endif + } +} +", + FixedCode = @"#define DEBUG +using System; +class C +{ + static void M(C c!!) + { + +#if DEBUG +#endif + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestWithAlias() + { + await new VerifyCS.Test() + { + TestCode = @"using Ex = System.ArgumentNullException; +class C +{ + static void M(C c) + { + [|if ((object)c == null) + throw new Ex(nameof(c));|] + } +} +", + FixedCode = @"using Ex = System.ArgumentNullException; +class C +{ + static void M(C c!!) + { + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestPointer() + { + await new VerifyCS.Test() + { + TestCode = @"using System; +class C +{ + static unsafe void M(int* ptr) + { + [|if (ptr == null) + throw new ArgumentNullException(nameof(ptr));|] + } +} +", + FixedCode = @"using System; +class C +{ + static unsafe void M(int* ptr!!) + { + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestFunctionPointer() + { + await new VerifyCS.Test() + { + TestCode = @"using System; +class C +{ + static unsafe void M(delegate* ptr) + { + [|if (ptr == null) + throw new ArgumentNullException(nameof(ptr));|] + } +} +", + FixedCode = @"using System; +class C +{ + static unsafe void M(delegate* ptr!!) + { + } +} +", + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseIsNullCheck)] + public async Task TestPartialMethod() + { + var partialDeclaration = @" +partial class C +{ + static partial void M(C c); +} +"; + + await new VerifyCS.Test() + { + TestState = + { + Sources = + { + partialDeclaration, + @"using System; +partial class C +{ + static partial void M(C c) + { + [|if ((object)c == null) + throw new ArgumentNullException(nameof(c));|] + } +} +" + } + }, + FixedState = + { + Sources = + { + partialDeclaration, + @"using System; +partial class C +{ + static partial void M(C c!!) + { + } +} +" + } + }, + LanguageVersion = LanguageVersionExtensions.CSharpNext + }.RunAsync(); + } + } +} diff --git a/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs b/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs index e1026b4c559a1..4f5e6d27eedfe 100644 --- a/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs +++ b/src/Analyzers/Core/Analyzers/EnforceOnBuildValues.cs @@ -87,6 +87,7 @@ internal static class EnforceOnBuildValues public const EnforceOnBuild PopulateSwitchStatement = /*IDE0010*/ EnforceOnBuild.WhenExplicitlyEnabled; public const EnforceOnBuild UseInferredMemberName = /*IDE0037*/ EnforceOnBuild.WhenExplicitlyEnabled; public const EnforceOnBuild UseIsNullCheck = /*IDE0041*/ EnforceOnBuild.WhenExplicitlyEnabled; + public const EnforceOnBuild UseParameterNullChecking = /*IDE0190*/ EnforceOnBuild.WhenExplicitlyEnabled; public const EnforceOnBuild AddRequiredParentheses = /*IDE0048*/ EnforceOnBuild.WhenExplicitlyEnabled; public const EnforceOnBuild ExpressionValueIsUnused = /*IDE0058*/ EnforceOnBuild.WhenExplicitlyEnabled; public const EnforceOnBuild MakeStructFieldsWritable = /*IDE0064*/ EnforceOnBuild.WhenExplicitlyEnabled; diff --git a/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs b/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs index 0b02a866c04ed..592390e6f38ab 100644 --- a/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs +++ b/src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs @@ -167,6 +167,8 @@ internal static class IDEDiagnosticIds public const string UseTupleSwapDiagnosticId = "IDE0180"; + public const string UseParameterNullCheckingId = "IDE0190"; + // Analyzer error Ids public const string AnalyzerChangedId = "IDE1001"; public const string AnalyzerDependencyConflictId = "IDE1002"; diff --git a/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs b/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs index 68ee4523ee54a..e84bd13bf549f 100644 --- a/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs +++ b/src/Analyzers/Core/CodeFixes/PredefinedCodeFixProviderNames.cs @@ -133,6 +133,7 @@ internal static class PredefinedCodeFixProviderNames public const string UseInterpolatedVerbatimString = nameof(UseInterpolatedVerbatimString); public const string UseIsNotExpression = nameof(UseIsNotExpression); public const string UseIsNullCheck = nameof(UseIsNullCheck); + public const string UseParameterNullChecking = nameof(UseParameterNullChecking); public const string UseIsNullCheckForCastAndEqualityOperator = nameof(UseIsNullCheckForCastAndEqualityOperator); public const string UseIsNullCheckForReferenceEquals = nameof(UseIsNullCheckForReferenceEquals); public const string UseLocalFunction = nameof(UseLocalFunction); diff --git a/src/Compilers/Test/Core/Traits/Traits.cs b/src/Compilers/Test/Core/Traits/Traits.cs index 66a26418fe87d..cb0992ea28c49 100644 --- a/src/Compilers/Test/Core/Traits/Traits.cs +++ b/src/Compilers/Test/Core/Traits/Traits.cs @@ -194,6 +194,7 @@ public static class Features public const string CodeActionsUseInterpolatedVerbatimString = "CodeActions.UseInterpolatedVerbatimString"; public const string CodeActionsUseIsNotExpression = "CodeActions.UseIsNotExpression"; public const string CodeActionsUseIsNullCheck = "CodeActions.UseIsNullCheck"; + public const string CodeActionsUseParameterNullChecking = "CodeActions.UseParameterNullChecking"; public const string CodeActionsUseLocalFunction = "CodeActions.UseLocalFunction"; public const string CodeActionsUseNamedArguments = "CodeActions.UseNamedArguments"; public const string CodeActionsUseNotPattern = "CodeActions.UseNotPattern"; diff --git a/src/EditorFeatures/CSharpTest/InitializeParameter/AddParameterCheckTests.cs b/src/EditorFeatures/CSharpTest/InitializeParameter/AddParameterCheckTests.cs index 7470bfef5aabb..5a7a43b86ad35 100644 --- a/src/EditorFeatures/CSharpTest/InitializeParameter/AddParameterCheckTests.cs +++ b/src/EditorFeatures/CSharpTest/InitializeParameter/AddParameterCheckTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -76,6 +76,27 @@ public C([||]string s!!) }.RunAsync(); } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInitializeParameter)] + public async Task TestRecordPrimaryConstructor() + { + // https://github.com/dotnet/roslyn/issues/58779 + // Note: we declare a field within the record to work around missing IsExternalInit errors + await new VerifyCS.Test + { + LanguageVersion = LanguageVersionExtensions.CSharpNext, + TestCode = @" +using System; + +record Rec([||]string s) { public string s = s; } +", + FixedCode = @" +using System; + +record Rec(string s) { public string s = s; } +" + }.RunAsync(); + } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsInitializeParameter)] public async Task TestSimpleReferenceType_AlreadyNullChecked2() { diff --git a/src/EditorFeatures/Test/Diagnostics/IDEDiagnosticIDConfigurationTests.cs b/src/EditorFeatures/Test/Diagnostics/IDEDiagnosticIDConfigurationTests.cs index 02c55dcec4e8c..5d8b95a977999 100644 --- a/src/EditorFeatures/Test/Diagnostics/IDEDiagnosticIDConfigurationTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/IDEDiagnosticIDConfigurationTests.cs @@ -429,6 +429,9 @@ public void CSharp_VerifyIDEDiagnosticSeveritiesAreConfigurable() # IDE0180 dotnet_diagnostic.IDE0180.severity = %value% +# IDE0190 +dotnet_diagnostic.IDE0190.severity = %value% + # IDE2000 dotnet_diagnostic.IDE2000.severity = %value% @@ -1010,6 +1013,9 @@ No editorconfig based code style option # IDE0180, PreferTupleSwap csharp_style_prefer_tuple_swap = true +# IDE0190, PreferParameterNullChecking +csharp_style_prefer_parameter_null_checking = true + # IDE1005, PreferConditionalDelegateCall csharp_style_conditional_delegate_call = true diff --git a/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx b/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx index 258958fa8a8b1..40a3b3131603e 100644 --- a/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx +++ b/src/VisualStudio/CSharp/Impl/CSharpVSResources.resx @@ -524,6 +524,9 @@ Prefer 'is null' for reference equality checks 'is null' is a C# string and should not be localized. + + Prefer parameter null checking + Report invalid placeholders in 'string.Format' calls diff --git a/src/VisualStudio/CSharp/Impl/EditorConfigSettings/DataProvider/CodeStyle/CSharpCodeStyleSettingsProvider.cs b/src/VisualStudio/CSharp/Impl/EditorConfigSettings/DataProvider/CodeStyle/CSharpCodeStyleSettingsProvider.cs index 1e726b9de6f24..5a391c3d78d37 100644 --- a/src/VisualStudio/CSharp/Impl/EditorConfigSettings/DataProvider/CodeStyle/CSharpCodeStyleSettingsProvider.cs +++ b/src/VisualStudio/CSharp/Impl/EditorConfigSettings/DataProvider/CodeStyle/CSharpCodeStyleSettingsProvider.cs @@ -103,6 +103,10 @@ private IEnumerable GetNullCheckingCodeStyleOptions(AnalyzerCo description: CSharpVSResources.Prefer_null_check_over_type_check, editorConfigOptions: editorConfigOptions, visualStudioOptions: visualStudioOptions, updater: updaterService, fileName: FileName); + yield return CodeStyleSetting.Create(option: CSharpCodeStyleOptions.PreferParameterNullChecking, + description: CSharpVSResources.Prefer_parameter_null_checking, + editorConfigOptions: editorConfigOptions, + visualStudioOptions: visualStudioOptions, updater: updaterService, fileName: FileName); } private IEnumerable GetModifierCodeStyleOptions(AnalyzerConfigOptions editorConfigOptions, OptionSet visualStudioOptions, OptionUpdater updaterService) diff --git a/src/VisualStudio/CSharp/Impl/Options/AutomationObject/AutomationObject.Style.cs b/src/VisualStudio/CSharp/Impl/Options/AutomationObject/AutomationObject.Style.cs index e8f977663d36b..f4b23b785a1b0 100644 --- a/src/VisualStudio/CSharp/Impl/Options/AutomationObject/AutomationObject.Style.cs +++ b/src/VisualStudio/CSharp/Impl/Options/AutomationObject/AutomationObject.Style.cs @@ -237,6 +237,12 @@ public string Style_PreferIsNullCheckOverReferenceEqualityMethod set { SetXmlOption(CodeStyleOptions2.PreferIsNullCheckOverReferenceEqualityMethod, value); } } + public string Style_PreferParameterNullChecking + { + get { return GetXmlOption(CSharpCodeStyleOptions.PreferParameterNullChecking); } + set { SetXmlOption(CSharpCodeStyleOptions.PreferParameterNullChecking, value); } + } + public string Style_PreferNullCheckOverTypeCheck { get { return GetXmlOption(CSharpCodeStyleOptions.PreferNullCheckOverTypeCheck); } diff --git a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs index d2fbb60139c4b..4a4d360b1675b 100644 --- a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs +++ b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs @@ -1122,6 +1122,28 @@ void M2(string value1, string value2) //] }} }} +"; + + private static readonly string s_preferParameterNullChecking = $@" +using System; + +class Customer +{{ +//[ + // {ServicesVSResources.Prefer_colon} + void M1(string value!!) + {{ + }} +//] +//[ + // {ServicesVSResources.Over_colon} + void M2(string value) + {{ + if (value is null) + throw new ArgumentNullException(nameof(value)); + }} +//] +}} "; private static readonly string s_preferNullcheckOverTypeCheck = $@" @@ -2108,6 +2130,7 @@ internal StyleViewModel(OptionStore optionStore, IServiceProvider serviceProvide CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CodeStyleOptions2.PreferCoalesceExpression, ServicesVSResources.Prefer_coalesce_expression, s_preferCoalesceExpression, s_preferCoalesceExpression, this, optionStore, nullCheckingGroupTitle)); CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CodeStyleOptions2.PreferNullPropagation, ServicesVSResources.Prefer_null_propagation, s_preferNullPropagation, s_preferNullPropagation, this, optionStore, nullCheckingGroupTitle)); CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CodeStyleOptions2.PreferIsNullCheckOverReferenceEqualityMethod, CSharpVSResources.Prefer_is_null_for_reference_equality_checks, s_preferIsNullOverReferenceEquals, s_preferIsNullOverReferenceEquals, this, optionStore, nullCheckingGroupTitle)); + CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferParameterNullChecking, CSharpVSResources.Prefer_parameter_null_checking, s_preferParameterNullChecking, s_preferParameterNullChecking, this, optionStore, nullCheckingGroupTitle)); CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferNullCheckOverTypeCheck, CSharpVSResources.Prefer_null_check_over_type_check, s_preferNullcheckOverTypeCheck, s_preferNullcheckOverTypeCheck, this, optionStore, nullCheckingGroupTitle)); // Using directive preferences. diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.cs.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.cs.xlf index bcb0b9dda1e3a..c06cecf28b22f 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.cs.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.cs.xlf @@ -142,6 +142,11 @@ Upřednostňovat kontrolu hodnoty null před kontrolou typu + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Upřednostňovat porovnávání vzorů diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.de.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.de.xlf index c8727c855de95..5939f02eec3c0 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.de.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.de.xlf @@ -142,6 +142,11 @@ "NULL"-Überprüfung vor Typüberprüfung bevorzugen + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Musterabgleich bevorzugen diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.es.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.es.xlf index 9f520f7aa0694..9881a2f526e40 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.es.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.es.xlf @@ -142,6 +142,11 @@ Preferir comprobación "null' sobre comprobación de tipo + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Preferir coincidencia de patrones diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.fr.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.fr.xlf index 4b3d4bbe92c02..bd23ab7804c77 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.fr.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.fr.xlf @@ -142,6 +142,11 @@ Préférer la vérification « null » à la vérification de type + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Préférer les critères spéciaux diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.it.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.it.xlf index 436467d250a55..129b9798d0e8e 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.it.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.it.xlf @@ -142,6 +142,11 @@ Preferisci il controllo 'null' al controllo del tipo + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Preferisci i criteri di ricerca diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ja.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ja.xlf index 74ded6521f820..23702d25d215c 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ja.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ja.xlf @@ -142,6 +142,11 @@ 型のチェックよりも 'null 値' チェックを優先する + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching パターン マッチングを優先する diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ko.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ko.xlf index ae5e528b54aef..7c44d4a628d73 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ko.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ko.xlf @@ -142,6 +142,11 @@ 형식 검사보다 'null' 검사 선호 + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching 패턴 일치 선호 diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pl.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pl.xlf index 230dbc961c0fd..c6fc1fd110317 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pl.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pl.xlf @@ -142,6 +142,11 @@ Ustaw preferencje na sprawdzanie wartości „null” zamiast sprawdzania typu + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Preferuj dopasowywanie do wzorca diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pt-BR.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pt-BR.xlf index c5ae128b775b0..d6544ac27686b 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pt-BR.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.pt-BR.xlf @@ -142,6 +142,11 @@ Preferir a verificação 'nula' à verificação de tipo + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Preferir a correspondência de padrões diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ru.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ru.xlf index c020550f114e7..40f05e0f8675c 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ru.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.ru.xlf @@ -142,6 +142,11 @@ Предпочитать проверку "null" проверке типа + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Предпочитать соответствие шаблону diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.tr.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.tr.xlf index d95ae7aa6044c..c0ec9176998da 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.tr.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.tr.xlf @@ -142,6 +142,11 @@ Tür denetimi yerine 'null' denetimini tercih edin + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching Desen eşleştirmeyi tercih et diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hans.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hans.xlf index 36757db206d37..c6461e38bfd9e 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hans.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hans.xlf @@ -142,6 +142,11 @@ 与类型检查相比,首选 Null 检查 + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching 首选模式匹配 diff --git a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hant.xlf b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hant.xlf index d020b3844fde6..e922ea7b6dfd6 100644 --- a/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hant.xlf +++ b/src/VisualStudio/CSharp/Impl/xlf/CSharpVSResources.zh-Hant.xlf @@ -142,6 +142,11 @@ 建議使用 'null' 檢查而非鍵入檢查 + + Prefer parameter null checking + Prefer parameter null checking + + Prefer pattern matching 建議使用模式比對 diff --git a/src/VisualStudio/Core/Test/Options/CSharpEditorConfigGeneratorTests.vb b/src/VisualStudio/Core/Test/Options/CSharpEditorConfigGeneratorTests.vb index 9ba945865f45c..0fd050a669a7b 100644 --- a/src/VisualStudio/Core/Test/Options/CSharpEditorConfigGeneratorTests.vb +++ b/src/VisualStudio/Core/Test/Options/CSharpEditorConfigGeneratorTests.vb @@ -118,6 +118,7 @@ csharp_style_prefer_switch_expression = true # Null-checking preferences csharp_style_conditional_delegate_call = true +csharp_style_prefer_parameter_null_checking = true # Modifier preferences csharp_prefer_static_local_function = true @@ -351,6 +352,7 @@ csharp_style_prefer_switch_expression = true # Null-checking preferences csharp_style_conditional_delegate_call = true +csharp_style_prefer_parameter_null_checking = true # Modifier preferences csharp_prefer_static_local_function = true diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeStyle/CSharpCodeStyleOptions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeStyle/CSharpCodeStyleOptions.cs index be5c6225f880a..a9430fceb2571 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeStyle/CSharpCodeStyleOptions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeStyle/CSharpCodeStyleOptions.cs @@ -300,6 +300,12 @@ private static Option2> CreateUsingDirectiv "csharp_style_prefer_null_check_over_type_check", $"TextEditor.CSharp.Specific.{nameof(PreferNullCheckOverTypeCheck)}"); + internal static readonly Option2> PreferParameterNullChecking = CreateOption( + CSharpCodeStyleOptionGroups.NullCheckingPreferences, nameof(PreferParameterNullChecking), + defaultValue: s_trueWithSuggestionEnforcement, + "csharp_style_prefer_parameter_null_checking", + $"TextEditor.CSharp.Specific.{nameof(PreferParameterNullChecking)}"); + public static Option2> AllowEmbeddedStatementsOnSameLine { get; } = CreateOption( CSharpCodeStyleOptionGroups.NewLinePreferences, nameof(AllowEmbeddedStatementsOnSameLine), defaultValue: CodeStyleOptions2.TrueWithSilentEnforcement,