diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs index 71e10dcfc1e58..44d34bbc55ff1 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs @@ -3306,7 +3306,7 @@ private BindValueKind GetRequiredReturnValueKind(RefKind refKind) return requiredValueKind; } - public virtual BoundNode BindMethodBody(CSharpSyntaxNode syntax, BindingDiagnosticBag diagnostics) + public virtual BoundNode BindMethodBody(CSharpSyntaxNode syntax, BindingDiagnosticBag diagnostics, bool includesFieldInitializers = false) { switch (syntax) { @@ -3316,7 +3316,7 @@ public virtual BoundNode BindMethodBody(CSharpSyntaxNode syntax, BindingDiagnost case BaseMethodDeclarationSyntax method: if (method.Kind() == SyntaxKind.ConstructorDeclaration) { - return BindConstructorBody((ConstructorDeclarationSyntax)method, diagnostics); + return BindConstructorBody((ConstructorDeclarationSyntax)method, diagnostics, includesFieldInitializers); } return BindMethodBody(method, method.Body, method.ExpressionBody, diagnostics); @@ -3388,9 +3388,10 @@ internal virtual BoundExpressionStatement BindConstructorInitializer(PrimaryCons return constructorInitializer; } - private BoundNode BindConstructorBody(ConstructorDeclarationSyntax constructor, BindingDiagnosticBag diagnostics) + private BoundNode BindConstructorBody(ConstructorDeclarationSyntax constructor, BindingDiagnosticBag diagnostics, bool includesFieldInitializers) { - if (constructor.Initializer == null && constructor.Body == null && constructor.ExpressionBody == null) + ConstructorInitializerSyntax initializer = constructor.Initializer; + if (initializer == null && constructor.Body == null && constructor.ExpressionBody == null) { return null; } @@ -3398,7 +3399,8 @@ private BoundNode BindConstructorBody(ConstructorDeclarationSyntax constructor, Binder bodyBinder = this.GetBinder(constructor); Debug.Assert(bodyBinder != null); - if (constructor.Initializer?.IsKind(SyntaxKind.ThisConstructorInitializer) != true && + bool thisInitializer = initializer?.IsKind(SyntaxKind.ThisConstructorInitializer) == true; + if (!thisInitializer && ContainingType.GetMembersUnordered().OfType().Any()) { var constructorSymbol = (MethodSymbol)this.ContainingMember(); @@ -3406,14 +3408,21 @@ private BoundNode BindConstructorBody(ConstructorDeclarationSyntax constructor, !SynthesizedRecordCopyCtor.IsCopyConstructor(constructorSymbol)) { // Note: we check the constructor initializer of copy constructors elsewhere - Error(diagnostics, ErrorCode.ERR_UnexpectedOrMissingConstructorInitializerInRecord, constructor.Initializer?.ThisOrBaseKeyword ?? constructor.Identifier); + Error(diagnostics, ErrorCode.ERR_UnexpectedOrMissingConstructorInitializerInRecord, initializer?.ThisOrBaseKeyword ?? constructor.Identifier); } } + // The `: this()` initializer is ignored when it is a default value type constructor + // and we need to include field initializers into the constructor. + bool skipInitializer = includesFieldInitializers + && thisInitializer + && ContainingType.IsDefaultValueTypeConstructor(initializer); + // Using BindStatement to bind block to make sure we are reusing results of partial binding in SemanticModel return new BoundConstructorMethodBody(constructor, bodyBinder.GetDeclaredLocalsForScope(constructor), - constructor.Initializer == null ? null : bodyBinder.BindConstructorInitializer(constructor.Initializer, diagnostics), + skipInitializer ? new BoundNoOpStatement(constructor, NoOpStatementFlavor.Default) + : initializer == null ? null : bodyBinder.BindConstructorInitializer(initializer, diagnostics), constructor.Body == null ? null : (BoundBlock)bodyBinder.BindStatement(constructor.Body, diagnostics), constructor.ExpressionBody == null ? null : diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index cef075a396654..9b6546a0ecaa2 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -2174,7 +2174,7 @@ - + diff --git a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs index a106610962e31..0de4972cd45df 100644 --- a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs @@ -2456,7 +2456,7 @@ private BoundNode TryGetBoundNodeFromMap(CSharpSyntaxNode node) return null; } - public override BoundNode BindMethodBody(CSharpSyntaxNode node, BindingDiagnosticBag diagnostics) + public override BoundNode BindMethodBody(CSharpSyntaxNode node, BindingDiagnosticBag diagnostics, bool includeInitializersInBody) { BoundNode boundNode = TryGetBoundNodeFromMap(node); @@ -2465,7 +2465,7 @@ public override BoundNode BindMethodBody(CSharpSyntaxNode node, BindingDiagnosti return boundNode; } - boundNode = base.BindMethodBody(node, diagnostics); + boundNode = base.BindMethodBody(node, diagnostics, includeInitializersInBody); return boundNode; } diff --git a/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs index 7d6df06e584b2..7b425fcee19c8 100644 --- a/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/MethodBodySemanticModel.cs @@ -23,14 +23,14 @@ internal readonly struct InitialState { internal readonly CSharpSyntaxNode Syntax; internal readonly BoundNode? Body; - internal readonly ExecutableCodeBinder? Binder; + internal readonly Binder? Binder; internal readonly NullableWalker.SnapshotManager? SnapshotManager; internal readonly ImmutableDictionary? RemappedSymbols; internal InitialState( CSharpSyntaxNode syntax, BoundNode? bodyOpt = null, - ExecutableCodeBinder? binder = null, + Binder? binder = null, NullableWalker.SnapshotManager? snapshotManager = null, ImmutableDictionary? remappedSymbols = null) { diff --git a/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs b/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs index 228d185fb7ba3..03b62dfb864e3 100644 --- a/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs +++ b/src/Compilers/CSharp/Portable/Compiler/MethodCompiler.cs @@ -1036,7 +1036,11 @@ private void CompileMethod( getFinalNullableState: true, out processedInitializers.AfterInitializersState); } - body = BindMethodBody(methodSymbol, compilationState, diagsForCurrentMethod, processedInitializers.AfterInitializersState, out importChain, out originalBodyNested, out forSemanticModel); + + body = BindMethodBody(methodSymbol, compilationState, diagsForCurrentMethod, processedInitializers.AfterInitializersState, + includeInitializersInBody && !processedInitializers.BoundInitializers.IsEmpty, + out importChain, out originalBodyNested, out forSemanticModel); + if (diagsForCurrentMethod.HasAnyErrors() && body != null) { body = (BoundBlock)body.WithHasErrors(); @@ -1673,12 +1677,13 @@ private static void GetStateMachineSlotDebugInfo( // NOTE: can return null if the method has no body. internal static BoundBlock BindMethodBody(MethodSymbol method, TypeCompilationState compilationState, BindingDiagnosticBag diagnostics) { - return BindMethodBody(method, compilationState, diagnostics, nullableInitialState: null, out _, out _, out _); + return BindMethodBody(method, compilationState, diagnostics, nullableInitialState: null, includesFieldInitializers: false, out _, out _, out _); } // NOTE: can return null if the method has no body. private static BoundBlock BindMethodBody(MethodSymbol method, TypeCompilationState compilationState, BindingDiagnosticBag diagnostics, NullableWalker.VariableState nullableInitialState, + bool includesFieldInitializers, out ImportChain importChain, out bool originalBodyNested, out MethodBodySemanticModel.InitialState forSemanticModel) { @@ -1713,12 +1718,11 @@ syntaxNode is ConstructorDeclarationSyntax constructorSyntax && return null; } - ExecutableCodeBinder bodyBinder = sourceMethod.TryGetBodyBinder(); + Binder bodyBinder = sourceMethod.TryGetBodyBinder(); if (bodyBinder != null) { importChain = bodyBinder.ImportChain; - - BoundNode methodBody = bodyBinder.BindMethodBody(syntaxNode, diagnostics); + BoundNode methodBody = bodyBinder.BindMethodBody(syntaxNode, diagnostics, includesFieldInitializers); BoundNode methodBodyForSemanticModel = methodBody; NullableWalker.SnapshotManager snapshotManager = null; ImmutableDictionary remappedSymbols = null; @@ -1760,9 +1764,15 @@ syntaxNode is ConstructorDeclarationSyntax constructorSyntax && var constructor = (BoundConstructorMethodBody)methodBody; body = constructor.BlockBody ?? constructor.ExpressionBody; - if (constructor.Initializer != null) + if (constructor.Initializer is BoundNoOpStatement) + { + // We have field initializers and `: this()` is a default value type constructor. + Debug.Assert(body is not null); + return body; + } + else if (constructor.Initializer is BoundExpressionStatement expressionStatement) { - ReportCtorInitializerCycles(method, constructor.Initializer.Expression, compilationState, diagnostics); + ReportCtorInitializerCycles(method, expressionStatement.Expression, compilationState, diagnostics); if (body == null) { @@ -1778,6 +1788,7 @@ syntaxNode is ConstructorDeclarationSyntax constructorSyntax && } else { + Debug.Assert(constructor.Initializer is null); Debug.Assert(constructor.Locals.IsEmpty); } break; diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs index 69069edb84c27..df4d53bdf8607 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs @@ -442,9 +442,6 @@ protected ImmutableArray Analyze(ref bool badRegion, Optional 0) + if (parameter.RefKind != RefKind.Out) { - var state = GetParameterState(parameterType, parameter.FlowAnalysisAnnotations).State; - this.State[slot] = state; - if (EmptyStructTypeCache.IsTrackableStructType(parameterType.Type)) + int slot = GetOrCreateSlot(parameter); + + Debug.Assert(!IsConditionalState); + if (slot > 0) { - InheritNullableStateOfTrackableStruct( - parameterType.Type, - slot, - valueSlot: -1, - isDefaultValue: parameter.ExplicitDefaultConstantValue?.IsNull == true); + var state = GetParameterState(parameterType, parameter.FlowAnalysisAnnotations).State; + this.State[slot] = state; + if (EmptyStructTypeCache.IsTrackableStructType(parameterType.Type)) + { + InheritNullableStateOfTrackableStruct( + parameterType.Type, + slot, + valueSlot: -1, + isDefaultValue: parameter.ExplicitDefaultConstantValue?.IsNull == true); + } } } } @@ -3071,7 +3075,6 @@ private void VisitObjectOrDynamicObjectCreation( { var boundObjectCreationExpression = node as BoundObjectCreationExpression; var constructor = boundObjectCreationExpression?.Constructor; - // PROTOTYPE: Test with structs with parameterless constructors. bool isDefaultValueTypeConstructor = constructor?.IsDefaultValueTypeConstructor(requireZeroInit: true) == true; if (EmptyStructTypeCache.IsTrackableStructType(type)) diff --git a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs index b6df30a1a81c0..cbcab61dd1efe 100644 --- a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs @@ -7985,7 +7985,7 @@ public BoundNonConstructorMethodBody Update(BoundBlock? blockBody, BoundBlock? e internal sealed partial class BoundConstructorMethodBody : BoundMethodBodyBase { - public BoundConstructorMethodBody(SyntaxNode syntax, ImmutableArray locals, BoundExpressionStatement? initializer, BoundBlock? blockBody, BoundBlock? expressionBody, bool hasErrors = false) + public BoundConstructorMethodBody(SyntaxNode syntax, ImmutableArray locals, BoundStatement? initializer, BoundBlock? blockBody, BoundBlock? expressionBody, bool hasErrors = false) : base(BoundKind.ConstructorMethodBody, syntax, blockBody, expressionBody, hasErrors || initializer.HasErrors() || blockBody.HasErrors() || expressionBody.HasErrors()) { @@ -7998,11 +7998,11 @@ public BoundConstructorMethodBody(SyntaxNode syntax, ImmutableArray public ImmutableArray Locals { get; } - public BoundExpressionStatement? Initializer { get; } + public BoundStatement? Initializer { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitConstructorMethodBody(this); - public BoundConstructorMethodBody Update(ImmutableArray locals, BoundExpressionStatement? initializer, BoundBlock? blockBody, BoundBlock? expressionBody) + public BoundConstructorMethodBody Update(ImmutableArray locals, BoundStatement? initializer, BoundBlock? blockBody, BoundBlock? expressionBody) { if (locals != this.Locals || initializer != this.Initializer || blockBody != this.BlockBody || expressionBody != this.ExpressionBody) { @@ -11055,7 +11055,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor } public override BoundNode? VisitConstructorMethodBody(BoundConstructorMethodBody node) { - BoundExpressionStatement? initializer = (BoundExpressionStatement?)this.Visit(node.Initializer); + BoundStatement? initializer = (BoundStatement?)this.Visit(node.Initializer); BoundBlock? blockBody = (BoundBlock?)this.Visit(node.BlockBody); BoundBlock? expressionBody = (BoundBlock?)this.Visit(node.ExpressionBody); return node.Update(node.Locals, initializer, blockBody, expressionBody); @@ -13426,7 +13426,7 @@ public NullabilityRewriter(ImmutableDictionary locals = GetUpdatedArray(node, node.Locals); - BoundExpressionStatement? initializer = (BoundExpressionStatement?)this.Visit(node.Initializer); + BoundStatement? initializer = (BoundStatement?)this.Visit(node.Initializer); BoundBlock? blockBody = (BoundBlock?)this.Visit(node.BlockBody); BoundBlock? expressionBody = (BoundBlock?)this.Visit(node.ExpressionBody); return node.Update(locals, initializer, blockBody, expressionBody); diff --git a/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs b/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs index a7e5623820c57..e08bee6499ab7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs @@ -392,22 +392,23 @@ internal static bool HasThisConstructorInitializer(this MethodSymbol method, out internal static bool IncludeFieldInitializersInBody(this MethodSymbol methodSymbol) { + // A struct constructor that calls ": this()" will need to include field initializers if the + // parameterless constructor is a synthesized default constructor that is not emitted. + return methodSymbol.IsConstructor() - && !(methodSymbol.HasThisConstructorInitializer(out var initializerSyntax) && !isDefaultValueTypeConstructor(methodSymbol.ContainingType, initializerSyntax)) + && !(methodSymbol.HasThisConstructorInitializer(out var initializerSyntax) && !methodSymbol.ContainingType.IsDefaultValueTypeConstructor(initializerSyntax)) && !(methodSymbol is SynthesizedRecordCopyCtor) // A record copy constructor is special, regular initializers are not supposed to be executed by it. && !Binder.IsUserDefinedRecordCopyConstructor(methodSymbol); + } - // A struct constructor that calls ": this()" will need to include field initializers if the - // parameterless constructor is a synthesized default constructor that is not emitted. - static bool isDefaultValueTypeConstructor(NamedTypeSymbol containingType, ConstructorInitializerSyntax initializerSyntax) + internal static bool IsDefaultValueTypeConstructor(this NamedTypeSymbol type, ConstructorInitializerSyntax initializerSyntax) + { + if (initializerSyntax.ArgumentList.Arguments.Count > 0) { - if (initializerSyntax.ArgumentList.Arguments.Count > 0) - { - return false; - } - var constructor = containingType.InstanceConstructors.SingleOrDefault(m => m.ParameterCount == 0); - return constructor?.IsDefaultValueTypeConstructor(requireZeroInit: true) == true; + return false; } + var constructor = type.InstanceConstructors.SingleOrDefault(m => m.ParameterCount == 0); + return constructor?.IsDefaultValueTypeConstructor(requireZeroInit: true) == true; } /// diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableContextTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableContextTests.cs index a68dccf4aca19..5c80ffac51c0b 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableContextTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableContextTests.cs @@ -961,11 +961,11 @@ struct S object F1; S(object obj) : this() { } }"; - // PROTOTYPE: NullableWalker.Scan() overwrites initial field state when calling EnterParameter(methodThisParameter). - verify(source, expectedAnalyzedKeys: new[] { ".ctor" }/*, + verify(source, expectedAnalyzedKeys: new[] { ".ctor" }, // (6,5): warning CS8618: Non-nullable field 'F1' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. // S(object obj) : this() { } - Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S").WithArguments("field", "F1").WithLocation(6, 5)*/); + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S").WithArguments("field", "F1").WithLocation(6, 5) + ); source = @"#pragma warning disable 169 diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs index 5e4c044ae1c62..1b421630ec390 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs @@ -58057,6 +58057,49 @@ static void Main() comp.VerifyDiagnostics(); } + [Fact] + public void ObjectInitializer_ParameterlessStructConstructor() + { + var src = @" +#nullable enable + +var s1 = new S1() { }; +s1.field.ToString(); + +var s2 = new S2() { }; +s2.field.ToString(); // 1 + +var s3 = new S3() { }; +s3.field.ToString(); + +public struct S1 +{ + public string field; + + public S1() + { + field = string.Empty; + } +} + +public struct S2 +{ + public string field; +} + +public struct S3 +{ + public string field = string.Empty; +} +"; + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (8,1): warning CS8602: Dereference of a possibly null reference. + // s2.field.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s2.field").WithLocation(8, 1) + ); + } + [Fact] public void IdentityConversion_ObjectElementInitializerArgumentsOrder() { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index 7cd09257b8b16..d3160c910d8ae 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -1048,6 +1048,96 @@ internal S2() { } Diagnostic(ErrorCode.ERR_NonPublicParameterlessStructConstructor, "S2").WithLocation(7, 14)); } + [Fact] + public void TypeDeclaration_ParameterlessConstructor_OtherConstructors() + { + var src = @" +record struct S1 +{ + public S1() { } + S1(object o) { } // ok because no record parameter list +} +record struct S2 +{ + S2(object o) { } +} +record struct S3() +{ + S3(object o) { } // 1 +} +record struct S4() +{ + S4(object o) : this() { } +} +record struct S5(object o) +{ + public S5() { } // 2 +} +record struct S6(object o) +{ + public S6() : this(null) { } +} +"; + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (13,5): error CS8862: A constructor declared in a record with parameter list must have 'this' constructor initializer. + // S3(object o) { } // 1 + Diagnostic(ErrorCode.ERR_UnexpectedOrMissingConstructorInitializerInRecord, "S3").WithLocation(13, 5), + // (21,12): error CS8862: A constructor declared in a record with parameter list must have 'this' constructor initializer. + // public S5() { } // 2 + Diagnostic(ErrorCode.ERR_UnexpectedOrMissingConstructorInitializerInRecord, "S5").WithLocation(21, 12) + ); + } + + [Fact] + public void TypeDeclaration_ParameterlessConstructor_Initializers() + { + var src = @" +var s1 = new S1(); +var s2 = new S2(null); +var s2b = new S2(); +var s3 = new S3(); +var s4 = new S4(new object()); +var s5 = new S5(); +var s6 = new S6(""s6.other""); + +System.Console.Write((s1.field, s2.field, s2b.field is null, s3.field, s4.field, s5.field, s6.field, s6.other)); + +record struct S1 +{ + public string field = ""s1""; + public S1() { } +} +record struct S2 +{ + public string field = ""s2""; + public S2(object o) { } +} +record struct S3() +{ + public string field = ""s3""; +} +record struct S4 +{ + public string field = ""s4""; + public S4(object o) : this() { } +} +record struct S5() +{ + public string field = ""s5""; + public S5(object o) : this() { } +} +record struct S6(string other) +{ + public string field = ""s6.field""; + public S6() : this(""ignored"") { } +} +"; + var comp = CreateCompilation(src); + comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "(s1, s2, True, s3, s4, s5, s6.field, s6.other)"); + } + [Fact] public void TypeDeclaration_InstanceInitializers() { @@ -1075,9 +1165,6 @@ public record struct S comp.VerifyDiagnostics(); } - // PROTOTYPE: Verify initializers are executed from synthesized and explicit parameterless constructors. - // PROTOTYPE: Verify explicit parameterless constructor calls 'this(...)' for primary constructor. - [Fact] public void TypeDeclaration_NoDestructor() { @@ -2986,12 +3073,15 @@ public record struct X(int a) public void ParameterlessConstructor() { var src = @" +System.Console.Write(new C().Property); + record struct C() { - int Property { get; set; } = 42; + public int Property { get; set; } = 42; }"; var comp = CreateCompilation(src); comp.VerifyDiagnostics(); + CompileAndVerify(comp, expectedOutput: "42"); } [Fact] @@ -3447,7 +3537,18 @@ static void Main() // case C(): Diagnostic(ErrorCode.ERR_MissingDeconstruct, "()").WithArguments("C", "0").WithLocation(8, 19)); - Assert.Null(comp.GetMember("C.Deconstruct")); + AssertEx.Equal(new[] { + "C..ctor()", + "void C.M(C c)", + "void C.Main()", + "System.String C.ToString()", + "System.Boolean C.PrintMembers(System.Text.StringBuilder builder)", + "System.Boolean C.op_Inequality(C left, C right)", + "System.Boolean C.op_Equality(C left, C right)", + "System.Int32 C.GetHashCode()", + "System.Boolean C.Equals(System.Object obj)", + "System.Boolean C.Equals(C other)" }, + comp.GetMember("C").GetMembers().ToTestDisplayStrings()); } [Fact] diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs index 16ba48f44c369..27977bcea55be 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/StructConstructorTests.cs @@ -478,12 +478,10 @@ static void Main() } }"; - // PROTOTYPE: S0(object) should set Value after initobj, not before, - // and should report: new S0(null).Value: 1. - var verifier = CompileAndVerify(source, expectedOutput: -@"new S0().Value: 0 + var verifier = CompileAndVerify(source, expectedOutput: @" +new S0().Value: 0 One() -new S0(null).Value: 0 +new S0(null).Value: 1 One() new S1().Value: 1 One() @@ -495,14 +493,12 @@ static void Main() verifier.VerifyMissing("S0..ctor()"); verifier.VerifyIL("S0..ctor(object)", @"{ - // Code size 19 (0x13) + // Code size 12 (0xc) .maxstack 2 IL_0000: ldarg.0 IL_0001: call ""int Program.One()"" IL_0006: stfld ""int S0.Value"" - IL_000b: ldarg.0 - IL_000c: initobj ""S0"" - IL_0012: ret + IL_000b: ret }"); verifier.VerifyIL("S1..ctor()", @"{ @@ -598,20 +594,17 @@ static void Main() "); verifier.VerifyMissing("S0..ctor()"); - // PROTOTYPE: S0(object) should set Value twice after initobj, not once before, once after. verifier.VerifyIL("S0..ctor(object)", @"{ - // Code size 30 (0x1e) + // Code size 23 (0x17) .maxstack 2 IL_0000: ldarg.0 IL_0001: call ""int Program.One()"" IL_0006: stfld ""int S0.Value"" IL_000b: ldarg.0 - IL_000c: initobj ""S0"" - IL_0012: ldarg.0 - IL_0013: call ""int Program.Two()"" - IL_0018: stfld ""int S0.Value"" - IL_001d: ret + IL_000c: call ""int Program.Two()"" + IL_0011: stfld ""int S0.Value"" + IL_0016: ret }"); verifier.VerifyIL("S1..ctor()", @"{ @@ -718,20 +711,17 @@ static void Main() "); verifier.VerifyMissing("S0..ctor()"); - // PROTOTYPE: S0(object) should set Value twice after initobj, not once before, once after. verifier.VerifyIL("S0..ctor(object)", @"{ - // Code size 26 (0x1a) + // Code size 19 (0x13) .maxstack 2 IL_0000: ldarg.0 IL_0001: ldc.i4.0 IL_0002: stfld ""int S0.Value"" IL_0007: ldarg.0 - IL_0008: initobj ""S0"" - IL_000e: ldarg.0 - IL_000f: call ""int Program.Two()"" - IL_0014: stfld ""int S0.Value"" - IL_0019: ret + IL_0008: call ""int Program.Two()"" + IL_000d: stfld ""int S0.Value"" + IL_0012: ret }"); verifier.VerifyIL("S1..ctor()", @"{ @@ -1581,9 +1571,15 @@ struct S3 }"; var comp = CreateCompilation(source); comp.VerifyDiagnostics( + // (10,12): warning CS8618: Non-nullable field 'F1' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. + // public S1() { } + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S1").WithArguments("field", "F1").WithLocation(10, 12), // (10,12): error CS0171: Field 'S1.F1' must be fully assigned before control is returned to the caller // public S1() { } Diagnostic(ErrorCode.ERR_UnassignedThis, "S1").WithArguments("S1.F1").WithLocation(10, 12), + // (16,5): warning CS8618: Non-nullable field 'F2' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. + // S2(object? obj) { } + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S2").WithArguments("field", "F2").WithLocation(16, 5), // (16,5): error CS0171: Field 'S2.F2' must be fully assigned before control is returned to the caller // S2(object? obj) { } Diagnostic(ErrorCode.ERR_UnassignedThis, "S2").WithArguments("S2.F2").WithLocation(16, 5), diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/UninitializedNonNullableFieldTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/UninitializedNonNullableFieldTests.cs index f12534e5449fa..44a2528b87e2b 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/UninitializedNonNullableFieldTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/UninitializedNonNullableFieldTests.cs @@ -875,47 +875,188 @@ public S1(string s1, string s2) : this(s1) public void StructConstructorInitializer_UninitializedField() { var source = @" +#nullable enable + +public struct S1 +{ + public string field; + + public S1(object obj) : this() + { + field.ToString(); // 1 + } + + public S1(object obj1, object obj2) : this() // 2 + { + } + + public S1(string s1, string s2) : this(s1) + { + field.ToString(); + } +} +"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics( + // (10,9): warning CS8602: Dereference of a possibly null reference. + // field.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "field").WithLocation(10, 9), + // (13,12): warning CS8618: Non-nullable field 'field' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. + // public S1(object obj1, object obj2) : this() // 2 + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S1").WithArguments("field", "field").WithLocation(13, 12)); + } + + [Fact, WorkItem(48574, "https://github.com/dotnet/roslyn/issues/48574")] + public void StructConstructorInitializer_InitializedFieldViaParameterlessConstructor() + { + var source = @" +#nullable enable + +new S1(new object()); + struct S1 { - public string field; // 0 - public S1(string s) // 1 + public string field; + + public S1() { - field.ToString(); // 2 + field = ""ok ""; } public S1(object obj) : this() { - field.ToString(); // 3 + System.Console.Write(field); } +} +"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics(); + var verifier = CompileAndVerify(comp, expectedOutput: "ok"); + + verifier.VerifyIL("S1..ctor(object)", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: call ""S1..ctor()"" + IL_0006: ldarg.0 + IL_0007: ldfld ""string S1.field"" + IL_000c: call ""void System.Console.Write(string)"" + IL_0011: ret +} +"); + } - public S1(object obj1, object obj2) : this() // 4 + [Fact, WorkItem(48574, "https://github.com/dotnet/roslyn/issues/48574")] + public void StructConstructorInitializer_UninitializedFieldWithParameterlessConstructor() + { + var source = @" +#nullable enable + +struct S1 +{ + public string field; + + public S1() { + field = ""ok ""; } - public S1(string s1, string s2) : this(s1) + public S1(string s) // 1, 2 { - field.ToString(); + System.Console.Write(field); // 3 } } "; - // PROTOTYPE: NullableWalker.Scan() overwrites initial field state when calling EnterParameter(methodThisParameter). - var comp = CreateCompilation(source, options: WithNullableEnable()); + var comp = CreateCompilation(source); comp.VerifyDiagnostics( - // (4,19): warning CS0649: Field 'S1.field' is never assigned to, and will always have its default value null - // public string field; // 0 - Diagnostic(ErrorCode.WRN_UnassignedInternalField, "field").WithArguments("S1.field", "null").WithLocation(4, 19), - // (5,12): error CS0171: Field 'S1.field' must be fully assigned before control is returned to the caller - // public S1(string s) // 1 - Diagnostic(ErrorCode.ERR_UnassignedThis, "S1").WithArguments("S1.field").WithLocation(5, 12), - // (7,9): error CS0170: Use of possibly unassigned field 'field' - // field.ToString(); // 2 - Diagnostic(ErrorCode.ERR_UseDefViolationField, "field").WithArguments("field").WithLocation(7, 9)/*, - // (12,9): warning CS8602: Dereference of a possibly null reference. - // field.ToString(); // 3 - Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "field").WithLocation(12, 9), - // (15,12): warning CS8618: Non-nullable field 'field' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. - // public S1(object obj1, object obj2) : this() // 4 - Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S1").WithArguments("field", "field").WithLocation(15, 12)*/); + // (13,12): warning CS8618: Non-nullable field 'field' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. + // public S1(string s) // 1, 2 + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S1").WithArguments("field", "field").WithLocation(13, 12), + // (13,12): error CS0171: Field 'S1.field' must be fully assigned before control is returned to the caller + // public S1(string s) // 1, 2 + Diagnostic(ErrorCode.ERR_UnassignedThis, "S1").WithArguments("S1.field").WithLocation(13, 12), + // (15,30): error CS0170: Use of possibly unassigned field 'field' + // System.Console.Write(field); // 3 + Diagnostic(ErrorCode.ERR_UseDefViolationField, "field").WithArguments("field").WithLocation(15, 30) + ); + } + + [Fact, WorkItem(48574, "https://github.com/dotnet/roslyn/issues/48574")] + public void StructConstructorInitializer_InitializedFieldViaInitializer() + { + var source = @" +#nullable enable + +new S1(); +new S1(new object()); +new S1(new object(), new object()); + +struct S1 +{ + public string field = ""ok ""; + + public S1() + { + System.Console.Write(field); + } + + public S1(object s) + { + System.Console.Write(field); + } + + public S1(object obj, object obj2) : this() + { + System.Console.Write(field); + } +} +"; + var comp = CreateCompilation(source); + comp.VerifyDiagnostics(); + + var verifier = CompileAndVerify(comp, expectedOutput: "ok ok ok ok"); + + verifier.VerifyIL("S1..ctor()", @" +{ + // Code size 23 (0x17) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldstr ""ok "" + IL_0006: stfld ""string S1.field"" + IL_000b: ldarg.0 + IL_000c: ldfld ""string S1.field"" + IL_0011: call ""void System.Console.Write(string)"" + IL_0016: ret +} +"); + + verifier.VerifyIL("S1..ctor(object)", @" +{ + // Code size 23 (0x17) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldstr ""ok "" + IL_0006: stfld ""string S1.field"" + IL_000b: ldarg.0 + IL_000c: ldfld ""string S1.field"" + IL_0011: call ""void System.Console.Write(string)"" + IL_0016: ret +} +"); + + verifier.VerifyIL("S1..ctor(object, object)", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: call ""S1..ctor()"" + IL_0006: ldarg.0 + IL_0007: ldfld ""string S1.field"" + IL_000c: call ""void System.Console.Write(string)"" + IL_0011: ret +} +"); } [Fact, WorkItem(43215, "https://github.com/dotnet/roslyn/issues/43215")] @@ -1530,6 +1671,9 @@ internal S(string s) // (6,14): warning CS8618: Non-nullable property 'P' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. // internal S(string s) Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S").WithArguments("property", "P").WithLocation(6, 14), + // (6,14): warning CS8618: Non-nullable field 'F' must contain a non-null value when exiting constructor. Consider declaring the field as nullable. + // internal S(string s) + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "S").WithArguments("field", "F").WithLocation(6, 14), // (6,14): error CS0843: Auto-implemented property 'S.P' must be fully assigned before control is returned to the caller. // internal S(string s) Diagnostic(ErrorCode.ERR_UnassignedThisAutoProperty, "S").WithArguments("S.P").WithLocation(6, 14),