#5: Implement inference for `[NotNullWhen(bool)]` attribute.
The overall idea here is: * in addition to the primary node for the parameter's type, we have two additional nodes whenTrue/whenFalse for out parameters on methods that return a boolean. * on return true|false;, connect the current flow-state of the parameter with the whenTrue/whenFalse node. * at the call side, if the method is called in a condition, use the whenTrue/whenFalse nodes as flow-states for the assigned variable * after nullabilities are inferred for nodes, if the primary node is nullable and exactly one of whenTrue/whenFalse is non-nullable, emit the `[NotNullWhen(..)]` attribute
This commit is contained in:
Родитель
10c09d85b3
Коммит
9dcfbcf1bc
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 33 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 27 KiB |
|
@ -16,6 +16,7 @@
|
|||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE
|
||||
|
||||
using System.Runtime.Remoting.Proxies;
|
||||
using Xunit;
|
||||
|
||||
namespace ICSharpCode.NullabilityInference.Tests
|
||||
|
@ -194,5 +195,66 @@ class Program {
|
|||
}
|
||||
}", returnNullable: false, returnDependsOnInput: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferNotNullWhenTrue()
|
||||
{
|
||||
string program = @"
|
||||
using System.Collections.Generic;
|
||||
class DataStructure
|
||||
{
|
||||
Dictionary<string, string> dict = new Dictionary<string, string>();
|
||||
|
||||
public bool TryGetValue(string key, [Attr] out string? val)
|
||||
{
|
||||
return dict.TryGetValue(key, out val);
|
||||
}
|
||||
}";
|
||||
AssertNullabilityInference(
|
||||
expectedProgram: program.Replace("[Attr]", "[NotNullWhen(true)]"),
|
||||
inputProgram: program.Replace("[Attr] ", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferNotNullWhenFalse()
|
||||
{
|
||||
string program = @"
|
||||
using System.Collections.Generic;
|
||||
class DataStructure
|
||||
{
|
||||
Dictionary<string, string> dict = new Dictionary<string, string>();
|
||||
|
||||
public bool TryGetValue(string key, [Attr] out string? val)
|
||||
{
|
||||
return !dict.TryGetValue(key, out val);
|
||||
}
|
||||
}";
|
||||
AssertNullabilityInference(
|
||||
expectedProgram: program.Replace("[Attr]", "[NotNullWhen(false)]"),
|
||||
inputProgram: program.Replace("[Attr] ", ""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferNotNullWhenTrueFromControlFlow()
|
||||
{
|
||||
string program = @"
|
||||
using System.Collections.Generic;
|
||||
class DataStructure
|
||||
{
|
||||
public bool TryGet(int i, [Attr] out string? name)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
name = string.Empty;
|
||||
return true;
|
||||
}
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
}";
|
||||
AssertNullabilityInference(
|
||||
expectedProgram: program.Replace("[Attr]", "[NotNullWhen(true)]"),
|
||||
inputProgram: program.Replace("[Attr] ", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace ICSharpCode.NullabilityInference
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes an action when it is disposed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class ensures the callback is invoked at most once,
|
||||
/// even when Dispose is called on multiple threads.
|
||||
/// </remarks>
|
||||
public sealed class CallbackOnDispose : IDisposable
|
||||
{
|
||||
private Action? action;
|
||||
|
||||
public CallbackOnDispose(Action action)
|
||||
{
|
||||
this.action = action ?? throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Action? a = Interlocked.Exchange(ref action, null);
|
||||
a?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -171,15 +171,15 @@ namespace ICSharpCode.NullabilityInference
|
|||
public override TypeWithNode VisitLocalFunction(ILocalFunctionOperation operation, EdgeBuildingContext argument)
|
||||
{
|
||||
var oldFlowState = flowState.SaveSnapshot();
|
||||
var outerReturnType = syntaxVisitor.currentMethodReturnType;
|
||||
try {
|
||||
using var outerMethod = syntaxVisitor.SaveCurrentMethod();
|
||||
syntaxVisitor.currentMethod = operation.Symbol;
|
||||
syntaxVisitor.currentMethodReturnType = syntaxVisitor.GetMethodReturnSymbol(operation.Symbol);
|
||||
flowState.Clear();
|
||||
foreach (var child in operation.Children)
|
||||
child.Accept(this, EdgeBuildingContext.Normal);
|
||||
return typeSystem.VoidType;
|
||||
} finally {
|
||||
syntaxVisitor.currentMethodReturnType = outerReturnType;
|
||||
flowState.RestoreSnapshot(oldFlowState);
|
||||
}
|
||||
}
|
||||
|
@ -418,8 +418,33 @@ namespace ICSharpCode.NullabilityInference
|
|||
public override TypeWithNode VisitReturn(IReturnOperation operation, EdgeBuildingContext argument)
|
||||
{
|
||||
if (operation.ReturnedValue != null) {
|
||||
var returnVal = Visit(operation.ReturnedValue, EdgeBuildingContext.Normal);
|
||||
var returnType = syntaxVisitor.currentMethodReturnType;
|
||||
if (operation.Kind == OperationKind.Return && returnType.Type?.SpecialType == SpecialType.System_Boolean && syntaxVisitor.currentMethod != null) {
|
||||
// When returning a boolean, export flow-states for out parameters
|
||||
var (onTrue, onFalse) = VisitCondition(operation.ReturnedValue);
|
||||
foreach (var param in syntaxVisitor.currentMethod.Parameters) {
|
||||
if (!typeSystem.TryGetOutParameterFlowNodes(param, out var outNodes))
|
||||
continue;
|
||||
var path = new AccessPath(AccessPathRoot.Local, ImmutableArray.Create<ISymbol>(param));
|
||||
if (!onTrue.Unreachable) {
|
||||
flowState.RestoreSnapshot(onTrue);
|
||||
if (!flowState.TryGetNode(path, out var node)) {
|
||||
node = typeSystem.GetSymbolType(param, ignoreAttributes: true).Node;
|
||||
}
|
||||
tsBuilder.CreateEdge(node, outNodes.whenTrue, new EdgeLabel($"flow-state of {param.Name} on return true", operation));
|
||||
}
|
||||
if (!onFalse.Unreachable) {
|
||||
flowState.RestoreSnapshot(onFalse);
|
||||
if (!flowState.TryGetNode(path, out var node)) {
|
||||
node = typeSystem.GetSymbolType(param, ignoreAttributes: true).Node;
|
||||
}
|
||||
tsBuilder.CreateEdge(node, outNodes.whenFalse, new EdgeLabel($"flow-state of {param.Name} on return false", operation));
|
||||
}
|
||||
}
|
||||
// no need to create edge for return value, as `bool` is a value-type
|
||||
return typeSystem.VoidType;
|
||||
}
|
||||
var returnVal = Visit(operation.ReturnedValue, EdgeBuildingContext.Normal);
|
||||
if (operation.Kind == OperationKind.YieldReturn) {
|
||||
if (returnType.TypeArguments.Count == 0) {
|
||||
// returning non-generic enumerable
|
||||
|
@ -942,7 +967,7 @@ namespace ICSharpCode.NullabilityInference
|
|||
_ => null,
|
||||
};
|
||||
|
||||
private TypeSubstitution HandleArguments(TypeSubstitution substitution, ImmutableArray<IArgumentOperation> arguments, EdgeBuildingContext invocationContext)
|
||||
private void HandleArguments(TypeSubstitution substitution, ImmutableArray<IArgumentOperation> arguments, EdgeBuildingContext invocationContext)
|
||||
{
|
||||
Action? afterCall = null;
|
||||
FlowState? flowStateOnTrue = null;
|
||||
|
@ -971,9 +996,9 @@ namespace ICSharpCode.NullabilityInference
|
|||
}
|
||||
if (param.RefKind == RefKind.Out) {
|
||||
// set flow-state to value provided by the method
|
||||
var (onTrue, onFalse) = typeSystem.GetOutParameterFlowNodes(param, substitution);
|
||||
flowStateOnTrue.SetNode(path, onTrue, clearMembers: true);
|
||||
flowStateOnFalse.SetNode(path, onFalse, clearMembers: true);
|
||||
var (whenTrue, whenFalse) = typeSystem.GetOutParameterFlowNodes(param, substitution);
|
||||
flowStateOnTrue.SetNode(path, whenTrue, clearMembers: true);
|
||||
flowStateOnFalse.SetNode(path, whenFalse, clearMembers: true);
|
||||
} else {
|
||||
// the method might have mutated the by-ref argument -> reset flow-state to argument's declared type
|
||||
flowStateOnTrue.SetNode(path, argumentType.Node, clearMembers: true);
|
||||
|
@ -999,7 +1024,6 @@ namespace ICSharpCode.NullabilityInference
|
|||
Debug.Assert(flowStateOnTrue == null && flowStateOnFalse == null);
|
||||
Debug.Assert(flowStateReturnedOnTrue == null && flowStateReturnedOnFalse == null);
|
||||
}
|
||||
return substitution;
|
||||
}
|
||||
|
||||
private TypeWithNode currentObjectCreationType;
|
||||
|
@ -1132,6 +1156,17 @@ namespace ICSharpCode.NullabilityInference
|
|||
|
||||
public override TypeWithNode VisitLiteral(ILiteralOperation operation, EdgeBuildingContext argument)
|
||||
{
|
||||
if (argument == EdgeBuildingContext.Condition && operation.ConstantValue.Value is bool booleanValue) {
|
||||
var snapshot = flowState.SaveSnapshot();
|
||||
if (booleanValue) {
|
||||
flowStateReturnedOnTrue = snapshot;
|
||||
flowStateReturnedOnFalse = snapshot.WithUnreachable();
|
||||
} else {
|
||||
flowStateReturnedOnTrue = snapshot.WithUnreachable();
|
||||
flowStateReturnedOnFalse = snapshot;
|
||||
}
|
||||
return new TypeWithNode(operation.Type, typeSystem.ObliviousNode); // bool
|
||||
}
|
||||
if (operation.Type?.IsValueType == true) {
|
||||
return new TypeWithNode(operation.Type, typeSystem.ObliviousNode);
|
||||
} else if (operation.ConstantValue.HasValue && operation.ConstantValue.Value == null) {
|
||||
|
@ -1561,9 +1596,10 @@ namespace ICSharpCode.NullabilityInference
|
|||
}
|
||||
}
|
||||
// Analyze the body, and treat any `return` statements as assignments to `delegateReturnType`.
|
||||
var outerMethodReturnType = syntaxVisitor.currentMethodReturnType;
|
||||
var outerFlowState = flowState.SaveSnapshot();
|
||||
try {
|
||||
using var outerMethod = syntaxVisitor.SaveCurrentMethod();
|
||||
syntaxVisitor.currentMethod = lambda.Symbol;
|
||||
if (lambda.Symbol.IsAsync) {
|
||||
syntaxVisitor.currentMethodReturnType = syntaxVisitor.ExtractTaskReturnType(delegateReturnType);
|
||||
} else {
|
||||
|
@ -1572,7 +1608,6 @@ namespace ICSharpCode.NullabilityInference
|
|||
flowState.Clear();
|
||||
lambda.Body.Accept(this, EdgeBuildingContext.Normal);
|
||||
} finally {
|
||||
syntaxVisitor.currentMethodReturnType = outerMethodReturnType;
|
||||
flowState.RestoreSnapshot(outerFlowState);
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -150,12 +150,13 @@ namespace ICSharpCode.NullabilityInference
|
|||
return mapping[syntax];
|
||||
}
|
||||
|
||||
internal IMethodSymbol? currentMethod;
|
||||
internal TypeWithNode currentMethodReturnType;
|
||||
|
||||
public override TypeWithNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
|
||||
{
|
||||
var outerMethodReturnType = currentMethodReturnType;
|
||||
try {
|
||||
using var outerMethod = SaveCurrentMethod();
|
||||
currentMethod = semanticModel.GetDeclaredSymbol(node, cancellationToken);
|
||||
currentMethodReturnType = typeSystem.VoidType;
|
||||
var operation = semanticModel.GetOperation(node, cancellationToken);
|
||||
if (operation == null)
|
||||
|
@ -177,9 +178,6 @@ namespace ICSharpCode.NullabilityInference
|
|||
}
|
||||
}
|
||||
return operation.Accept(operationVisitor, new EdgeBuildingContext());
|
||||
} finally {
|
||||
currentMethodReturnType = outerMethodReturnType;
|
||||
}
|
||||
}
|
||||
|
||||
internal void HandleCref(NameMemberCrefSyntax cref)
|
||||
|
@ -216,14 +214,23 @@ namespace ICSharpCode.NullabilityInference
|
|||
return HandleMethodDeclaration(node);
|
||||
}
|
||||
|
||||
internal IDisposable SaveCurrentMethod()
|
||||
{
|
||||
var outerMethod = currentMethod;
|
||||
var outerMethodReturnType = currentMethodReturnType;
|
||||
return new CallbackOnDispose(delegate {
|
||||
currentMethod = outerMethod;
|
||||
currentMethodReturnType = outerMethodReturnType;
|
||||
});
|
||||
}
|
||||
|
||||
private TypeWithNode HandleMethodDeclaration(BaseMethodDeclarationSyntax node)
|
||||
{
|
||||
var outerMethodReturnType = currentMethodReturnType;
|
||||
try {
|
||||
var symbol = semanticModel.GetDeclaredSymbol(node);
|
||||
if (symbol != null) {
|
||||
CreateOverrideEdge(symbol, symbol.OverriddenMethod);
|
||||
currentMethodReturnType = GetMethodReturnSymbol(symbol);
|
||||
using var outerMethod = SaveCurrentMethod();
|
||||
currentMethod = semanticModel.GetDeclaredSymbol(node);
|
||||
if (currentMethod != null) {
|
||||
CreateOverrideEdge(currentMethod, currentMethod.OverriddenMethod);
|
||||
currentMethodReturnType = GetMethodReturnSymbol(currentMethod);
|
||||
} else {
|
||||
currentMethodReturnType = typeSystem.VoidType;
|
||||
}
|
||||
|
@ -232,9 +239,6 @@ namespace ICSharpCode.NullabilityInference
|
|||
} else {
|
||||
return typeSystem.VoidType;
|
||||
}
|
||||
} finally {
|
||||
currentMethodReturnType = outerMethodReturnType;
|
||||
}
|
||||
}
|
||||
|
||||
internal TypeWithNode GetMethodReturnSymbol(IMethodSymbol method)
|
||||
|
@ -248,6 +252,7 @@ namespace ICSharpCode.NullabilityInference
|
|||
|
||||
internal TypeWithNode ExtractTaskReturnType(TypeWithNode taskType)
|
||||
{
|
||||
// See also: EffectiveReturnType() extension method
|
||||
if (taskType.TypeArguments.Count == 0) {
|
||||
return typeSystem.VoidType;
|
||||
} else {
|
||||
|
@ -258,8 +263,7 @@ namespace ICSharpCode.NullabilityInference
|
|||
public override TypeWithNode VisitPropertyDeclaration(PropertyDeclarationSyntax node)
|
||||
{
|
||||
node.ExplicitInterfaceSpecifier?.Accept(this);
|
||||
var outerMethodReturnType = currentMethodReturnType;
|
||||
try {
|
||||
using var outerMethod = SaveCurrentMethod();
|
||||
var symbol = semanticModel.GetDeclaredSymbol(node);
|
||||
if (symbol != null) {
|
||||
CreateOverrideEdge(symbol, symbol.OverriddenProperty);
|
||||
|
@ -267,20 +271,17 @@ namespace ICSharpCode.NullabilityInference
|
|||
} else {
|
||||
currentMethodReturnType = typeSystem.VoidType;
|
||||
}
|
||||
currentMethod = null;
|
||||
node.AccessorList?.Accept(this);
|
||||
node.ExpressionBody?.Accept(this);
|
||||
node.Initializer?.Accept(this);
|
||||
return typeSystem.VoidType;
|
||||
} finally {
|
||||
currentMethodReturnType = outerMethodReturnType;
|
||||
}
|
||||
}
|
||||
|
||||
public override TypeWithNode VisitIndexerDeclaration(IndexerDeclarationSyntax node)
|
||||
{
|
||||
node.ExplicitInterfaceSpecifier?.Accept(this);
|
||||
var outerMethodReturnType = currentMethodReturnType;
|
||||
try {
|
||||
using var outerMethod = SaveCurrentMethod();
|
||||
var symbol = semanticModel.GetDeclaredSymbol(node);
|
||||
if (symbol != null) {
|
||||
CreateOverrideEdge(symbol, symbol.OverriddenProperty);
|
||||
|
@ -288,19 +289,16 @@ namespace ICSharpCode.NullabilityInference
|
|||
} else {
|
||||
currentMethodReturnType = typeSystem.VoidType;
|
||||
}
|
||||
currentMethod = null;
|
||||
node.AccessorList?.Accept(this);
|
||||
node.ExpressionBody?.Accept(this);
|
||||
return typeSystem.VoidType;
|
||||
} finally {
|
||||
currentMethodReturnType = outerMethodReturnType;
|
||||
}
|
||||
}
|
||||
|
||||
public override TypeWithNode VisitEventDeclaration(EventDeclarationSyntax node)
|
||||
{
|
||||
node.ExplicitInterfaceSpecifier?.Accept(this);
|
||||
var outerMethodReturnType = currentMethodReturnType;
|
||||
try {
|
||||
using var outerMethod = SaveCurrentMethod();
|
||||
var symbol = semanticModel.GetDeclaredSymbol(node);
|
||||
if (symbol != null) {
|
||||
CreateOverrideEdge(symbol, symbol.OverriddenEvent);
|
||||
|
@ -308,11 +306,9 @@ namespace ICSharpCode.NullabilityInference
|
|||
} else {
|
||||
currentMethodReturnType = typeSystem.VoidType;
|
||||
}
|
||||
currentMethod = null;
|
||||
node.AccessorList?.Accept(this);
|
||||
return typeSystem.VoidType;
|
||||
} finally {
|
||||
currentMethodReturnType = outerMethodReturnType;
|
||||
}
|
||||
}
|
||||
|
||||
public override TypeWithNode VisitBlock(BlockSyntax node)
|
||||
|
|
|
@ -258,6 +258,19 @@ namespace ICSharpCode.NullabilityInference
|
|||
return type?.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the return type used for "return" statements within the method.
|
||||
/// </summary>
|
||||
public static ITypeSymbol EffectiveReturnType(this IMethodSymbol method)
|
||||
{
|
||||
// See also: ExtractTaskReturnType()
|
||||
var returnType = method.ReturnType;
|
||||
if (method.IsAsync && returnType is INamedTypeSymbol namedType && namedType.TypeArguments.Length == 1) {
|
||||
returnType = namedType.TypeArguments.Single();
|
||||
}
|
||||
return returnType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove item at index <c>index</c> in O(1) by swapping it with the last element in the collection before removing it.
|
||||
/// Useful when the order of elements in the list is not relevant.
|
||||
|
|
|
@ -25,6 +25,11 @@ namespace ICSharpCode.NullabilityInference
|
|||
this.Locals = locals;
|
||||
this.Unreachable = unreachable;
|
||||
}
|
||||
|
||||
internal Snapshot? WithUnreachable()
|
||||
{
|
||||
return new Snapshot(ThisPath, Locals, unreachable: true);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct PathNode
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
@ -29,13 +32,15 @@ namespace ICSharpCode.NullabilityInference
|
|||
internal sealed class InferredNullabilitySyntaxRewriter : CSharpSyntaxRewriter
|
||||
{
|
||||
private readonly SemanticModel semanticModel;
|
||||
private readonly TypeSystem typeSystem;
|
||||
private readonly SyntaxToNodeMapping mapping;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
|
||||
public InferredNullabilitySyntaxRewriter(SemanticModel semanticModel, SyntaxToNodeMapping mapping, CancellationToken cancellationToken)
|
||||
public InferredNullabilitySyntaxRewriter(SemanticModel semanticModel, TypeSystem typeSystem, SyntaxToNodeMapping mapping, CancellationToken cancellationToken)
|
||||
: base(visitIntoStructuredTrivia: true)
|
||||
{
|
||||
this.semanticModel = semanticModel;
|
||||
this.typeSystem = typeSystem;
|
||||
this.mapping = mapping;
|
||||
this.cancellationToken = cancellationToken;
|
||||
}
|
||||
|
@ -114,5 +119,24 @@ namespace ICSharpCode.NullabilityInference
|
|||
}
|
||||
return newNode;
|
||||
}
|
||||
|
||||
public override SyntaxNode? VisitParameter(ParameterSyntax node)
|
||||
{
|
||||
var param = semanticModel.GetDeclaredSymbol(node, cancellationToken);
|
||||
node = (ParameterSyntax)base.VisitParameter(node)!;
|
||||
if (isActive && param != null && typeSystem.TryGetOutParameterFlowNodes(param, out var pair)) {
|
||||
if (pair.whenTrue.NullType != pair.whenFalse.NullType) {
|
||||
Debug.Assert(pair.whenTrue.NullType == NullType.NonNull || pair.whenFalse.NullType == NullType.NonNull);
|
||||
// Create [NotNullWhen] attribute
|
||||
bool notNullWhen = (pair.whenTrue.NullType == NullType.NonNull);
|
||||
var attrArgument = SyntaxFactory.LiteralExpression(notNullWhen ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression);
|
||||
var newAttribute = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("NotNullWhen"),
|
||||
SyntaxFactory.AttributeArgumentList(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.AttributeArgument(attrArgument))));
|
||||
var newAttributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(newAttribute));
|
||||
node = node.AddAttributeLists(newAttributeList.WithTrailingTrivia(SyntaxFactory.Space));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,12 +21,10 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.Security;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
|
||||
namespace ICSharpCode.NullabilityInference
|
||||
{
|
||||
|
@ -294,6 +292,10 @@ namespace ICSharpCode.NullabilityInference
|
|||
}
|
||||
parameterTypes.Add(symbol, type);
|
||||
typeSystem.AddSymbolType(symbol, type);
|
||||
|
||||
if (symbol.RefKind == RefKind.Out && (symbol.ContainingSymbol as IMethodSymbol)?.EffectiveReturnType().SpecialType == SpecialType.System_Boolean) {
|
||||
typeSystem.RegisterOutParamFlowNodes(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
node.Default?.Accept(this);
|
||||
|
|
|
@ -108,7 +108,7 @@ namespace ICSharpCode.NullabilityInference
|
|||
{
|
||||
return compilation.SyntaxTrees.AsParallel().WithCancellation(cancellationToken).Select(syntaxTree => {
|
||||
var semanticModel = compilation.GetSemanticModel(syntaxTree);
|
||||
var rewriter = new InferredNullabilitySyntaxRewriter(semanticModel, typeSystem.GetMapping(syntaxTree), cancellationToken);
|
||||
var rewriter = new InferredNullabilitySyntaxRewriter(semanticModel, typeSystem, typeSystem.GetMapping(syntaxTree), cancellationToken);
|
||||
var newRoot = rewriter.Visit(syntaxTree.GetRoot());
|
||||
return syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);
|
||||
});
|
||||
|
|
|
@ -23,7 +23,6 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Operations;
|
||||
|
||||
namespace ICSharpCode.NullabilityInference
|
||||
{
|
||||
|
@ -52,6 +51,8 @@ namespace ICSharpCode.NullabilityInference
|
|||
private readonly Dictionary<ISymbol, TypeWithNode> symbolType = new Dictionary<ISymbol, TypeWithNode>();
|
||||
private readonly Dictionary<(ITypeSymbol, ITypeSymbol), TypeWithNode> baseTypes = new Dictionary<(ITypeSymbol, ITypeSymbol), TypeWithNode>();
|
||||
private readonly List<NullabilityNode> additionalNodes = new List<NullabilityNode>();
|
||||
private readonly Dictionary<IParameterSymbol, (NullabilityNode whenTrue, NullabilityNode whenFalse)> outParamFlowNodes
|
||||
= new Dictionary<IParameterSymbol, (NullabilityNode whenTrue, NullabilityNode whenFalse)>();
|
||||
|
||||
|
||||
private readonly INamedTypeSymbol voidType;
|
||||
|
@ -183,27 +184,48 @@ namespace ICSharpCode.NullabilityInference
|
|||
return new TypeWithNode(type, ObliviousNode);
|
||||
}
|
||||
|
||||
internal (NullabilityNode onTrue, NullabilityNode onFalse) GetOutParameterFlowNodes(IParameterSymbol param, TypeSubstitution substitution)
|
||||
internal bool TryGetOutParameterFlowNodes(IParameterSymbol param, out (NullabilityNode whenTrue, NullabilityNode whenFalse) pair)
|
||||
{
|
||||
return outParamFlowNodes.TryGetValue(param, out pair);
|
||||
}
|
||||
|
||||
internal (NullabilityNode whenTrue, NullabilityNode whenFalse) GetOutParameterFlowNodes(IParameterSymbol param, TypeSubstitution substitution)
|
||||
{
|
||||
if (outParamFlowNodes.TryGetValue(param, out var pair)) {
|
||||
return pair;
|
||||
}
|
||||
var ty = GetSymbolType(param, ignoreAttributes: true);
|
||||
if (ty.Type is ITypeParameterSymbol tp) {
|
||||
ty = substitution[tp.TypeParameterKind, tp.FullOrdinal()];
|
||||
}
|
||||
var onTrue = ty.Node;
|
||||
var onFalse = ty.Node;
|
||||
var whenTrue = ty.Node;
|
||||
var whenFalse = ty.Node;
|
||||
foreach (var attr in param.GetAttributes()) {
|
||||
string? attrName = attr.AttributeClass?.GetFullName();
|
||||
if (attrName == "System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute") {
|
||||
if (attr.ConstructorArguments.Single().Value is bool b) {
|
||||
(b ? ref onTrue : ref onFalse) = NullableNode;
|
||||
(b ? ref whenTrue : ref whenFalse) = NullableNode;
|
||||
}
|
||||
} else if (attrName == "System.Diagnostics.CodeAnalysis.NotNullWhenAttribute") {
|
||||
if (attr.ConstructorArguments.Single().Value is bool b) {
|
||||
(b ? ref onTrue : ref onFalse) = NonNullNode;
|
||||
(b ? ref whenTrue : ref whenFalse) = NonNullNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (onTrue, onFalse);
|
||||
return (whenTrue, whenFalse);
|
||||
}
|
||||
|
||||
private static bool HasAnyOutParamFlowAttribute(IParameterSymbol param)
|
||||
{
|
||||
foreach (var attr in param.GetAttributes()) {
|
||||
string? attrName = attr.AttributeClass?.GetFullName();
|
||||
if (attrName == "System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute") {
|
||||
return true;
|
||||
} else if (attrName == "System.Diagnostics.CodeAnalysis.NotNullWhenAttribute") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private TypeWithNode GetDirectBase(ITypeSymbol derivedTypeDef, INamedTypeSymbol baseType, INamedTypeSymbol baseTypeInstance, TypeSubstitution substitution)
|
||||
|
@ -369,7 +391,8 @@ namespace ICSharpCode.NullabilityInference
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Neither the type-system nor the builder is thread-safe.
|
||||
/// However, multiple builders can be used concurrently
|
||||
/// However, multiple builders wrapping the same type-system can be used concurrently,
|
||||
/// as long as the builders are not flushed concurrently.
|
||||
/// </remarks>
|
||||
internal class Builder
|
||||
{
|
||||
|
@ -380,14 +403,14 @@ namespace ICSharpCode.NullabilityInference
|
|||
|
||||
public Builder(TypeSystem typeSystem)
|
||||
{
|
||||
// Don't store the typeSystem is this; we may not access it outside of Flush().
|
||||
// Don't store the typeSystem in this; we may not access it outside of Flush().
|
||||
this.NullableNode = typeSystem.NullableNode;
|
||||
this.NonNullNode = typeSystem.NonNullNode;
|
||||
this.ObliviousNode = typeSystem.ObliviousNode;
|
||||
this.VoidType = typeSystem.VoidType;
|
||||
}
|
||||
|
||||
internal NullabilityNode FromAttributes(ImmutableArray<AttributeData> attributeData)
|
||||
internal NullabilityNode? FromAttributes(ImmutableArray<AttributeData> attributeData)
|
||||
{
|
||||
return TypeSystem.FromAttributes(attributeData, new SpecialNodes(NullableNode, NonNullNode, ObliviousNode));
|
||||
}
|
||||
|
@ -407,6 +430,19 @@ namespace ICSharpCode.NullabilityInference
|
|||
AddAction(ts => ts.baseTypes[key] = baseType);
|
||||
}
|
||||
|
||||
public void RegisterOutParamFlowNodes(IParameterSymbol parameter)
|
||||
{
|
||||
Debug.Assert(SymbolEqualityComparer.Default.Equals(parameter, parameter.OriginalDefinition));
|
||||
if (HasAnyOutParamFlowAttribute(parameter)) {
|
||||
return; // use existing attribute, don't try to infer a new one
|
||||
}
|
||||
var whenTrue = CreateHelperNode();
|
||||
var whenFalse = CreateHelperNode();
|
||||
whenTrue.SetName(parameter.Name + "_when_true");
|
||||
whenFalse.SetName(parameter.Name + "_when_false");
|
||||
AddAction(ts => ts.outParamFlowNodes.Add(parameter, (whenTrue, whenFalse)));
|
||||
}
|
||||
|
||||
private readonly List<Action<TypeSystem>> cachedActions = new List<Action<TypeSystem>>();
|
||||
private readonly List<HelperNullabilityNode> newNodes = new List<HelperNullabilityNode>();
|
||||
private readonly List<NullabilityEdge> newEdges = new List<NullabilityEdge>();
|
||||
|
@ -418,6 +454,7 @@ namespace ICSharpCode.NullabilityInference
|
|||
|
||||
public void Flush(TypeSystem typeSystem)
|
||||
{
|
||||
Debug.Assert(typeSystem.NonNullNode == this.NonNullNode, "Flush called with wrong type-system instance");
|
||||
foreach (var action in cachedActions) {
|
||||
action(typeSystem);
|
||||
}
|
||||
|
|
123
README.md
123
README.md
|
@ -182,3 +182,126 @@ Thus, `n#1`, the type argument `#2` and `a#3` are all marked as nullable. But `b
|
|||
|
||||
If the type arguments are not explicitly specified but inferred by the compiler, nullability inference will create additional "helper nodes" for the graph
|
||||
that are not associated with any syntax. This allows us to construct the edges for the calls in the same way.
|
||||
|
||||
### Generic Types
|
||||
|
||||
```csharp
|
||||
1: using System.Collections.Generic;
|
||||
2: class Program
|
||||
3: {
|
||||
4: List<string> list = new List<string>();
|
||||
5:
|
||||
6: public void Add(string name) => list.Add(name);
|
||||
7: public string Get(int i) => list[i];
|
||||
8: }
|
||||
```
|
||||
|
||||
<img src="https://github.com/icsharpcode/NullabilityInference/raw/master/.github/img/GenericType.png" />
|
||||
|
||||
In this graph, you can see how generic types are handled:
|
||||
The type of the `list` field generates two nodes:
|
||||
* `list#3` represents the nullability of the list itself.
|
||||
* `list!0#2` represents the nullability of the strings within the list.
|
||||
Similarly, `new!0#1` represents the nullability of the string type argument in the `new List<string>` expression.
|
||||
Because the type parameter of `List` is invariant, the field initialization in line 4 creates a pair of edges (in both directions)
|
||||
between the `new!0#1` and `list!0#2` nodes. This forces both type arguments to have the same nullability.
|
||||
|
||||
The resulting graph expresses that the nullability of the return type of `Get` (represented by `Get#5`) depends on the nullability
|
||||
of the `name` parameter in the `Add` method (node `name#4`).
|
||||
Whether these types will be inferred as nullable or non-nullable will depend on whether the remainder of the program passes a nullable type to `Add`,
|
||||
and on the existance of code that uses the return value of `Get` without null checks.
|
||||
|
||||
### Flow-analysis
|
||||
|
||||
```csharp
|
||||
01: using System.Collections.Generic;
|
||||
02: using System.Diagnostics.CodeAnalysis;
|
||||
03: class Program
|
||||
04: {
|
||||
05: public string someString = "hello";
|
||||
06:
|
||||
07: public bool TryGet(int i, out string name)
|
||||
08: {
|
||||
09: if (i > 0)
|
||||
10: {
|
||||
11: name = someString;
|
||||
12: return true;
|
||||
13: }
|
||||
14: name = null;
|
||||
15: return false;
|
||||
16: }
|
||||
17:
|
||||
18: public int Use(int i)
|
||||
19: {
|
||||
20: if (TryGet(i, out string x))
|
||||
21: {
|
||||
22: return x.Length;
|
||||
23: }
|
||||
24: else
|
||||
25: {
|
||||
26: return 0;
|
||||
27: }
|
||||
28: }
|
||||
29: }
|
||||
```
|
||||
|
||||
The `TryGet` function involves a common C# code pattern: the nullability of an `out` parameter depends on the boolean return value.
|
||||
If the function returns true, callers can assume the out variable was assigned a non-null value.
|
||||
But if the function returns false, the value might be null.
|
||||
|
||||
Using our [own flow-analysis](https://github.com/icsharpcode/NullabilityInference/issues/5), the InferNull tool can handle this case
|
||||
and automatically infer the `[NotNullWhen(true)]` attribute!
|
||||
|
||||
<img src="https://github.com/icsharpcode/NullabilityInference/raw/master/.github/img/FlowState.png" />
|
||||
|
||||
For the `name` parameter (in general: for any out-parameters in functions returning `bool`), we create not only the declared type `name#2`,
|
||||
but also the `name_when_true` and `name_when_false` nodes. These extra helper nodes represent the nullability of `out string name` in the cases
|
||||
where `TryGet` returns true/false.
|
||||
|
||||
Within the body of `TryGet`, we track the nullability of `name` based on the previous assignment as the "flow-state".
|
||||
After the assignment `name = someString;` in line 11, the nullability of `name` is the same as the nullability of `someString`.
|
||||
We represent this by saving the nullability node `someString#1` as the flow-state of `name`.
|
||||
On the `return true;` statement in line 12, we connect the current flow-state of the `out` parameters with the `when_true` helper nodes,
|
||||
resulting in the `someString#1`->`<name_when_true#1>` edge.
|
||||
Similarly, the `return false;` statement in line 15 results in an edge from `<nullable>` to `<name_when_false#2>`, because the `name = null;` assignment
|
||||
has set the flow-state of `name` to `<nullable>`.
|
||||
|
||||
In the `Use` method, we also employ flow-state: even though `x` itself needs to be nullable, the then-branch of the `if` uses the `<name_when_true#1>` node
|
||||
as flow-state for the `x` variable.
|
||||
This causes the `x.Length` dereference to create an edge starting at `<name_when_true#1>`, rather than x's declared type (`x#3`).
|
||||
|
||||
This allows inference to success (no path from `<nullable>` to `<nonnull>`. In the inference result, `name#2` and `<name_when_false#2>` are nullable,
|
||||
but `<name_when_true#1>` is non-nullable. The difference in nullabilities between the when_false and when_true cases causes the tool to emit a `[NotNullWhen(true)]`
|
||||
attribute:
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
class Program
|
||||
{
|
||||
public string someString = "hello";
|
||||
|
||||
public bool TryGet(int i, [NotNullWhen(true)] out string? name)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
name = someString;
|
||||
return true;
|
||||
}
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int Use(int i)
|
||||
{
|
||||
if (TryGet(i, out string? x))
|
||||
{
|
||||
return x.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
Загрузка…
Ссылка в новой задаче