#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:
Daniel Grunwald 2020-06-13 21:53:26 +02:00
Родитель 10c09d85b3
Коммит 9dcfbcf1bc
13 изменённых файлов: 430 добавлений и 105 удалений

Двоичные данные
.github/img/FlowState.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 33 KiB

Двоичные данные
.github/img/GenericType.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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,36 +150,34 @@ namespace ICSharpCode.NullabilityInference
return mapping[syntax];
}
internal IMethodSymbol? currentMethod;
internal TypeWithNode currentMethodReturnType;
public override TypeWithNode VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
{
var outerMethodReturnType = currentMethodReturnType;
try {
currentMethodReturnType = typeSystem.VoidType;
var operation = semanticModel.GetOperation(node, cancellationToken);
if (operation == null)
throw new NotSupportedException($"Could not get operation for {node}");
if (node.Initializer?.ThisOrBaseKeyword.Kind() != SyntaxKind.ThisKeyword) {
HashSet<ISymbol> initializedSymbols = new HashSet<ISymbol>();
foreach (var assgn in operation.DescendantsAndSelf().OfType<ISimpleAssignmentOperation>()) {
if (assgn.Target is IFieldReferenceOperation fieldRef) {
initializedSymbols.Add(fieldRef.Field);
} else if (assgn.Target is IPropertyReferenceOperation propertyRef) {
initializedSymbols.Add(propertyRef.Property);
} else if (assgn.Target is IEventReferenceOperation eventRef) {
initializedSymbols.Add(eventRef.Event);
}
}
if (node.Parent is TypeDeclarationSyntax typeSyntax) {
bool isStatic = node.Modifiers.Any(SyntaxKind.StaticKeyword);
MarkFieldsAndPropertiesAsNullable(typeSyntax.Members, isStatic, initializedSymbols, new EdgeLabel("uninit", node));
using var outerMethod = SaveCurrentMethod();
currentMethod = semanticModel.GetDeclaredSymbol(node, cancellationToken);
currentMethodReturnType = typeSystem.VoidType;
var operation = semanticModel.GetOperation(node, cancellationToken);
if (operation == null)
throw new NotSupportedException($"Could not get operation for {node}");
if (node.Initializer?.ThisOrBaseKeyword.Kind() != SyntaxKind.ThisKeyword) {
HashSet<ISymbol> initializedSymbols = new HashSet<ISymbol>();
foreach (var assgn in operation.DescendantsAndSelf().OfType<ISimpleAssignmentOperation>()) {
if (assgn.Target is IFieldReferenceOperation fieldRef) {
initializedSymbols.Add(fieldRef.Field);
} else if (assgn.Target is IPropertyReferenceOperation propertyRef) {
initializedSymbols.Add(propertyRef.Property);
} else if (assgn.Target is IEventReferenceOperation eventRef) {
initializedSymbols.Add(eventRef.Event);
}
}
return operation.Accept(operationVisitor, new EdgeBuildingContext());
} finally {
currentMethodReturnType = outerMethodReturnType;
if (node.Parent is TypeDeclarationSyntax typeSyntax) {
bool isStatic = node.Modifiers.Any(SyntaxKind.StaticKeyword);
MarkFieldsAndPropertiesAsNullable(typeSyntax.Members, isStatic, initializedSymbols, new EdgeLabel("uninit", node));
}
}
return operation.Accept(operationVisitor, new EdgeBuildingContext());
}
internal void HandleCref(NameMemberCrefSyntax cref)
@ -216,24 +214,30 @@ 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);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
if (node.Body != null || node.ExpressionBody != null) {
return HandleAsOperation(node);
} else {
return typeSystem.VoidType;
}
} finally {
currentMethodReturnType = outerMethodReturnType;
using var outerMethod = SaveCurrentMethod();
currentMethod = semanticModel.GetDeclaredSymbol(node);
if (currentMethod != null) {
CreateOverrideEdge(currentMethod, currentMethod.OverriddenMethod);
currentMethodReturnType = GetMethodReturnSymbol(currentMethod);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
if (node.Body != null || node.ExpressionBody != null) {
return HandleAsOperation(node);
} else {
return typeSystem.VoidType;
}
}
@ -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,61 +263,52 @@ namespace ICSharpCode.NullabilityInference
public override TypeWithNode VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
node.ExplicitInterfaceSpecifier?.Accept(this);
var outerMethodReturnType = currentMethodReturnType;
try {
var symbol = semanticModel.GetDeclaredSymbol(node);
if (symbol != null) {
CreateOverrideEdge(symbol, symbol.OverriddenProperty);
currentMethodReturnType = typeSystem.GetSymbolType(symbol);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
node.AccessorList?.Accept(this);
node.ExpressionBody?.Accept(this);
node.Initializer?.Accept(this);
return typeSystem.VoidType;
} finally {
currentMethodReturnType = outerMethodReturnType;
using var outerMethod = SaveCurrentMethod();
var symbol = semanticModel.GetDeclaredSymbol(node);
if (symbol != null) {
CreateOverrideEdge(symbol, symbol.OverriddenProperty);
currentMethodReturnType = typeSystem.GetSymbolType(symbol);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
currentMethod = null;
node.AccessorList?.Accept(this);
node.ExpressionBody?.Accept(this);
node.Initializer?.Accept(this);
return typeSystem.VoidType;
}
public override TypeWithNode VisitIndexerDeclaration(IndexerDeclarationSyntax node)
{
node.ExplicitInterfaceSpecifier?.Accept(this);
var outerMethodReturnType = currentMethodReturnType;
try {
var symbol = semanticModel.GetDeclaredSymbol(node);
if (symbol != null) {
CreateOverrideEdge(symbol, symbol.OverriddenProperty);
currentMethodReturnType = typeSystem.GetSymbolType(symbol);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
node.AccessorList?.Accept(this);
node.ExpressionBody?.Accept(this);
return typeSystem.VoidType;
} finally {
currentMethodReturnType = outerMethodReturnType;
using var outerMethod = SaveCurrentMethod();
var symbol = semanticModel.GetDeclaredSymbol(node);
if (symbol != null) {
CreateOverrideEdge(symbol, symbol.OverriddenProperty);
currentMethodReturnType = typeSystem.GetSymbolType(symbol);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
currentMethod = null;
node.AccessorList?.Accept(this);
node.ExpressionBody?.Accept(this);
return typeSystem.VoidType;
}
public override TypeWithNode VisitEventDeclaration(EventDeclarationSyntax node)
{
node.ExplicitInterfaceSpecifier?.Accept(this);
var outerMethodReturnType = currentMethodReturnType;
try {
var symbol = semanticModel.GetDeclaredSymbol(node);
if (symbol != null) {
CreateOverrideEdge(symbol, symbol.OverriddenEvent);
currentMethodReturnType = typeSystem.GetSymbolType(symbol);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
node.AccessorList?.Accept(this);
return typeSystem.VoidType;
} finally {
currentMethodReturnType = outerMethodReturnType;
using var outerMethod = SaveCurrentMethod();
var symbol = semanticModel.GetDeclaredSymbol(node);
if (symbol != null) {
CreateOverrideEdge(symbol, symbol.OverriddenEvent);
currentMethodReturnType = typeSystem.GetSymbolType(symbol);
} else {
currentMethodReturnType = typeSystem.VoidType;
}
currentMethod = null;
node.AccessorList?.Accept(this);
return typeSystem.VoidType;
}
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
Просмотреть файл

@ -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;
}
}
}
```