Background and Motivation
The parameter null checking feature #36024 needs some new public APIs and behavior changes to public API.
Proposed API
**Rejected** symbol API
namespace Microsoft.CodeAnalysis
{
public interface IParameterSymbol
{
+ /// <summary>
+ /// True if the compiler will synthesize a null check for this parameter (the parameter is declared in source with a <c>!!</c> following the parameter name).
+ /// </summary>
+ bool IsNullChecked { get; }
}
}
namespace Microsoft.CodeAnalysis.CSharp
{
public enum SyntaxKind
{
// ...
/// <summary>Represents <c>??=</c> token.</summary>
QuestionQuestionEqualsToken = 8284,
+ /// <summary>Represents <c>!!</c> token.</summary>
+ ExclamationExclamationToken = 8285,
}
}
namespace Microsoft.CodeAnalysis.CSharp.Syntax
{
public sealed class ParameterSyntax
{
+ public SyntaxToken ExclamationExclamationToken { get; }
}
}
Usage Examples
The IDE uses these APIs to properly support the feature. For example, the refactoring to add a null check for a parameter:
|
protected bool ParameterValidForNullCheck(Document document, IParameterSymbol parameter, SemanticModel semanticModel, |
|
IBlockOperation? blockStatementOpt, CancellationToken cancellationToken) |
|
{ |
|
if (parameter.Type.IsReferenceType) |
|
{ |
|
// Don't add null checks to things explicitly declared nullable |
|
if (parameter.Type.NullableAnnotation == NullableAnnotation.Annotated) |
|
{ |
|
return false; |
|
} |
|
} |
|
else if (!parameter.Type.IsNullable()) |
|
{ |
|
return false; |
|
} |
|
|
|
if (parameter.RefKind == RefKind.Out) |
|
{ |
|
return false; |
|
} |
|
|
|
if (parameter.IsNullChecked) |
|
{ |
|
return false; |
|
} |
|
protected override Document? TryAddNullCheckToParameterDeclaration(Document document, ParameterSyntax parameterSyntax, CancellationToken cancellationToken) |
|
{ |
|
var tree = parameterSyntax.SyntaxTree; |
|
var options = (CSharpParseOptions)tree.Options; |
|
if (options.LanguageVersion < LanguageVersionExtensions.CSharpNext) |
|
{ |
|
return null; |
|
} |
|
|
|
// We expect the syntax tree to already be in memory since we already have a node from the tree |
|
var syntaxRoot = tree.GetRoot(cancellationToken); |
|
syntaxRoot = syntaxRoot.ReplaceNode( |
|
parameterSyntax, |
|
parameterSyntax.WithExclamationExclamationToken(Token(SyntaxKind.ExclamationExclamationToken))); |
|
return document.WithSyntaxRoot(syntaxRoot); |
|
} |
Alternative Designs
One alternative would be to eliminate IParameterSymbol.IsNullChecked, and force public users to go through syntax and check the ParameterSyntax.ExclamationExclamationToken to determine if a parameter is null checked. It seems inconvenient, especially when it comes to IOperation scenarios, where the user may not be referencing MS.CA.CSharp but still want to know if a parameter was null checked.
Risks
Not aware of any risks from the current design.
Control flow graph
In the current feature implementation, the IOperation behavior is the same whether or not parameter null checking is used. Users are expected to grab the parameter symbol and check its IsNullChecked property if they care about it. This seems consistent with IOperation behaviors in other "implicit code" scenarios.
However, the control flow graph behavior does currently differ depending on whether parameter null checking is used. Nodes are inserted to represent where the null check occurs and what the semantics of the null check roughly are. Should the API do this? Or should the graph be the same regardless of whether parameter null-checking is used, leaving users to check the IParameterSymbol.IsNullChecked API and handle appropriately if they need to?
|
public void NullCheckedMethodDeclarationIOp() |
|
{ |
|
var source = @" |
|
public class C |
|
{ |
|
public void M(string input!!) { } |
|
}"; |
|
var compilation = CreateCompilation(source, parseOptions: TestOptions.RegularPreview); |
|
|
|
compilation.VerifyDiagnostics(); |
|
|
|
var tree = compilation.SyntaxTrees.Single(); |
|
var node1 = tree.GetRoot().DescendantNodes().OfType<BaseMethodDeclarationSyntax>().Single(); |
|
|
|
compilation.VerifyOperationTree(node1, expectedOperationTree: @" |
|
IMethodBodyOperation (OperationKind.MethodBody, Type: null) (Syntax: 'public void ... nput!!) { }') |
|
BlockBody: |
|
IBlockOperation (0 statements) (OperationKind.Block, Type: null) (Syntax: '{ }') |
|
ExpressionBody: |
|
null"); |
|
|
|
VerifyFlowGraph(compilation, node1, expectedFlowGraph: @" |
|
Block[B0] - Entry |
|
Statements (0) |
|
Next (Regular) Block[B1] |
|
Block[B1] - Block |
|
Predecessors: [B0] |
|
Statements (0) |
|
Jump if False (Regular) to Block[B3] |
|
IBinaryOperation (BinaryOperatorKind.Equals) (OperationKind.Binary, Type: System.Boolean, IsImplicit) (Syntax: 'public void ... nput!!) { }') |
|
Left: |
|
IParameterReferenceOperation: input (OperationKind.ParameterReference, Type: System.String, IsImplicit) (Syntax: 'public void ... nput!!) { }') |
|
Right: |
|
ILiteralOperation (OperationKind.Literal, Type: System.String, Constant: null, IsImplicit) (Syntax: 'public void ... nput!!) { }') |
|
Next (Regular) Block[B2] |
|
Block[B2] - Block |
Background and Motivation
The parameter null checking feature #36024 needs some new public APIs and behavior changes to public API.
Proposed API
**Rejected** symbol API
namespace Microsoft.CodeAnalysis { public interface IParameterSymbol { + /// <summary> + /// True if the compiler will synthesize a null check for this parameter (the parameter is declared in source with a <c>!!</c> following the parameter name). + /// </summary> + bool IsNullChecked { get; } } }namespace Microsoft.CodeAnalysis.CSharp { public enum SyntaxKind { // ... /// <summary>Represents <c>??=</c> token.</summary> QuestionQuestionEqualsToken = 8284, + /// <summary>Represents <c>!!</c> token.</summary> + ExclamationExclamationToken = 8285, } } namespace Microsoft.CodeAnalysis.CSharp.Syntax { public sealed class ParameterSyntax { + public SyntaxToken ExclamationExclamationToken { get; } } }Usage Examples
The IDE uses these APIs to properly support the feature. For example, the refactoring to add a null check for a parameter:
roslyn/src/Features/Core/Portable/InitializeParameter/AbstractAddParameterCheckCodeRefactoringProvider.cs
Lines 246 to 270 in 828a71d
roslyn/src/Features/CSharp/Portable/InitializeParameter/CSharpAddParameterCheckCodeRefactoringProvider.cs
Lines 97 to 112 in 828a71d
Alternative Designs
One alternative would be to eliminate
IParameterSymbol.IsNullChecked, and force public users to go through syntax and check theParameterSyntax.ExclamationExclamationTokento determine if a parameter is null checked. It seems inconvenient, especially when it comes to IOperation scenarios, where the user may not be referencingMS.CA.CSharpbut still want to know if a parameter was null checked.Risks
Not aware of any risks from the current design.
Control flow graph
In the current feature implementation, the IOperation behavior is the same whether or not parameter null checking is used. Users are expected to grab the parameter symbol and check its
IsNullCheckedproperty if they care about it. This seems consistent with IOperation behaviors in other "implicit code" scenarios.However, the control flow graph behavior does currently differ depending on whether parameter null checking is used. Nodes are inserted to represent where the null check occurs and what the semantics of the null check roughly are. Should the API do this? Or should the graph be the same regardless of whether parameter null-checking is used, leaving users to check the
IParameterSymbol.IsNullCheckedAPI and handle appropriately if they need to?roslyn/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_NullCheckedParameters.cs
Lines 19 to 54 in 6ba3dea