diff --git a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs index 1904045ddddb9..c3532eadac9b6 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs +++ b/src/Compilers/CSharp/Portable/CSharpResources.Designer.cs @@ -4660,6 +4660,15 @@ internal static string ERR_FeatureNotAvailableInVersion7_2 { } } + /// + /// Looks up a localized string similar to Feature '{0}' is not available in C# 7.3. Please use language version {1} or greater.. + /// + internal static string ERR_FeatureNotAvailableInVersion7_3 { + get { + return ResourceManager.GetString("ERR_FeatureNotAvailableInVersion7_3", resourceCulture); + } + } + /// /// Looks up a localized string similar to Feature '{0}' is not available in C# 8. Please use language version {1} or greater.. /// diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPassBase.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPassBase.cs index 87ba7a1f08263..6b5903774890d 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPassBase.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/DataFlowPassBase.cs @@ -116,7 +116,16 @@ protected int GetOrCreateSlot(Symbol symbol, int containingSlot = 0) variableBySlot[slot] = identifier; } - Normalize(ref this.State); + if (IsConditionalState) + { + Normalize(ref this.StateWhenTrue); + Normalize(ref this.StateWhenFalse); + } + else + { + Normalize(ref this.State); + } + return slot; } diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs index fa81d9b3e96e4..3a1afd333c436 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs @@ -59,6 +59,12 @@ internal sealed partial class NullableWalker : DataFlowPassBase private PooledDictionary _placeholderLocals; + /// + /// For methods with annotations, we'll need to visit the arguments twice. + /// Once for diagnostics and once for result state (but disabling diagnostics). + /// + private bool _disableDiagnostics = false; + protected override void Free() { _variableTypes.Free(); @@ -464,7 +470,10 @@ private bool IsByRefTarget(int slot) private void ReportStaticNullCheckingDiagnostics(ErrorCode errorCode, SyntaxNode syntaxNode, params object[] arguments) { - Diagnostics.Add(errorCode, syntaxNode.GetLocation(), arguments); + if (!_disableDiagnostics) + { + Diagnostics.Add(errorCode, syntaxNode.GetLocation(), arguments); + } } private void InheritNullableStateOfTrackableStruct(TypeSymbol targetType, int targetSlot, int valueSlot, bool isByRefTarget, int slotWatermark) @@ -1578,12 +1587,14 @@ public override BoundNode VisitCall(BoundCall node) { ImmutableArray refKindsOpt = node.ArgumentRefKindsOpt; ImmutableArray arguments = RemoveArgumentConversions(node.Arguments, refKindsOpt); - ImmutableArray results = VisitArgumentsEvaluate(arguments, refKindsOpt, node.Expanded); + ImmutableArray argsToParamsOpt = node.ArgsToParamsOpt; + ImmutableArray results = VisitArgumentsEvaluate(arguments, refKindsOpt, method.Parameters, argsToParamsOpt, node.Expanded); + if (method.IsGenericMethod && HasImplicitTypeArguments(node)) { method = InferMethod(node, method, results.SelectAsArray(r => r.Type)); } - VisitArgumentsWarn(arguments, refKindsOpt, method.Parameters, node.ArgsToParamsOpt, node.Expanded, results); + VisitArgumentsWarn(arguments, refKindsOpt, method.Parameters, argsToParamsOpt, node.Expanded, results); } UpdateStateForCall(node); @@ -1594,7 +1605,6 @@ public override BoundNode VisitCall(BoundCall node) ReplayReadsAndWrites(localFunc, node.Syntax, writes: true); } - Debug.Assert(!IsConditionalState); //if (this.State.Reachable) // PROTOTYPE(NullableReferenceTypes): Consider reachability? { _result = method.ReturnType; @@ -1603,6 +1613,49 @@ public override BoundNode VisitCall(BoundCall node) return null; } + /// + /// For each argument, figure out if its corresponding parameter is annotated with NotNullWhenFalse or + /// EnsuresNotNull. + /// + private static ImmutableArray GetAnnotations(int numArguments, + bool expanded, ImmutableArray parameters, ImmutableArray argsToParamsOpt) + { + ArrayBuilder builder = null; + + for (int i = 0; i < numArguments; i++) + { + (ParameterSymbol parameter, _) = GetCorrespondingParameter(i, parameters, argsToParamsOpt, expanded); + AttributeAnnotations annotations = parameter?.FlowAnalysisAnnotations ?? AttributeAnnotations.None; + + // We'll ignore NotNullWhenFalse that is misused in metadata + if ((annotations & AttributeAnnotations.NotNullWhenFalse) != 0 && + parameter.ContainingSymbol.GetTypeOrReturnType().SpecialType != SpecialType.System_Boolean) + { + annotations &= ~AttributeAnnotations.NotNullWhenFalse; + } + + // We'll ignore EnsuresNotNull that is misused in metadata + if ((annotations & AttributeAnnotations.EnsuresNotNull) != 0 && + (parameter.Type?.IsValueType != false || parameter.IsParams)) + { + annotations &= ~AttributeAnnotations.EnsuresNotNull; + } + + if (annotations != AttributeAnnotations.None && builder == null) + { + builder = ArrayBuilder.GetInstance(numArguments); + builder.AddMany(AttributeAnnotations.None, i); + } + + if (builder != null) + { + builder.Add(annotations); + } + } + + return builder == null ? default : builder.ToImmutableAndFree(); + } + // PROTOTYPE(NullableReferenceTypes): Record in the node whether type // arguments were implicit, to allow for cases where the syntax is not an // invocation (such as a synthesized call from a query interpretation). @@ -1661,7 +1714,7 @@ private void VisitArguments( bool expanded) { Debug.Assert(!arguments.IsDefault); - ImmutableArray results = VisitArgumentsEvaluate(arguments, refKindsOpt, expanded); + ImmutableArray results = VisitArgumentsEvaluate(arguments, refKindsOpt, parameters, argsToParamsOpt, expanded); // PROTOTYPE(NullableReferenceTypes): Can we handle some error cases? // (Compare with CSharpOperationFactory.CreateBoundCallOperation.) if (!node.HasErrors && !parameters.IsDefault) @@ -1673,7 +1726,31 @@ private void VisitArguments( private ImmutableArray VisitArgumentsEvaluate( ImmutableArray arguments, ImmutableArray refKindsOpt, + ImmutableArray parameters, + ImmutableArray argsToParamsOpt, bool expanded) + { + var savedState = this.State.Clone(); + // We do a first pass to work through the arguments without making any assumptions + var results = VisitArgumentsEvaluate(arguments, refKindsOpt); + + // We do a second pass through the arguments, ignoring any diagnostics produced, but honoring the annotations, + // to get the proper result state. + ImmutableArray annotations = GetAnnotations(arguments.Length, + expanded, parameters, argsToParamsOpt); + + if (!annotations.IsDefault) + { + this.SetState(savedState); + VisitArgumentsEvaluateHonoringAnnotations(arguments, refKindsOpt, annotations); + } + + return results; + } + + private ImmutableArray VisitArgumentsEvaluate( + ImmutableArray arguments, + ImmutableArray refKindsOpt) { Debug.Assert(!IsConditionalState); int n = arguments.Length; @@ -1684,22 +1761,118 @@ private ImmutableArray VisitArgumentsEvaluate( var builder = ArrayBuilder.GetInstance(n); for (int i = 0; i < n; i++) { - RefKind refKind = GetRefKind(refKindsOpt, i); - var argument = arguments[i]; - if (refKind != RefKind.Out) + VisitArgumentEvaluate(arguments, refKindsOpt, i); + builder.Add(_result); + } + + _result = _invalidType; + return builder.ToImmutableAndFree(); + } + + private void VisitArgumentEvaluate(ImmutableArray arguments, ImmutableArray refKindsOpt, int i) + { + RefKind refKind = GetRefKind(refKindsOpt, i); + var argument = arguments[i]; + if (refKind != RefKind.Out) + { + // PROTOTYPE(NullReferenceTypes): `ref` arguments should be treated as l-values + // for assignment. See `ref x3` in StaticNullChecking.PassingParameters_01. + VisitRvalue(argument); + } + else + { + VisitLvalue(argument); + } + } + + /// + /// Visit all the arguments for the purpose of computing the exit state of the method, + /// given the annotations, but disabling warnings. + /// + private void VisitArgumentsEvaluateHonoringAnnotations( + ImmutableArray arguments, + ImmutableArray refKindsOpt, + ImmutableArray annotations) + { + Debug.Assert(!IsConditionalState); + + bool saveDisableDiagnostics = _disableDiagnostics; + _disableDiagnostics = true; + + Debug.Assert(annotations.Length == arguments.Length); + + for (int i = 0; i < arguments.Length; i++) + { + if (this.IsConditionalState) { - // PROTOTYPE(NullReferenceTypes): `ref` arguments should be treated as l-values - // for assignment. See `ref x3` in StaticNullChecking.PassingParameters_01. - VisitRvalue(argument); + // We could be in a conditional state because of NotNullWhenFalse annotation + // Then WhenTrue/False states correspond to the invocation returning true/false + + // We'll assume that we're in the unconditional state where the method returns true, + // then we'll repeat assuming the method returns false. + + LocalState whenTrue = this.StateWhenTrue.Clone(); + LocalState whenFalse = this.StateWhenFalse.Clone(); + + this.SetState(whenTrue); + VisitArgumentEvaluate(arguments, refKindsOpt, i); + Debug.Assert(!IsConditionalState); + whenTrue = this.State; // LocalState may be a struct + + this.SetState(whenFalse); + VisitArgumentEvaluate(arguments, refKindsOpt, i); + Debug.Assert(!IsConditionalState); + whenFalse = this.State; // LocalState may be a struct + + this.SetConditionalState(whenTrue, whenFalse); } else { - VisitLvalue(argument); + VisitArgumentEvaluate(arguments, refKindsOpt, i); + } + + var argument = arguments[i]; + if (argument.Type?.IsReferenceType != true) + { + continue; + } + + int slot = MakeSlot(argument); + if (slot <= 0) + { + continue; + } + + AttributeAnnotations annotation = annotations[i]; + bool notNullWhenFalse = (annotation & AttributeAnnotations.NotNullWhenFalse) != 0; + bool ensuresNotNull = (annotation & AttributeAnnotations.EnsuresNotNull) != 0; + + if (ensuresNotNull) + { + // The variable in this slot is not null + if (this.IsConditionalState) + { + this.StateWhenTrue[slot] = true; + this.StateWhenFalse[slot] = true; + } + else + { + this.State[slot] = true; + } + } + else if (notNullWhenFalse) + { + // We'll use the WhenTrue/False states to represent whether the invocation returns true/false + // PROTOTYPE(NullableReferenceTypes): Consider splitting for the entire method, not just once the first annotated argument is encountered + Split(); + + // The variable in this slot is not null when the method returns false + this.StateWhenFalse[slot] = true; } - builder.Add(_result); } + _result = _invalidType; - return builder.ToImmutableAndFree(); + _disableDiagnostics = saveDisableDiagnostics; } private void VisitArgumentsWarn( @@ -1813,7 +1986,10 @@ private static ImmutableArray RemoveArgumentConversions(Immutab private static (ParameterSymbol, TypeSymbolWithAnnotations) GetCorrespondingParameter(int argumentOrdinal, ImmutableArray parameters, ImmutableArray argsToParamsOpt, bool expanded) { - Debug.Assert(!parameters.IsDefault); + if (parameters.IsDefault) + { + return (null, null); + } int n = parameters.Length; ParameterSymbol parameter; @@ -3068,7 +3244,7 @@ public override BoundNode VisitArgList(BoundArgList node) public override BoundNode VisitArgListOperator(BoundArgListOperator node) { - VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt, expanded: false); + VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt); Debug.Assert((object)node.Type == null); SetResult(node); return null; @@ -3148,7 +3324,7 @@ public override BoundNode VisitDynamicMemberAccess(BoundDynamicMemberAccess node public override BoundNode VisitDynamicInvocation(BoundDynamicInvocation node) { VisitRvalue(node.Expression); - VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt, expanded: false); + VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt); Debug.Assert(node.Type.IsDynamic()); Debug.Assert(node.Type.IsReferenceType); @@ -3176,7 +3352,7 @@ public override BoundNode VisitEventAssignmentOperator(BoundEventAssignmentOpera public override BoundNode VisitDynamicObjectCreationExpression(BoundDynamicObjectCreationExpression node) { Debug.Assert(!IsConditionalState); - VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt, expanded: false); + VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt); VisitObjectOrDynamicObjectCreation(node, node.InitializerExpressionOpt); return null; } @@ -3255,7 +3431,7 @@ public override BoundNode VisitDynamicIndexerAccess(BoundDynamicIndexerAccess no var receiver = node.ReceiverOpt; VisitRvalue(receiver); CheckPossibleNullReceiver(receiver); - VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt, expanded: false); + VisitArgumentsEvaluate(node.Arguments, node.ArgumentRefKindsOpt); Debug.Assert(node.Type.IsDynamic()); @@ -3421,7 +3597,7 @@ public override BoundNode VisitThrowExpression(BoundThrowExpression node) return result; } -#endregion Visitors + #endregion Visitors protected override string Dump(LocalState state) { @@ -3573,6 +3749,19 @@ public bool Reachable return _knownNullState.Capacity > 0; } } + + public override string ToString() + { + var pooledBuilder = PooledStringBuilder.GetInstance(); + var builder = pooledBuilder.Builder; + builder.Append(" "); + for (int i = this.Capacity - 1; i >= 0; i--) + { + builder.Append(_knownNullState[i] ? (_notNull[i] ? "!" : "?") : "_"); + } + + return pooledBuilder.ToStringAndFree(); + } } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/ExtraAnnotations.cs b/src/Compilers/CSharp/Portable/Symbols/ExtraAnnotations.cs new file mode 100644 index 0000000000000..86b28364d801f --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/ExtraAnnotations.cs @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Text; +using Microsoft.CodeAnalysis.CSharp.Symbols.Metadata.PE; +using Microsoft.CodeAnalysis.PooledObjects; +using static Microsoft.CodeAnalysis.CSharp.Symbols.AttributeAnnotations; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + [Flags] + internal enum AttributeAnnotations + { + None = 0, + NotNullWhenFalse, + EnsuresNotNull, + } + + // PROTOTYPE(NullableReferenceTypes): external annotations should be removed or fully designed/productized + // If we choose to stick with an ad-hoc key (rather than annotations as source or as PE/ref assembly), + // we should consider the assembly qualified name format used in metadata (with backticks and such). + internal static class ExtraAnnotations + { + // APIs that are useful to annotate: + // 1) don't accept null input + // 2) return a reference type + // All types in a member which can be annotated should be annotated. Value types and void can be skipped (with a `default`) + private static readonly ImmutableDictionary>> Annotations = + new Dictionary>> + { + { "System.Boolean System.Boolean.Parse(System.String)", Array(default, Nullable(false)) }, + { "System.Void System.Buffer.BlockCopy(System.Array, System.Int32, System.Array, System.Int32, System.Int32)", Array(default, Nullable(false), default, Nullable(false), default, default) }, + { "System.Int32 System.Buffer.ByteLength(System.Array)", Array(default, Nullable(false)) }, + { "System.Byte System.Buffer.GetByte(System.Array, System.Int32)", Array(default, Nullable(false), default) }, + { "System.Void System.Buffer.SetByte(System.Array, System.Int32, System.Byte)", Array(default, Nullable(false), default, default) }, + { "System.Byte System.Byte.Parse(System.String)", Array(default, Nullable(false)) }, + { "System.Byte System.Byte.Parse(System.String, System.IFormatProvider)", Array(default, Nullable(false), default) }, + { "System.Byte System.Byte.Parse(System.String, System.Globalization.NumberStyles)", Array(default, Nullable(false), Nullable(false)) }, + { "System.Byte System.Byte.Parse(System.String, System.Globalization.NumberStyles, System.IFormatProvider)", Array(default, Nullable(false), Nullable(false), default) }, + { "System.String System.String.Concat(System.String, System.String)", Array(Nullable(false), Nullable(true), Nullable(true)) }, + }.ToImmutableDictionary(); + + private static readonly ImmutableDictionary> Attributes = + new Dictionary> + { + { "System.Boolean System.String.IsNullOrEmpty(System.String)", Array(default, NotNullWhenFalse) }, + { "System.Boolean System.String.IsNullOrWhiteSpace(System.String)", Array(default, NotNullWhenFalse) }, + { "System.Boolean System.String.Contains(System.String)", Array(default, EnsuresNotNull) }, + }.ToImmutableDictionary(); + + internal static string MakeMethodKey(PEMethodSymbol method, ParamInfo[] paramInfo) + { + var pooledBuilder = PooledStringBuilder.GetInstance(); + + StringBuilder builder = pooledBuilder.Builder; + Add(paramInfo[0].Type, builder); + builder.Append(' '); + + Add(method.ContainingType, builder); + builder.Append('.'); + + builder.Append(method.Name); + builder.Append('('); + + for (int i = 1; i < paramInfo.Length; i++) + { + Add(paramInfo[i].Type, builder); + if (i < paramInfo.Length - 1) + { + builder.Append(", "); + } + } + + builder.Append(')'); + return pooledBuilder.ToStringAndFree(); + + // PROTOTYPE(NullableReferenceTypes): Many cases are not yet handled + // generic type args + // ref kind + // 'this' + // static vs. instance + // use assembly qualified name format (used in metadata) rather than symbol display? + } + + internal static string MakeMethodKey(MethodSymbol method) + { + var containingType = method.ContainingSymbol as TypeSymbol; + if (containingType is null) + { + return null; + } + + var pooledBuilder = PooledStringBuilder.GetInstance(); + + StringBuilder builder = pooledBuilder.Builder; + Add(method.ReturnType.TypeSymbol, builder); + builder.Append(' '); + + Add(containingType, builder); + builder.Append('.'); + + builder.Append(method.Name); + builder.Append('('); + + var parameterTypes = method.ParameterTypes; + for (int i = 0; i < parameterTypes.Length; i++) + { + Add(parameterTypes[i].TypeSymbol, builder); + if (i < parameterTypes.Length - 1) + { + builder.Append(", "); + } + } + + builder.Append(')'); + return pooledBuilder.ToStringAndFree(); + + // PROTOTYPE(NullableReferenceTypes): Many cases are not yet handled + // generic type args + // ref kind + // 'this' + // static vs. instance + // use assembly qualified name format (used in metadata) rather than symbol display? + } + + private static ImmutableArray> Array(params ImmutableArray[] values) + => values.ToImmutableArray(); + + private static ImmutableArray Array(params AttributeAnnotations[] values) + => values.ToImmutableArray(); + + private static ImmutableArray Nullable(params bool[] values) + { + Debug.Assert(values.Length > 0); + return values.ToImmutableArray(); + } + + internal static ImmutableArray> GetExtraAnnotations(string key) + { + if (key is null) + { + return default; + } + + if (!Annotations.TryGetValue(key, out var flags)) + { + return default; + } + + return flags; + } + + private static void Add(TypeSymbol type, StringBuilder builder) + => builder.Append( + type.ToDisplayString( + SymbolDisplayFormat.CSharpErrorMessageFormat + .RemoveMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes) + .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier) + // displaying tuple syntax causes to load the members of ValueTuple, which can cause a cycle, so we use long-hand format instead + .WithCompilerInternalOptions(SymbolDisplayCompilerInternalOptions.UseValueTuple))); + + /// + /// index 0 is used for return type + /// other parameters follow + /// + internal static (bool hasAny, AttributeAnnotations annotations) GetExtraAttributes(string key, int parameterIndex) + { + if (key is null) + { + return (false, default); + } + + if (!Attributes.TryGetValue(key, out var extraAttributes)) + { + return (false, default); + } + + return (true, extraAttributes[parameterIndex + 1]); + } + } + + internal static class ParameterAnnotationsExtensions + { + internal static AttributeAnnotations With(this AttributeAnnotations value, bool notNullWhenFalse, bool ensuresNotNull) + { + if (notNullWhenFalse) + { + value |= NotNullWhenFalse; + } + + if (ensuresNotNull) + { + value |= EnsuresNotNull; + } + + return value; + } + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/ExternalAnnotations.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/ExternalAnnotations.cs deleted file mode 100644 index ef9ee57835704..0000000000000 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/ExternalAnnotations.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Text; -using Microsoft.CodeAnalysis.PooledObjects; - -namespace Microsoft.CodeAnalysis.CSharp.Symbols.Metadata.PE -{ - // PROTOTYPE(NullableReferenceTypes): external annotations should be removed or fully designed/productized - // If we choose to stick with an ad-hoc key (rather than annotations as source or as PE/ref assembly), - // we should consider the assembly qualified name format used in metadata (with backticks and such). - internal static class ExtraAnnotations - { - // APIs that are useful to annotate: - // 1) don't accept null input - // 2) return a reference type - private static readonly ImmutableDictionary>> Annotations = - new Dictionary>> - { - { "System.Boolean System.Boolean.Parse(System.String)", Parameters(skip, Nullable(false)) }, - { "System.Void System.Buffer.BlockCopy(System.Array, System.Int32, System.Array, System.Int32, System.Int32)", Parameters(skip, Nullable(false), skip, Nullable(false), skip, skip) }, - { "System.Int32 System.Buffer.ByteLength(System.Array)", Parameters(skip, Nullable(false)) }, - { "System.Byte System.Buffer.GetByte(System.Array, System.Int32)", Parameters(skip, Nullable(false), skip) }, - { "System.Void System.Buffer.SetByte(System.Array, System.Int32, System.Byte)", Parameters(skip, Nullable(false), skip, skip) }, - { "System.Byte System.Byte.Parse(System.String)", Parameters(skip, Nullable(false)) }, - { "System.Byte System.Byte.Parse(System.String, System.IFormatProvider)", Parameters(skip, Nullable(false), skip) }, - { "System.Byte System.Byte.Parse(System.String, System.Globalization.NumberStyles)", Parameters(skip, Nullable(false), Nullable(false)) }, - { "System.Byte System.Byte.Parse(System.String, System.Globalization.NumberStyles, System.IFormatProvider)", Parameters(skip, Nullable(false), Nullable(false), skip) }, - { "System.String System.String.Concat(System.String, System.String)", Parameters(Nullable(false), Nullable(true), Nullable(true)) }, - }.ToImmutableDictionary(); - - internal static string MakeMethodKey(PEMethodSymbol method, ParamInfo[] paramInfo) - { - var pooledBuilder = PooledStringBuilder.GetInstance(); - - StringBuilder builder = pooledBuilder.Builder; - Add(paramInfo[0].Type, builder); - builder.Append(' '); - - Add(method.ContainingType, builder); - builder.Append('.'); - - builder.Append(method.Name); - builder.Append('('); - - for (int i = 1; i < paramInfo.Length; i++) - { - Add(paramInfo[i].Type, builder); - if (i < paramInfo.Length - 1) - { - builder.Append(", "); - } - } - - builder.Append(')'); - return pooledBuilder.ToStringAndFree(); - - // PROTOTYPE(NullableReferenceTypes): Many cases are not yet handled - // generic type args - // ref kind - // 'this' - // static vs. instance - // use assembly qualified name format (used in metadata) rather than symbol display? - } - - /// - /// All types in a member which can be annotated should be annotated. Value types and void can be skipped. - /// - private static readonly ImmutableArray skip = default; - - private static ImmutableArray> Parameters(params ImmutableArray[] values) - => values.ToImmutableArray(); - - private static ImmutableArray Nullable(params bool[] values) - { - Debug.Assert(values.Length > 0); - return values.ToImmutableArray(); - } - - internal static ImmutableArray> GetExtraAnnotations(string key) - { - if (!Annotations.TryGetValue(key, out var flags)) - { - return default; - } - - return flags; - } - - private static void Add(TypeSymbol type, StringBuilder builder) - => builder.Append( - type.ToDisplayString( - SymbolDisplayFormat.CSharpErrorMessageFormat - .RemoveMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes) - .AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier) - // displaying tuple syntax causes to load the members of ValueTuple, which can cause a cycle, so we use long-hand format instead - .WithCompilerInternalOptions(SymbolDisplayCompilerInternalOptions.UseValueTuple))); - } -} diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEParameterSymbol.cs index b128edded1722..dcc40d089c8ae 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEParameterSymbol.cs @@ -31,27 +31,29 @@ private enum WellKnownAttributeFlags IsCallerFilePath = 0x1 << 5, IsCallerLineNumber = 0x1 << 6, IsCallerMemberName = 0x1 << 7, + NotNullWhenFalse = 0x1 << 8, + EnsuresNotNull = 0x1 << 9, } private struct PackedFlags { // Layout: - // |.............|n|rr|cccccccc|vvvvvvvv| + // |.........|n|rr|cccccccccc|vvvvvvvvvv| // - // v = decoded well known attribute values. 8 bits. - // c = completion states for well known attributes. 1 if given attribute has been decoded, 0 otherwise. 8 bits. + // v = decoded well known attribute values. 10 bits. + // c = completion states for well known attributes. 1 if given attribute has been decoded, 0 otherwise. 10 bits. // r = RefKind. 2 bits. // n = hasNameInMetadata. 1 bit. private const int WellKnownAttributeDataOffset = 0; - private const int WellKnownAttributeCompletionFlagOffset = 8; - private const int RefKindOffset = 16; + private const int WellKnownAttributeCompletionFlagOffset = 10; + private const int RefKindOffset = 20; private const int RefKindMask = 0x3; - private const int WellKnownAttributeDataMask = 0xFF; + private const int WellKnownAttributeDataMask = 0x3FF; private const int WellKnownAttributeCompletionFlagMask = WellKnownAttributeDataMask; - private const int HasNameInMetadataBit = 0x1 << 18; + private const int HasNameInMetadataBit = 0x1 << 22; private const int AllWellKnownAttributesCompleteNoData = WellKnownAttributeCompletionFlagMask << WellKnownAttributeCompletionFlagOffset; @@ -645,6 +647,44 @@ internal override bool IsCallerMemberName } } + internal override AttributeAnnotations FlowAnalysisAnnotations + { + get + { + const WellKnownAttributeFlags notNullWhenFalse = WellKnownAttributeFlags.NotNullWhenFalse; + const WellKnownAttributeFlags ensuresNotNull = WellKnownAttributeFlags.EnsuresNotNull; + + // PROTOTYPE(NullableReferenceTypes): the flags could be packed more + if (!_packedFlags.TryGetWellKnownAttribute(notNullWhenFalse, out bool hasNotNullWhenFalse) || + !_packedFlags.TryGetWellKnownAttribute(ensuresNotNull, out bool hasEnsuresNotNull)) + { + (bool memberHasAny, AttributeAnnotations annotations) = TryGetExtraAttributeAnnotations(); + + if (memberHasAny) + { + // External annotations win, if any is present on the member + hasNotNullWhenFalse = (annotations & AttributeAnnotations.NotNullWhenFalse) != 0; + hasEnsuresNotNull = (annotations & AttributeAnnotations.EnsuresNotNull) != 0; + } + else + { + hasNotNullWhenFalse = _moduleSymbol.Module.HasAttribute(_handle, AttributeDescription.NotNullWhenFalseAttribute); + hasEnsuresNotNull = _moduleSymbol.Module.HasAttribute(_handle, AttributeDescription.EnsuresNotNullAttribute); + } + + _packedFlags.SetWellKnownAttribute(notNullWhenFalse, hasNotNullWhenFalse); + _packedFlags.SetWellKnownAttribute(ensuresNotNull, hasEnsuresNotNull); + + if (memberHasAny) + { + return annotations; + } + } + + return AttributeAnnotations.None.With(notNullWhenFalse: hasNotNullWhenFalse, ensuresNotNull: hasEnsuresNotNull); + } + } + public override TypeSymbolWithAnnotations Type { get @@ -653,7 +693,6 @@ public override TypeSymbolWithAnnotations Type } } - public override ImmutableArray RefCustomModifiers { get diff --git a/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs index 1ac342cad96c2..8f130640373b0 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs @@ -385,6 +385,27 @@ internal sealed override ObsoleteAttributeData ObsoleteAttributeData internal abstract bool IsCallerMemberName { get; } + internal abstract AttributeAnnotations FlowAnalysisAnnotations { get; } + + /// + /// If there are any annotations on the member (not just that parameter), then memberHasExtra is true. The purpose is to ensure + /// that if some annotations are present on the member, then annotations win over the attributes on the member in all positions. + /// That could mean removing an attribute. + /// + protected (bool memberHasExtra, AttributeAnnotations annotations) TryGetExtraAttributeAnnotations() + { + ParameterSymbol originalParameter = this.OriginalDefinition; + var containingMethod = originalParameter.ContainingSymbol as MethodSymbol; + + if (containingMethod is null) + { + return (false, AttributeAnnotations.None); + } + + string key = ExtraAnnotations.MakeMethodKey(containingMethod); + return ExtraAnnotations.GetExtraAttributes(key, this.Ordinal); + } + protected sealed override int HighestPriorityUseSiteError { get diff --git a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyParameterSymbol.cs index 51522bb85a1e7..926d0121d9ef4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyParameterSymbol.cs @@ -71,6 +71,8 @@ public override bool IsImplicitlyDeclared internal override bool IsCallerMemberName { get { throw ExceptionUtilities.Unreachable; } } + internal override AttributeAnnotations FlowAnalysisAnnotations { get { throw ExceptionUtilities.Unreachable; } } + public override Symbol ContainingSymbol { get { throw ExceptionUtilities.Unreachable; } } public override ImmutableArray Locations { get { throw ExceptionUtilities.Unreachable; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceClonedParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceClonedParameterSymbol.cs index c9452b9304c78..e80348dab543c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceClonedParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceClonedParameterSymbol.cs @@ -151,6 +151,11 @@ internal override bool IsCallerMemberName get { return _originalParam.IsCallerMemberName; } } + internal override AttributeAnnotations FlowAnalysisAnnotations + { + get { return AttributeAnnotations.None; } + } + #endregion } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs index bc305c6134578..1bd2f254c26d2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceComplexParameterSymbol.cs @@ -127,6 +127,25 @@ private bool HasCallerMemberNameAttribute && !HasCallerFilePathAttribute && HasCallerMemberNameAttribute; + internal override AttributeAnnotations FlowAnalysisAnnotations + { + get + { + (bool memberHasAny, AttributeAnnotations annotations) = TryGetExtraAttributeAnnotations(); + if (memberHasAny) + { + // PROTOTYPE(NullableReferenceTypes): Make sure this is covered by test + return annotations; + } + + CommonParameterWellKnownAttributeData attributeData = GetDecodedWellKnownAttributeData(); + + return AttributeAnnotations.None + .With(notNullWhenFalse: attributeData?.HasNotNullWhenFalseAttribute == true, + ensuresNotNull: attributeData?.HasEnsuresNotNullAttribute == true); + } + } + private ConstantValue DefaultSyntaxValue { get @@ -605,6 +624,14 @@ internal override void DecodeWellKnownAttribute(ref DecodeWellKnownAttributeArgu // NullableAttribute should not be set explicitly. arguments.Diagnostics.Add(ErrorCode.ERR_ExplicitNullableAttribute, arguments.AttributeSyntaxOpt.Location); } + else if (attribute.IsTargetAttribute(this, AttributeDescription.NotNullWhenFalseAttribute)) + { + arguments.GetOrCreateData().HasNotNullWhenFalseAttribute = true; + } + else if (attribute.IsTargetAttribute(this, AttributeDescription.EnsuresNotNullAttribute)) + { + arguments.GetOrCreateData().HasEnsuresNotNullAttribute = true; + } } private void DecodeDefaultParameterValueAttribute(AttributeDescription description, ref DecodeWellKnownAttributeArguments arguments) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceSimpleParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceSimpleParameterSymbol.cs index 037efa5046799..176c2e34ace97 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceSimpleParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceSimpleParameterSymbol.cs @@ -82,6 +82,15 @@ internal override bool IsCallerMemberName get { return false; } } + internal override AttributeAnnotations FlowAnalysisAnnotations + { + get + { + (_, AttributeAnnotations annotations) = TryGetExtraAttributeAnnotations(); + return annotations; + } + } + internal override MarshalPseudoCustomAttributeData MarshallingInformation { get { return null; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ThisParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ThisParameterSymbol.cs index 7df824a4c6cf6..d6ab91dc3a47f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ThisParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ThisParameterSymbol.cs @@ -114,6 +114,11 @@ internal override bool IsCallerMemberName get { return false; } } + internal override AttributeAnnotations FlowAnalysisAnnotations + { + get { return AttributeAnnotations.None; } + } + public override int Ordinal { get { return -1; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedParameterSymbol.cs index a49b86318f420..fc006dece1250 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedParameterSymbol.cs @@ -115,6 +115,11 @@ internal override bool IsCallerMemberName get { return false; } } + internal override AttributeAnnotations FlowAnalysisAnnotations + { + get { return AttributeAnnotations.None; } + } + public override Symbol ContainingSymbol { get { return _container; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedParameterSymbol.cs index 832d824c1aff2..c8d016c50725e 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedParameterSymbol.cs @@ -153,7 +153,13 @@ internal override bool IsCallerMemberName get { return _underlyingParameter.IsCallerMemberName; } } - public override string GetDocumentationCommentXml(CultureInfo preferredCulture = null, bool expandIncludes = false, CancellationToken cancellationToken = default(CancellationToken)) + internal override AttributeAnnotations FlowAnalysisAnnotations + { + // PROTOTYPE(NullableReferenceTypes): Consider moving to leaf types + get { return _underlyingParameter.FlowAnalysisAnnotations; } + } + + public override string GetDocumentationCommentXml(CultureInfo preferredCulture = null, bool expandIncludes = false, CancellationToken cancellationToken = default) { return _underlyingParameter.GetDocumentationCommentXml(preferredCulture, expandIncludes, cancellationToken); } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/StaticNullChecking.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/StaticNullChecking.cs index de793af9db693..bd43b0c8c95ce 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/StaticNullChecking.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/StaticNullChecking.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; +using static Microsoft.CodeAnalysis.CSharp.Symbols.AttributeAnnotations; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics { @@ -70,6 +71,30 @@ class NullableOptOutAttribute : Attribute public NullableOptOutAttribute(bool flag = true) { } } } +"; + + private const string NotNullWhenFalseAttributeDefinition = @" +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Parameter, + AllowMultiple = false)] + public class NotNullWhenFalseAttribute : Attribute + { + public NotNullWhenFalseAttribute() { } + } +} +"; + + private const string EnsuresNotNullAttributeDefinition = @" +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Parameter, + AllowMultiple = false)] + public class EnsuresNotNullAttribute : Attribute + { + public EnsuresNotNullAttribute() { } + } +} "; [Fact] @@ -5437,6 +5462,1591 @@ class CL1 ); } + [Fact] + public void MakeMethodKeyForWhereMethod() + { + // this test verifies that a bad method symbol doesn't crash when generating a key for external annotations + CSharpCompilation c = CreateCompilation(@" +class Test +{ + public void SimpleWhere() + { + int[] numbers = { 1, 2, 3 }; + var lowNums = from n in numbers + where n < 5 + select n; + } +}", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (7,33): error CS1935: Could not find an implementation of the query pattern for source type 'int[]'. 'Where' not found. Are you missing a reference to 'System.Core.dll' or a using directive for 'System.Linq'? + // var lowNums = from n in numbers + Diagnostic(ErrorCode.ERR_QueryNoProviderStandard, "numbers").WithArguments("int[]", "Where").WithLocation(7, 33) + ); + } + + [Fact] + public void NotNullWhenFalse_Simple() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + public void Main(string? s) + { + if (MyIsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + + s.ToString(); // warn 2 + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (16,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(16, 9) + ); + + VerifyAnnotationsAndMetadata(c, "C.Main", None); + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_EqualsTrue() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + public void Main(string? s) + { + if (MyIsNullOrEmpty(s) == true) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + + s.ToString(); // warn 2 + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + // PROTOTYPE(NullableReferenceTypes): there should only be two diagnostics + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // ok + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13), + // (16,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(16, 9) + ); + } + + [Fact] + public void NotNullWhenFalse_EqualsFalse() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + public void Main(string? s) + { + if (false == MyIsNullOrEmpty(s)) + { + s.ToString(); // ok + } + else + { + s.ToString(); // warn + } + + s.ToString(); // warn 2 + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + // PROTOTYPE(NullableReferenceTypes): there should only be two diagnostics + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // ok + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13), + // (16,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(16, 9) + ); + } + + [Fact] + public void NotNullWhenFalse_NotEqualsFalse() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + public void Main(string? s) + { + if (false != MyIsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + + s.ToString(); // warn 2 + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + // PROTOTYPE(NullableReferenceTypes): there should only be two diagnostics + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // ok + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13), + // (16,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(16, 9) + ); + } + + [Fact] + public void NotNullWhenFalse_RequiresBoolReturn() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + public static object MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotations(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_RequiresBoolReturn_OnGenericMethod() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(string? s) + { + if (MyIsNullOrEmpty(s, true)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } + public static T MyIsNullOrEmpty([NotNullWhenFalse] string? s, T t) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13) + ); + } + + [Fact] + public void NotNullWhenFalse_OnTwoParameters() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s, string? s2) + { + if (MyIsNullOrEmpty(s, s2)) + { + s.ToString(); // warn 1 + s2.ToString(); // warn 2 + } + else + { + s.ToString(); // ok + s2.ToString(); // ok + } + + s.ToString(); // warn 3 + s2.ToString(); // warn 4 + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s, [NotNullWhenFalse] string? s2) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (10,13): warning CS8602: Possible dereference of a null reference. + // s2.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(10, 13), + // (18,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 3 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(18, 9), + // (19,9): warning CS8602: Possible dereference of a null reference. + // s2.ToString(); // warn 4 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(19, 9) + ); + + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", NotNullWhenFalse, NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_OnIndexer() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s, int x) + { + if (this[s, x]) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + + s.ToString(); // warn 2 + } + public bool this[[NotNullWhenFalse] string? s, int x] => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (16,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(16, 9) + ); + } + + [Fact] + public void NotNullWhenFalse_SecondArgumentDereferences() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + if (Method(s, s.ToString())) // warn 1 + { + s.ToString(); // warn 2 + } + else + { + s.ToString(); // ok + } + } + static bool Method([NotNullWhenFalse] string? s, string s2) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (7,23): warning CS8602: Possible dereference of a null reference. + // if (Method(s, s.ToString())) // warn 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(7, 23), + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13) + ); + } + + [Fact] + public void NotNullWhenFalse_SecondArgumentAssigns() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + if (Method(s, s = null)) + { + s.ToString(); // warn 1 + } + else + { + s.ToString(); // warn 2 + } + } + static bool Method([NotNullWhenFalse] string? s, string? s2) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) + ); + } + + [Fact] + public void NotNullWhenFalse_MissingAttribute() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void Main(string? s) + { + if (MyIsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // warn 2 + } + + s.ToString(); // warn 3 + } + static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (17,34): error CS0246: The type or namespace name 'NotNullWhenFalseAttribute' could not be found (are you missing a using directive or an assembly reference?) + // static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; + Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "NotNullWhenFalse").WithArguments("NotNullWhenFalseAttribute").WithLocation(17, 34), + // (17,34): error CS0246: The type or namespace name 'NotNullWhenFalse' could not be found (are you missing a using directive or an assembly reference?) + // static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; + Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "NotNullWhenFalse").WithArguments("NotNullWhenFalse").WithLocation(17, 34), + // (8,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 13), + // (12,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(12, 13), + // (15,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 3 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(15, 9) + ); + + VerifyAnnotations(c, "C.MyIsNullOrEmpty", None); + } + + private static void VerifyAnnotations(Compilation compilation, string memberName, params AttributeAnnotations[] expected) + { + var method = compilation.GetMember(memberName); + Assert.True((object)method != null, $"Could not find method '{memberName}'"); + var actual = method.Parameters.Select(p => p.FlowAnalysisAnnotations); + Assert.Equal(expected, actual); + } + + private void VerifyAnnotationsAndMetadata(Compilation compilation, string memberName, params AttributeAnnotations[] expected) + { + VerifyAnnotations(compilation, memberName, expected); + + // Also verify from metadata + var compilation2 = CreateCompilation("", references: new[] { compilation.EmitToImageReference() }); + VerifyAnnotations(compilation2, memberName, expected); + } + + [Fact] + public void NotNullWhenFalse_BadAttribute() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (MyIsNullOrEmpty(s)) + { + s.ToString(); // warn 1 + } + else + { + s.ToString(); // warn 2 + } + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse(true)] string? s) => throw null; +} +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Parameter, + AllowMultiple = false)] + public class NotNullWhenFalseAttribute : Attribute + { + public NotNullWhenFalseAttribute(bool bad) { } + } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) + ); + + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", None); + } + + [Fact] + public void NotNullWhenFalse_InvertIf() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (!MyIsNullOrEmpty(s)) + { + s.ToString(); // ok + } + else + { + s.ToString(); // warn + } + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) + ); + + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_WithNullLiteral() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + _ = MyIsNullOrEmpty(null); + } + static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + } + + [Fact] + public void NotNullWhenFalse_InstanceMethod() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (this.MyIsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } + public bool MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13) + ); + + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_ExtensionMethod() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (this.MyIsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } +} +public static class Extension +{ + public static bool MyIsNullOrEmpty(this C c, [NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13) + ); + + VerifyAnnotationsAndMetadata(c, "Extension.MyIsNullOrEmpty", None, NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_String_IsNullOrEmpty() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void Main(string? s) + { + if (string.IsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 13) + ); + + VerifyAnnotationsAndMetadata(c, "System.String.IsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_String_IsNullOrEmpty_WithoutCorlib() + { + CSharpCompilation c = CreateEmptyCompilation(@" +class C +{ + void Main(string? s) + { + if (string.IsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } +} +namespace System +{ + public class Object + { + public virtual string ToString() => throw null; + } + public class String + { + public static bool IsNullOrEmpty(string? s) => throw null; + } + public struct Void { } + public struct Boolean { } + public class ValueType { } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 13) + ); + + VerifyAnnotations(c, "System.String.IsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_String_IsNullOrWhiteSpace_WithoutCorlib() + { + CSharpCompilation c = CreateEmptyCompilation(@" +class C +{ + void Main(string? s) + { + if (string.IsNullOrWhiteSpace(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } +} +namespace System +{ + public class Object + { + public virtual string ToString() => throw null; + } + public class String + { + public static bool IsNullOrWhiteSpace(string? s) => throw null; + } + public struct Void { } + public struct Boolean { } + public class ValueType { } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 13) + ); + + VerifyAnnotations(c, "System.String.IsNullOrWhiteSpace", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_String_NotIsNullOrEmpty() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void Main(string? s) + { + if (!string.IsNullOrEmpty(s)) + { + s.ToString(); // ok + } + else + { + s.ToString(); // warn + } + } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (12,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(12, 13) + ); + + VerifyAnnotationsAndMetadata(c, "System.String.IsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_String_NotIsNullOrEmpty_NoDuplicateWarnings() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void M() + { + if (!string.IsNullOrEmpty(M2(null))) + { + } + } + string? M2(string s) => throw null; +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (6,38): warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter. + // if (!string.IsNullOrEmpty(M2(null))) + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(6, 38) + ); + } + + [Fact] + public void NotNullWhenFalse_String_NotIsNullOrEmpty_NotAString() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void M() + { + if (!string.IsNullOrEmpty(M2(null))) + { + } + } + void M2(string s) => throw null; +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (6,35): error CS1503: Argument 1: cannot convert from 'void' to 'string' + // if (!string.IsNullOrEmpty(M2(null))) + Diagnostic(ErrorCode.ERR_BadArgType, "M2(null)").WithArguments("1", "void", "string").WithLocation(6, 35) + ); + } + + [Fact] + public void NotNullWhenFalse_String_IsNullOrWhiteSpace() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void Main(string? s) + { + if (string.IsNullOrWhiteSpace(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // ok + } + } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 13) + ); + + VerifyAnnotationsAndMetadata(c, "System.String.IsNullOrWhiteSpace", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_PartialMethod() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public partial class C +{ + partial void M1(string? s); + partial void M1([NotNullWhenFalse] string? s) => throw null; + + partial void M2([NotNullWhenFalse] string? s); + partial void M2(string? s) => throw null; + + partial void M3([NotNullWhenFalse] string? s); + partial void M3([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (11,22): error CS0579: Duplicate 'NotNullWhenFalse' attribute + // partial void M3([NotNullWhenFalse] string? s); + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "NotNullWhenFalse").WithArguments("NotNullWhenFalse").WithLocation(11, 22) + ); + + VerifyAnnotations(c, "C.M1", NotNullWhenFalse); + VerifyAnnotations(c, "C.M2", NotNullWhenFalse); + VerifyAnnotations(c, "C.M3", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_ReturningDynamic() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (MyIsNullOrEmpty(s)) + { + s.ToString(); // warn + } + else + { + s.ToString(); // warn 2 + } + } + public dynamic MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (9,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(9, 13), + // (13,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(13, 13) + ); + + VerifyAnnotations(c, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_ReturningObject_FromMetadata() + { + string il = @" +.class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.NullableAttribute + extends [mscorlib]System.Attribute +{ + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + IL_0000: ldnull + IL_0001: throw + } +} + +.class public auto ansi beforefieldinit C + extends [mscorlib]System.Object +{ + .method public hidebysig + instance object MyIsNullOrEmpty (string s) cil managed + { + .param [1] + .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor() = ( + 01 00 00 00 + ) + .custom instance void System.Runtime.CompilerServices.NotNullWhenFalseAttribute::.ctor() = ( + 01 00 00 00 + ) + + IL_0000: ldnull + IL_0001: throw + } + + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + IL_0000: ldnull + IL_0001: throw + } +} + +.class public auto ansi beforefieldinit System.Runtime.CompilerServices.NotNullWhenFalseAttribute + extends [mscorlib]System.Attribute +{ + .custom instance void [mscorlib]System.AttributeUsageAttribute::.ctor(valuetype [mscorlib]System.AttributeTargets) = ( + 01 00 00 08 00 00 01 00 54 02 0d 41 6c 6c 6f 77 4d 75 6c 74 69 70 6c 65 00 + ) + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + IL_0000: ldnull + IL_0001: throw + } +} +"; + string source = @" +public class D +{ + void Main(C c, string? s) + { + if ((bool)c.MyIsNullOrEmpty(s)) + { + s.ToString(); // warn 1 + } + else + { + s.ToString(); // warn 2 + } + } +} +"; + var compilation = CreateCompilationWithIL(source, il, parseOptions: TestOptions.Regular8); + compilation.VerifyDiagnostics( + // (8,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 13), + // (12,13): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(12, 13) + ); + + VerifyAnnotations(compilation, "C.MyIsNullOrEmpty", NotNullWhenFalse); + } + + [Fact] + public void NotNullWhenFalse_ReturningObject() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + MyIsNullOrEmpty(s); + s.ToString(); // warn + } + object MyIsNullOrEmpty([NotNullWhenFalse] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 9) + ); + } + + [Fact] + public void NotNullWhenFalse_FollowedByEnsuresNotNull() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (MyIsNullOrEmpty(s, s)) + { + s.ToString(); // ok + } + else + { + s.ToString(); // ok + } + + s.ToString(); // ok + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse] string? s, [EnsuresNotNull] string? s2) => throw null; +} +" + NotNullWhenFalseAttributeDefinition + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", NotNullWhenFalse, EnsuresNotNull); + } + + [Fact] + public void NotNullWhenFalse_AndEnsuresNotNull() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + if (MyIsNullOrEmpty(s)) + { + s.ToString(); // ok + } + else + { + s.ToString(); // ok + } + + s.ToString(); // ok + } + public static bool MyIsNullOrEmpty([NotNullWhenFalse, EnsuresNotNull] string? s) => throw null; +} +" + NotNullWhenFalseAttributeDefinition + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotationsAndMetadata(c, "C.MyIsNullOrEmpty", NotNullWhenFalse | EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_Simple() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + ThrowIfNull(42, s); + s.ToString(); // ok + } + public static void ThrowIfNull(int x, [EnsuresNotNull] string? s) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotationsAndMetadata(c, "C.ThrowIfNull", None, EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_Generic_WithRefType() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + ThrowIfNull(s); + s.ToString(); // ok + } + public static void ThrowIfNull([EnsuresNotNull] T s) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + } + + [Fact] + public void EnsuresNotNull_Generic_WithValueType() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(int s) + { + ThrowIfNull(s); + s.ToString(); + } + public static void ThrowIfNull([EnsuresNotNull] T s) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + } + + [Fact] + public void EnsuresNotNull_Generic_WithUnconstrainedGenericType() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(U u) + { + ThrowIfNull(u); + u.ToString(); + } + public static void ThrowIfNull([EnsuresNotNull] T s) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // u.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "u").WithLocation(8, 9) + ); + } + + [Fact] + public void EnsuresNotNull_OnInterface() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s, Interface i) + { + i.ThrowIfNull(42, s); + s.ToString(); // ok + } +} +public interface Interface +{ + void ThrowIfNull(int x, [EnsuresNotNull] string? s); +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotationsAndMetadata(c, "Interface.ThrowIfNull", None, EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_OnInterface_ImplementedWithoutAttribute() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C : Interface +{ + void Main(string? s) + { + this.ThrowIfNull(42, s); + s.ToString(); // warn + ((Interface)this).ThrowIfNull(42, s); + s.ToString(); // ok + } + public void ThrowIfNull(int x, string? s) => throw null; +} +public interface Interface +{ + void ThrowIfNull(int x, [EnsuresNotNull] string? s); +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 9) + ); + + VerifyAnnotationsAndMetadata(c, "Interface.ThrowIfNull", None, EnsuresNotNull); + VerifyAnnotationsAndMetadata(c, "C.ThrowIfNull", None, None); + } + + [Fact] + public void EnsuresNotNull_OnInterface_ImplementedWithAttribute() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C : Interface +{ + void Main(string? s) + { + ((Interface)this).ThrowIfNull(42, s); + s.ToString(); // warn + this.ThrowIfNull(42, s); + s.ToString(); // ok + } + public void ThrowIfNull(int x, [EnsuresNotNull] string? s) => throw null; +} +public interface Interface +{ + void ThrowIfNull(int x, string? s); +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 9) + ); + + VerifyAnnotationsAndMetadata(c, "Interface.ThrowIfNull", None, None); + VerifyAnnotationsAndMetadata(c, "C.ThrowIfNull", None, EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_OnDelegate() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +delegate void D([EnsuresNotNull] object? o); +public class C +{ + void Main(string? s, D d) + { + d(s); + s.ToString(); // ok + } +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + } + + [Fact] + public void EnsuresNotNull_WithParams() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + static void EnsuresNotNull([EnsuresNotNull] params object?[]? args) { } + static void F(object? x, object? y, object[]? a) + { + EnsuresNotNull(); + a.ToString(); // warn 1 + + EnsuresNotNull(a); + a.ToString(); // warn 2 + + EnsuresNotNull(x, y); + x.ToString(); // warn 3 + y.ToString(); // warn 4 + } +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + // PROTOTYPE(NullableReferenceTypes): Consider allowing EnsuresNotNull on params parameter + c.VerifyDiagnostics( + // (9,9): warning CS8602: Possible dereference of a null reference. + // a.ToString(); // warn 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "a").WithLocation(9, 9), + // (12,9): warning CS8602: Possible dereference of a null reference. + // a.ToString(); // warn 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "a").WithLocation(12, 9), + // (15,9): warning CS8602: Possible dereference of a null reference. + // x.ToString(); // warn 3 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(15, 9), + // (16,9): warning CS8602: Possible dereference of a null reference. + // y.ToString(); // warn 4 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "y").WithLocation(16, 9) + ); + } + + [Fact] + public void EnsuresNotNull_WithNamedArguments() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + static void EnsuresNotNull1([EnsuresNotNull] object? x = null, object? y = null) { } + static void EnsuresNotNull2(object? x = null, [EnsuresNotNull] object? y = null) { } + static void F(object? x) + { + EnsuresNotNull1(); + EnsuresNotNull1(y: x); + x.ToString(); // warn + EnsuresNotNull2(y: x); + x.ToString(); // ok + } +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (11,9): warning CS8602: Possible dereference of a null reference. + // x.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(11, 9) + ); + } + + [Fact] + public void EnsuresNotNull_OnDifferentTypes() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + public static void Bad([EnsuresNotNull] int i) => throw null; + public static void ThrowIfNull([EnsuresNotNull] T t) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotations(c, "C.Bad", EnsuresNotNull); + VerifyAnnotations(c, "C.ThrowIfNull", EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_GenericMethod() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(T t) + { + t.ToString(); // warn + ThrowIfNull(t); + t.ToString(); // ok + } + public static void ThrowIfNull([EnsuresNotNull] T s) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + // PROTOTYPE(NullableReferenceTypes): We're not tracking unconstrained generic types + c.VerifyDiagnostics( + // (7,9): warning CS8602: Possible dereference of a null reference. + // t.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t").WithLocation(7, 9), + // (9,9): warning CS8602: Possible dereference of a null reference. + // t.ToString(); // ok + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t").WithLocation(9, 9) + ); + + VerifyAnnotationsAndMetadata(c, "C.ThrowIfNull", EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_BeginInvoke() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public delegate void Delegate([EnsuresNotNull] string? s); +public class C +{ + void M(Delegate d, string? s) + { + s.ToString(); // warn + d.BeginInvoke(s, null, null); + s.ToString(); // warn 2 + } +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 9), + // (9,26): warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter. + // d.BeginInvoke(s, null, null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(9, 26), + // (9,32): warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter. + // d.BeginInvoke(s, null, null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(9, 32), + // (10,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // ok + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(10, 9) + ); + } + + [Fact] + public void EnsuresNotNull_BackEffect() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(string? s1, string? s2) + { + ThrowIfNull(s2 = s1, s1); + s2.ToString(); // warn + } + public static void ThrowIfNull(string? x1, [EnsuresNotNull] string? x2) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + // PROTOTYPE(NullableReferenceTypes): Should we be able to trace that s2 was assigned a non-null value? + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s2.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(8, 9) + ); + } + + [Fact] + public void EnsuresNotNull_ForwardEffect() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(string? s1, string? s2) + { + ThrowIfNull(s1, s2 = s1); + s2.ToString(); // ok + } + public static void ThrowIfNull([EnsuresNotNull] string? x1, string? x2) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + } + + [Fact] + public void EnsuresNotNull_ForwardEffect2() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(string? s1) + { + ThrowIfNull(s1, s1 = null); + s1.ToString(); // warn + } + public static void ThrowIfNull([EnsuresNotNull] string? x1, string? x2) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s1.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s1").WithLocation(8, 9) + ); + } + + [Fact] + public void EnsuresNotNull_ForwardEffect3() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(string? s1, string? s2) + { + ThrowIfNull(s1 = null, s2 = s1, s1 = """", s1); + s2.ToString(); // warn + } + public static void ThrowIfNull(string? x1, string? x2, string? x3, [EnsuresNotNull] string? x4) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s2.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2").WithLocation(8, 9) + ); + } + + [Fact] + public void EnsuresNotNull_TypeInference() + { + // PROTOTYPE(NullableReferenceTypes): This test raises the question of flowing information from annotations into the inferred type + // But the issue is currently masked by a broader problem with `out var` and nullability + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void M(string? s1) + { + ThrowIfNull(s1, out var s2); + s2/*T:string!*/.ToString(); + } + public static void ThrowIfNull([EnsuresNotNull] T x1, out T x2) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyTypes(); + c.VerifyDiagnostics( + // (7,21): warning CS8604: Possible null reference argument for parameter 'x1' in 'void C.ThrowIfNull(string x1, out string x2)'. + // ThrowIfNull(s1, out var s2); + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "s1").WithArguments("x1", "void C.ThrowIfNull(string x1, out string x2)").WithLocation(7, 21) + ); + } + + [Fact] + public void EnsuresNotNull_ConditionalMethodInReleaseMode() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + ThrowIfNull(42, s); + s.ToString(); // ok + } + [System.Diagnostics.Conditional(""DEBUG"")] + static void ThrowIfNull(int x, [EnsuresNotNull] string? s) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8, options: TestOptions.ReleaseDll); + + c.VerifyDiagnostics(); + } + + [Fact] + public void EnsuresNotNull_SecondArgumentDereferences() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +public class C +{ + void Main(string? s) + { + ThrowIfNull(s, s.ToString()); // warn + s.ToString(); // ok + } + public static void ThrowIfNull([EnsuresNotNull] string? s, string s2) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (7,24): warning CS8602: Possible dereference of a null reference. + // ThrowIfNull(s, s.ToString()); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(7, 24) + ); + + VerifyAnnotationsAndMetadata(c, "C.ThrowIfNull", EnsuresNotNull, None); + } + + [Fact] + public void EnsuresNotNull_SecondArgumentAssigns() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + ThrowIfNull(s, s = null); + s.ToString(); // warn + } + static void ThrowIfNull([EnsuresNotNull] string? s, string? s2) => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics( + // (8,9): warning CS8602: Possible dereference of a null reference. + // s.ToString(); // warn + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s").WithLocation(8, 9) + ); + } + + [Fact] + public void EnsuresNotNull_String_Contains() + { + CSharpCompilation c = CreateCompilation(@" +class C +{ + void Main(string? s) + { + ""hello"".Contains(s); + s.ToString(); // ok + } +} +", parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + + VerifyAnnotationsAndMetadata(c, "System.String.Contains", EnsuresNotNull); + } + + [Fact] + public void EnsuresNotNull_Indexer() + { + CSharpCompilation c = CreateCompilation(@" +using System.Runtime.CompilerServices; +class C +{ + void Main(string? s) + { + _ = this[42, s]; + s.ToString(); // ok + } + public int this[int x, [EnsuresNotNull] string? s] => throw null; +} +" + EnsuresNotNullAttributeDefinition, parseOptions: TestOptions.Regular8); + + c.VerifyDiagnostics(); + } + [Fact] public void ConditionalBranching_01() { diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index fe3fd861adcc8..3e40971c2cce3 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -258,6 +258,8 @@ static AttributeDescription() private static readonly byte[][] s_signaturesOfOutAttribute = { s_signature_HasThis_Void }; private static readonly byte[][] s_signaturesOfIsReadOnlyAttribute = { s_signature_HasThis_Void }; private static readonly byte[][] s_signaturesOfIsUnmanagedAttribute = { s_signature_HasThis_Void }; + private static readonly byte[][] s_signaturesOfNotNullWhenFalseAttribute = { s_signature_HasThis_Void }; + private static readonly byte[][] s_signaturesOfEnsuresNotNullAttribute = { s_signature_HasThis_Void }; private static readonly byte[][] s_signaturesOfFixedBufferAttribute = { s_signature_HasThis_Void_Type_Int32 }; private static readonly byte[][] s_signaturesOfSuppressUnmanagedCodeSecurityAttribute = { s_signature_HasThis_Void }; private static readonly byte[][] s_signaturesOfPrincipalPermissionAttribute = { s_signature_HasThis_Void_SecurityAction }; @@ -454,6 +456,8 @@ static AttributeDescription() internal static readonly AttributeDescription StructLayoutAttribute = new AttributeDescription("System.Runtime.InteropServices", "StructLayoutAttribute", s_signaturesOfStructLayoutAttribute); internal static readonly AttributeDescription FieldOffsetAttribute = new AttributeDescription("System.Runtime.InteropServices", "FieldOffsetAttribute", s_signaturesOfFieldOffsetAttribute); internal static readonly AttributeDescription FixedBufferAttribute = new AttributeDescription("System.Runtime.CompilerServices", "FixedBufferAttribute", s_signaturesOfFixedBufferAttribute); + internal static readonly AttributeDescription NotNullWhenFalseAttribute = new AttributeDescription("System.Runtime.CompilerServices", "NotNullWhenFalseAttribute", s_signaturesOfNotNullWhenFalseAttribute); + internal static readonly AttributeDescription EnsuresNotNullAttribute = new AttributeDescription("System.Runtime.CompilerServices", "EnsuresNotNullAttribute", s_signaturesOfEnsuresNotNullAttribute); internal static readonly AttributeDescription MarshalAsAttribute = new AttributeDescription("System.Runtime.InteropServices", "MarshalAsAttribute", s_signaturesOfMarshalAsAttribute); internal static readonly AttributeDescription InAttribute = new AttributeDescription("System.Runtime.InteropServices", "InAttribute", s_signaturesOfInAttribute); internal static readonly AttributeDescription OutAttribute = new AttributeDescription("System.Runtime.InteropServices", "OutAttribute", s_signaturesOfOutAttribute); diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/CommonParameterWellKnownAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/CommonParameterWellKnownAttributeData.cs index 74cc508d9fa43..020d56fa86c07 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/CommonParameterWellKnownAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/CommonParameterWellKnownAttributeData.cs @@ -109,5 +109,38 @@ public bool HasIUnknownConstantAttribute } } #endregion + + // PROTOTYPE(NullableReferenceTypes): Consider moving the attribute for nullability to C#-specific type + private bool _hasNotNullWhenFalseAttribute; + public bool HasNotNullWhenFalseAttribute + { + get + { + VerifySealed(expected: true); + return _hasNotNullWhenFalseAttribute; + } + set + { + VerifySealed(expected: false); + _hasNotNullWhenFalseAttribute = value; + SetDataStored(); + } + } + + private bool _hasEnsuresNotNullAttribute; + public bool HasEnsuresNotNullAttribute + { + get + { + VerifySealed(expected: true); + return _hasEnsuresNotNullAttribute; + } + set + { + VerifySealed(expected: false); + _hasEnsuresNotNullAttribute = value; + SetDataStored(); + } + } } } diff --git a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs index 046b8bee9ab17..0f81d94e7fd63 100644 --- a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs +++ b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs @@ -306,7 +306,11 @@ public static CSharpCompilation CreateCompilationWithIL( string ilSource, IEnumerable references = null, CSharpCompilationOptions options = null, - bool appendDefaultHeader = true) => CreateCompilationWithILAndMscorlib40(source, ilSource, TargetFramework.Standard, references, options, appendDefaultHeader); + CSharpParseOptions parseOptions = null, + bool appendDefaultHeader = true) + { + return CreateCompilationWithILAndMscorlib40(source, ilSource, TargetFramework.Standard, references, options, parseOptions, appendDefaultHeader); + } public static CSharpCompilation CreateCompilationWithILAndMscorlib40( CSharpTestSource source, @@ -314,11 +318,12 @@ public static CSharpCompilation CreateCompilationWithILAndMscorlib40( TargetFramework targetFramework = TargetFramework.Mscorlib40, IEnumerable references = null, CSharpCompilationOptions options = null, + CSharpParseOptions parseOptions = null, bool appendDefaultHeader = true) { MetadataReference ilReference = CompileIL(ilSource, appendDefaultHeader); var allReferences = TargetFrameworkUtil.GetReferences(targetFramework, references).Add(ilReference); - return CreateEmptyCompilation(source, allReferences, options); + return CreateEmptyCompilation(source, allReferences, options, parseOptions); } public static CSharpCompilation CreateCompilationWithMscorlib40(