#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters private static ImmutableArray <SymbolDisplayPart> ToDisplayParts( ITypeSymbol symbol, CodeAnalysis.NullableFlowState nullableFlowState, SemanticModel semanticModelOpt, int positionOpt, SymbolDisplayFormat format, bool minimal) { // https://github.com/dotnet/roslyn/issues/35035: Refactor this. We need to be able to handle non-TypeSymbol inputs var annotation = (CodeAnalysis.NullableAnnotation?)TypeWithState.Create((TypeSymbol)symbol, nullableFlowState.ToInternalFlowState()).ToTypeWithAnnotations().NullableAnnotation.ToPublicAnnotation(); return(ToDisplayParts(symbol, annotation, semanticModelOpt, positionOpt, format, minimal)); }
LearnFromDecisionDag( SyntaxNode node, BoundDecisionDag decisionDag, BoundExpression expression, TypeWithState expressionType, ref LocalState initialState) { // We reuse the slot at the beginning of a switch (or is-pattern expression), pretending that we are // not copying the input to evaluate the patterns. In this way we infer non-nullability of the original // variable's parts based on matched pattern parts. Mutations in `when` clauses can show the inaccuracy // of analysis based on this choice. var rootTemp = BoundDagTemp.ForOriginalInput(expression); int originalInputSlot = MakeSlot(expression); if (originalInputSlot <= 0) { originalInputSlot = makeDagTempSlot(expressionType.ToTypeWithAnnotations(), rootTemp); initialState[originalInputSlot] = expressionType.State; } var tempMap = PooledDictionary <BoundDagTemp, (int slot, TypeSymbol type)> .GetInstance(); Debug.Assert(originalInputSlot > 0); tempMap.Add(rootTemp, (originalInputSlot, expressionType.Type)); var nodeStateMap = PooledDictionary <BoundDecisionDagNode, (LocalState state, bool believedReachable)> .GetInstance(); nodeStateMap.Add(decisionDag.RootNode, (state: initialState.Clone(), believedReachable: true)); var labelStateMap = PooledDictionary <LabelSymbol, (LocalState state, bool believedReachable)> .GetInstance(); foreach (var dagNode in decisionDag.TopologicallySortedNodes) { bool found = nodeStateMap.TryGetValue(dagNode, out var nodeStateAndBelievedReachable); Debug.Assert(found); // the topologically sorted nodes should contain only reachable nodes (LocalState nodeState, bool nodeBelievedReachable) = nodeStateAndBelievedReachable; SetState(nodeState); switch (dagNode) { case BoundEvaluationDecisionDagNode p: { var evaluation = p.Evaluation; (int inputSlot, TypeSymbol inputType) = tempMap.TryGetValue(evaluation.Input, out var slotAndType) ? slotAndType : throw ExceptionUtilities.Unreachable; Debug.Assert(inputSlot > 0); var inputState = this.State[inputSlot]; switch (evaluation) { case BoundDagDeconstructEvaluation e: { // https://github.com/dotnet/roslyn/issues/34232 // We may need to recompute the Deconstruct method for a deconstruction if // the receiver type has changed (e.g. its nested nullability). var method = e.DeconstructMethod; int extensionExtra = method.IsStatic ? 1 : 0; for (int i = 0; i < method.ParameterCount - extensionExtra; i++) { var parameterType = method.Parameters[i + extensionExtra].TypeWithAnnotations; var output = new BoundDagTemp(e.Syntax, parameterType.Type, e, i); int outputSlot = makeDagTempSlot(parameterType, output); Debug.Assert(outputSlot > 0); addToTempMap(output, outputSlot, parameterType.Type); } break; } case BoundDagTypeEvaluation e: { var output = new BoundDagTemp(e.Syntax, e.Type, e); HashSet <DiagnosticInfo> discardedDiagnostics = null; int outputSlot; switch (_conversions.WithNullability(false).ClassifyConversionFromType(inputType, e.Type, ref discardedDiagnostics).Kind) { case ConversionKind.Identity: case ConversionKind.ImplicitReference: case ConversionKind.NoConversion: case ConversionKind.ExplicitReference: outputSlot = inputSlot; break; case ConversionKind.ExplicitNullable when AreNullableAndUnderlyingTypes(inputType, e.Type, out _): outputSlot = GetNullableOfTValueSlot(inputType, inputSlot, out _, forceSlotEvenIfEmpty: true); if (outputSlot < 0) { goto default; } break; default: outputSlot = makeDagTempSlot(TypeWithAnnotations.Create(e.Type, NullableAnnotation.NotAnnotated), output); break; } State[outputSlot] = NullableFlowState.NotNull; var outputType = TypeWithState.Create(e.Type, inputState); addToTempMap(output, outputSlot, outputType.Type); break; } case BoundDagFieldEvaluation e: { Debug.Assert(inputSlot > 0); var field = (FieldSymbol)AsMemberOfType(inputType, e.Field); int outputSlot = GetOrCreateSlot(field, inputSlot, forceSlotEvenIfEmpty: true); Debug.Assert(outputSlot > 0); var type = field.Type; var output = new BoundDagTemp(e.Syntax, type, e); addToTempMap(output, outputSlot, type); break; } case BoundDagPropertyEvaluation e: { Debug.Assert(inputSlot > 0); var property = (PropertySymbol)AsMemberOfType(inputType, e.Property); var type = property.TypeWithAnnotations; var output = new BoundDagTemp(e.Syntax, type.Type, e); int outputSlot = GetOrCreateSlot(property, inputSlot, forceSlotEvenIfEmpty: true); if (outputSlot <= 0) { // This is needed due to https://github.com/dotnet/roslyn/issues/29619 outputSlot = makeDagTempSlot(type, output); } Debug.Assert(outputSlot > 0); addToTempMap(output, outputSlot, type.Type); break; } case BoundDagIndexEvaluation e: { var type = TypeWithAnnotations.Create(e.Property.Type, NullableAnnotation.Annotated); var output = new BoundDagTemp(e.Syntax, type.Type, e); int outputSlot = makeDagTempSlot(type, output); Debug.Assert(outputSlot > 0); addToTempMap(output, outputSlot, type.Type); break; } default: throw ExceptionUtilities.UnexpectedValue(p.Evaluation.Kind); } gotoNode(p.Next, this.State, nodeBelievedReachable); break; } case BoundTestDecisionDagNode p: { var test = p.Test; bool foundTemp = tempMap.TryGetValue(test.Input, out var slotAndType); Debug.Assert(foundTemp); (int inputSlot, TypeSymbol inputType) = slotAndType; var inputState = this.State[inputSlot]; Split(); switch (test) { case BoundDagTypeTest t: if (inputSlot > 0) { learnFromNonNullTest(inputSlot, ref this.StateWhenTrue); } gotoNode(p.WhenTrue, this.StateWhenTrue, nodeBelievedReachable); gotoNode(p.WhenFalse, this.StateWhenFalse, nodeBelievedReachable); break; case BoundDagNonNullTest t: if (inputSlot > 0) { learnFromNonNullTest(inputSlot, ref this.StateWhenTrue); } gotoNode(p.WhenTrue, this.StateWhenTrue, nodeBelievedReachable); gotoNode(p.WhenFalse, this.StateWhenFalse, nodeBelievedReachable & inputState.MayBeNull()); break; case BoundDagExplicitNullTest t: if (inputSlot > 0) { LearnFromNullTest(inputSlot, inputType, ref this.StateWhenTrue); learnFromNonNullTest(inputSlot, ref this.StateWhenFalse); } gotoNode(p.WhenTrue, this.StateWhenTrue, nodeBelievedReachable); gotoNode(p.WhenFalse, this.StateWhenFalse, nodeBelievedReachable); break; case BoundDagValueTest t: Debug.Assert(t.Value != ConstantValue.Null); if (inputSlot > 0) { learnFromNonNullTest(inputSlot, ref this.StateWhenTrue); } gotoNode(p.WhenTrue, this.StateWhenTrue, nodeBelievedReachable); gotoNode(p.WhenFalse, this.StateWhenFalse, nodeBelievedReachable); break; default: throw ExceptionUtilities.UnexpectedValue(test.Kind); } break; } case BoundLeafDecisionDagNode d: // We have one leaf decision dag node per reachable label labelStateMap.Add(d.Label, (this.State, nodeBelievedReachable)); break; case BoundWhenDecisionDagNode w: // bind the pattern variables, inferring their types as well foreach (var binding in w.Bindings) { var variableAccess = binding.VariableAccess; var tempSource = binding.TempContainingValue; var foundTemp = tempMap.TryGetValue(tempSource, out var tempSlotAndType); Debug.Assert(foundTemp); var(tempSlot, tempType) = tempSlotAndType; var tempState = this.State[tempSlot]; if (variableAccess is BoundLocal { LocalSymbol: SourceLocalSymbol { IsVar: true } local }) { var inferredType = TypeWithState.Create(tempType, tempState).ToTypeWithAnnotations(); if (_variableTypes.TryGetValue(local, out var existingType)) { // merge inferred nullable annotation from different branches of the decision tree _variableTypes[local] = TypeWithAnnotations.Create(existingType.Type, existingType.NullableAnnotation.Join(inferredType.NullableAnnotation)); } else { _variableTypes[local] = inferredType; } int localSlot = GetOrCreateSlot(local, forceSlotEvenIfEmpty: true); this.State[localSlot] = tempState; }
/// <summary> /// Verify the type and nullability inferred by NullabilityWalker of all expressions in the source /// that are followed by specific annotations. Annotations are of the form /*T:type*/. /// </summary> internal static void VerifyTypes(this CSharpCompilation compilation, SyntaxTree tree = null) { Assert.True(compilation.NullableSemanticAnalysisEnabled); if (tree == null) { foreach (var syntaxTree in compilation.SyntaxTrees) { VerifyTypes(compilation, syntaxTree); } return; } var root = tree.GetRoot(); var allAnnotations = getAnnotations(); if (allAnnotations.IsEmpty) { return; } var model = compilation.GetSemanticModel(tree); var annotationsByMethod = allAnnotations.GroupBy(annotation => annotation.Expression.Ancestors().OfType <BaseMethodDeclarationSyntax>().First()).ToArray(); foreach (var annotations in annotationsByMethod) { var methodSyntax = annotations.Key; var method = model.GetDeclaredSymbol(methodSyntax); var expectedTypes = annotations.SelectAsArray(annotation => annotation.Text); var actualTypes = annotations.SelectAsArray(annotation => { var typeInfo = model.GetTypeInfo(annotation.Expression); Assert.NotEqual(CodeAnalysis.NullableFlowState.None, typeInfo.Nullability.FlowState); // https://github.com/dotnet/roslyn/issues/35035: After refactoring symboldisplay, we should be able to just call something like typeInfo.Type.ToDisplayString(typeInfo.Nullability.FlowState, TypeWithState.TestDisplayFormat) var type = TypeWithState.Create( (annotation.IsConverted ? typeInfo.ConvertedType : typeInfo.Type).GetSymbol(), (annotation.IsConverted ? typeInfo.ConvertedNullability : typeInfo.Nullability).FlowState.ToInternalFlowState()).ToTypeWithAnnotations(); return(type.ToDisplayString(TypeWithAnnotations.TestDisplayFormat)); }); // Consider reporting the correct source with annotations on mismatch. AssertEx.Equal(expectedTypes, actualTypes, message: method.ToTestDisplayString()); } ImmutableArray <(ExpressionSyntax Expression, string Text, bool IsConverted)> getAnnotations() { var builder = ArrayBuilder <(ExpressionSyntax, string, bool)> .GetInstance(); foreach (var token in root.DescendantTokens()) { foreach (var trivia in token.TrailingTrivia) { if (trivia.Kind() == SyntaxKind.MultiLineCommentTrivia) { var text = trivia.ToFullString(); const string typePrefix = "/*T:"; const string convertedPrefix = "/*CT:"; const string suffix = "*/"; bool startsWithTypePrefix = text.StartsWith(typePrefix); if (text.EndsWith(suffix) && (startsWithTypePrefix || text.StartsWith(convertedPrefix))) { var prefix = startsWithTypePrefix ? typePrefix : convertedPrefix; var expr = getEnclosingExpression(token); Assert.True(expr != null, $"VerifyTypes could not find a matching expression for annotation '{text}'."); var content = text.Substring(prefix.Length, text.Length - prefix.Length - suffix.Length); builder.Add((expr, content, !startsWithTypePrefix)); } } } } return(builder.ToImmutableAndFree()); } ExpressionSyntax getEnclosingExpression(SyntaxToken token) { var node = token.Parent; while (true) { var expr = asExpression(node); if (expr != null) { return(expr); } if (node == root) { break; } node = node.Parent; } return(null); } ExpressionSyntax asExpression(SyntaxNode node) { while (true) { switch (node) { case null: return(null); case ParenthesizedExpressionSyntax paren: return(paren.Expression); case IdentifierNameSyntax id when id.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == node: node = memberAccess; continue; case ExpressionSyntax expr when expr.Parent is ConditionalAccessExpressionSyntax cond && cond.WhenNotNull == node: node = cond; continue; case ExpressionSyntax expr: return(expr); case { Parent: var parent } : node = parent; continue; }
public void VisitWithNullability(TypeSymbol symbol, NullableFlowState topLevelNullability) { VisitWithAnnotation(TypeWithState.Create(symbol, topLevelNullability).ToTypeWithAnnotations()); }
/// <summary> /// Verify the type and nullability inferred by NullabilityWalker of all expressions in the source /// that are followed by specific annotations. Annotations are of the form /*T:type*/. /// </summary> internal static void VerifyTypes(this CSharpCompilation compilation, SyntaxTree tree = null) { // When nullable analysis does not require a feature flag, this can be removed so that we // don't need to create an extra compilation if (compilation.Feature("run-nullable-analysis") != "true") { compilation = compilation.WithAdditionalFeatures(("run-nullable-analysis", "true")); } if (tree == null) { foreach (var syntaxTree in compilation.SyntaxTrees) { VerifyTypes(compilation, syntaxTree); } return; } var root = tree.GetRoot(); var allAnnotations = getAnnotations(); if (allAnnotations.IsEmpty) { return; } var model = compilation.GetSemanticModel(tree); var annotationsByMethod = allAnnotations.GroupBy(annotation => annotation.Expression.Ancestors().OfType <BaseMethodDeclarationSyntax>().First()).ToArray(); foreach (var annotations in annotationsByMethod) { var methodSyntax = annotations.Key; var method = model.GetDeclaredSymbol(methodSyntax); var expectedTypes = annotations.SelectAsArray(annotation => annotation.Text); var actualTypes = annotations.SelectAsArray(annotation => { var typeInfo = model.GetTypeInfo(annotation.Expression); Assert.NotEqual(CodeAnalysis.NullableAnnotation.NotApplicable, typeInfo.Nullability.Annotation); Assert.NotEqual(CodeAnalysis.NullableFlowState.NotApplicable, typeInfo.Nullability.FlowState); // https://github.com/dotnet/roslyn/issues/35035: After refactoring symboldisplay, we should be able to just call something like typeInfo.Type.ToDisplayString(typeInfo.Nullability.FlowState, TypeWithState.TestDisplayFormat) return(TypeWithState.Create((TypeSymbol)typeInfo.Type, typeInfo.Nullability.FlowState.ToInternalFlowState()).ToTypeWithAnnotations().ToDisplayString(TypeWithAnnotations.TestDisplayFormat)); }); // Consider reporting the correct source with annotations on mismatch. AssertEx.Equal(expectedTypes, actualTypes, message: method.ToTestDisplayString()); } ImmutableArray <(ExpressionSyntax Expression, string Text)> getAnnotations() { var builder = ArrayBuilder <(ExpressionSyntax, string)> .GetInstance(); foreach (var token in root.DescendantTokens()) { foreach (var trivia in token.TrailingTrivia) { if (trivia.Kind() == SyntaxKind.MultiLineCommentTrivia) { var text = trivia.ToFullString(); const string prefix = "/*T:"; const string suffix = "*/"; if (text.StartsWith(prefix) && text.EndsWith(suffix)) { var expr = getEnclosingExpression(token); Assert.True(expr != null, $"VerifyTypes could not find a matching expression for annotation '{text}'."); var content = text.Substring(prefix.Length, text.Length - prefix.Length - suffix.Length); builder.Add((expr, content)); } } } } return(builder.ToImmutableAndFree()); } ExpressionSyntax getEnclosingExpression(SyntaxToken token) { var node = token.Parent; while (true) { var expr = asExpression(node); if (expr != null) { return(expr); } if (node == root) { break; } node = node.Parent; } return(null); } ExpressionSyntax asExpression(SyntaxNode node) { var expr = node as ExpressionSyntax; if (expr == null) { return(null); } switch (expr.Kind()) { case SyntaxKind.ParenthesizedExpression: return(((ParenthesizedExpressionSyntax)expr).Expression); case SyntaxKind.IdentifierName: if (expr.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == expr) { return(memberAccess); } break; } return(expr); } }