From 595d31204fa1200e1142c4cb525db4ead3d2bcef Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Mon, 29 Mar 2021 17:45:56 -0700 Subject: [PATCH 1/8] Record-structs: Address some PROTOTYPE markers --- .../CSharpDeclarationComputer.cs | 16 +- .../Compilation/SyntaxTreeSemanticModel.cs | 53 +- .../Portable/Declarations/DeclarationKind.cs | 1 - .../Symbols/Metadata/PE/PENamedTypeSymbol.cs | 3 +- .../Source/SourceMemberContainerSymbol.cs | 5 +- .../CSharp/Portable/Symbols/TypeSymbol.cs | 1 - .../CSharp/Portable/Syntax/SyntaxKindFacts.cs | 1 - .../Portable/Syntax/TypeDeclarationSyntax.cs | 1 - .../Semantic/Semantics/RecordStructTests.cs | 1234 ++++++++++++++--- .../Test/Semantic/Semantics/RecordTests.cs | 36 + .../Test/Syntax/Parsing/RecordParsing.cs | 1 - 11 files changed, 1170 insertions(+), 182 deletions(-) diff --git a/src/Compilers/CSharp/CSharpAnalyzerDriver/CSharpDeclarationComputer.cs b/src/Compilers/CSharp/CSharpAnalyzerDriver/CSharpDeclarationComputer.cs index daa9ceeec40af..c22352438cb70 100644 --- a/src/Compilers/CSharp/CSharpAnalyzerDriver/CSharpDeclarationComputer.cs +++ b/src/Compilers/CSharp/CSharpAnalyzerDriver/CSharpDeclarationComputer.cs @@ -97,8 +97,6 @@ private static void ComputeDeclarations( return; } - - // PROTOTYPE(record-structs): update for record structs case SyntaxKind.RecordDeclaration: { if (associatedSymbol is IMethodSymbol ctor) @@ -117,6 +115,20 @@ private static void ComputeDeclarations( return; } + goto case SyntaxKind.ClassDeclaration; + } + case SyntaxKind.RecordStructDeclaration: + { + if (associatedSymbol is IMethodSymbol ctor) + { + var recordDeclaration = (RecordStructDeclarationSyntax)node; + Debug.Assert(ctor.MethodKind == MethodKind.Constructor && recordDeclaration.ParameterList is object); + + var codeBlocks = GetParameterListInitializersAndAttributes(recordDeclaration.ParameterList); + builder.Add(GetDeclarationInfo(node, associatedSymbol, codeBlocks)); + return; + } + goto case SyntaxKind.ClassDeclaration; } case SyntaxKind.ClassDeclaration: diff --git a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs index f56b5868bf707..bcc126d547985 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs @@ -181,7 +181,6 @@ internal override IOperation GetOperationWorker(CSharpSyntaxNode node, Cancellat case AccessorDeclarationSyntax accessor: model = (accessor.Body != null || accessor.ExpressionBody != null) ? GetOrAddModel(node) : null; break; - // PROTOTYPE(record-structs): update for record structs case RecordDeclarationSyntax { ParameterList: { }, PrimaryConstructorBaseType: { } } recordDeclaration when TryGetSynthesizedRecordConstructor(recordDeclaration) is SynthesizedRecordConstructor: model = GetOrAddModel(recordDeclaration); break; @@ -805,7 +804,6 @@ private MemberSemanticModel GetMemberModel(int position) !LookupPosition.IsInConstructorParameterScope(position, constructorDecl) && !LookupPosition.IsInParameterList(position, constructorDecl); break; - // PROTOTYPE(record-structs): update for record structs case SyntaxKind.RecordDeclaration: { var recordDecl = (RecordDeclarationSyntax)memberDecl; @@ -875,7 +873,6 @@ internal override MemberSemanticModel GetMemberModel(SyntaxNode node) GetOrAddModel(constructorDecl) : null; } - // PROTOTYPE(record-structs): update for record structs case SyntaxKind.RecordDeclaration: { var recordDecl = (RecordDeclarationSyntax)memberDecl; @@ -1094,17 +1091,10 @@ private MemberSemanticModel CreateMemberModel(CSharpSyntaxNode node) return createMethodBodySemanticModel(memberDecl, symbol); } - // PROTOTYPE(record-structs): update for record structs case SyntaxKind.RecordDeclaration: { SynthesizedRecordConstructor symbol = TryGetSynthesizedRecordConstructor((RecordDeclarationSyntax)node); - - if (symbol is null) - { - return null; - } - - return createMethodBodySemanticModel(node, symbol); + return symbol is null ? null : createMethodBodySemanticModel(node, symbol); } case SyntaxKind.GetAccessorDeclaration: @@ -1259,8 +1249,9 @@ MemberSemanticModel createMethodBodySemanticModel(CSharpSyntaxNode memberDecl, S } } - private SynthesizedRecordConstructor TryGetSynthesizedRecordConstructor(RecordDeclarationSyntax node) + private SynthesizedRecordConstructor TryGetSynthesizedRecordConstructor(TypeDeclarationSyntax node) { + Debug.Assert(node is RecordDeclarationSyntax or RecordStructDeclarationSyntax); NamedTypeSymbol recordType = GetDeclaredType(node); var symbol = recordType.GetMembersUnordered().OfType().SingleOrDefault(); @@ -2026,11 +2017,14 @@ private ParameterSymbol GetMethodParameterSymbol( MethodSymbol method; - // PROTOTYPE(record-structs): update for record structs if (memberDecl is RecordDeclarationSyntax recordDecl && recordDecl.ParameterList == paramList) { method = TryGetSynthesizedRecordConstructor(recordDecl); } + else if (memberDecl is RecordStructDeclarationSyntax recordStructDecl && recordStructDecl.ParameterList == paramList) + { + method = TryGetSynthesizedRecordConstructor(recordStructDecl); + } else { method = (GetDeclaredSymbol(memberDecl, cancellationToken) as IMethodSymbol).GetSymbol(); @@ -2374,7 +2368,6 @@ internal override Symbol RemapSymbolIfNecessaryCore(Symbol symbol) internal override Func GetSyntaxNodesToAnalyzeFilter(SyntaxNode declaredNode, ISymbol declaredSymbol) { - // PROTOTYPE(record-structs): update for record structs switch (declaredNode) { case CompilationUnitSyntax unit when SimpleProgramNamedTypeSymbol.GetSimpleProgramEntryPoint(Compilation, unit, fallbackToMainEntryPoint: false) is SynthesizedSimpleProgramEntryPointSymbol entryPoint: @@ -2427,6 +2420,7 @@ internal override Func GetSyntaxNodesToAnalyzeFilter(SyntaxNod case SymbolKind.NamedType: Debug.Assert((object)declaredSymbol.GetSymbol() == (object)ctor.ContainingSymbol); // Accept nodes that do not match a 'parameter list'/'base arguments list'. + //return (node) => true; return (node) => node != recordDeclaration.ParameterList && !(node.Kind() == SyntaxKind.ArgumentList && node == recordDeclaration.PrimaryConstructorBaseType?.ArgumentList); @@ -2436,6 +2430,33 @@ internal override Func GetSyntaxNodesToAnalyzeFilter(SyntaxNod } break; + case RecordStructDeclarationSyntax recordStructDeclaration when TryGetSynthesizedRecordConstructor(recordStructDeclaration) is SynthesizedRecordConstructor ctor: + switch (declaredSymbol.Kind) + { + case SymbolKind.Method: + Debug.Assert((object)declaredSymbol.GetSymbol() == (object)ctor); + return (node) => + { + // Accept only nodes that either match, or above/below of a 'parameter list'. + if (node.Parent == recordStructDeclaration) + { + return node == recordStructDeclaration.ParameterList; + } + + return true; + }; + + case SymbolKind.NamedType: + Debug.Assert((object)declaredSymbol.GetSymbol() == (object)ctor.ContainingSymbol); + // Accept nodes that do not match a 'parameter list'. + return (node) => node != recordStructDeclaration.ParameterList; + + default: + ExceptionUtilities.UnexpectedValue(declaredSymbol.Kind); + break; + } + break; + case PrimaryConstructorBaseTypeSyntax { Parent: BaseListSyntax { Parent: RecordDeclarationSyntax recordDeclaration } } baseType when recordDeclaration.PrimaryConstructorBaseType == declaredNode && TryGetSynthesizedRecordConstructor(recordDeclaration) is SynthesizedRecordConstructor ctor: if ((object)declaredSymbol.GetSymbol() == (object)ctor) @@ -2448,6 +2469,10 @@ internal override Func GetSyntaxNodesToAnalyzeFilter(SyntaxNod case ParameterSyntax param when declaredSymbol.Kind == SymbolKind.Property && param.Parent?.Parent is RecordDeclarationSyntax recordDeclaration && recordDeclaration.ParameterList == param.Parent: Debug.Assert(declaredSymbol.GetSymbol() is SynthesizedRecordPropertySymbol); return (node) => false; + + case ParameterSyntax param when declaredSymbol.Kind == SymbolKind.Property && param.Parent?.Parent is RecordStructDeclarationSyntax recordStructDeclaration && recordStructDeclaration.ParameterList == param.Parent: + Debug.Assert(declaredSymbol.GetSymbol() is SynthesizedRecordPropertySymbol); + return (node) => false; } return null; diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs index bc1b1e6e0789b..67a98d7fceb9f 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs @@ -23,7 +23,6 @@ internal enum DeclarationKind : byte Submission, ImplicitClass, SimpleProgram, - // PROTOTYPE(record-structs): rename to RecordClass? Record, RecordStruct } diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs index 5c5407cf9e07c..b08517dc3976b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs @@ -565,7 +565,8 @@ internal override bool IsRecord } } - // PROTOTYPE(record-structs): update for record structs (is there a way to recognize a record struct from PE?) + // Is there a way to recognize a record struct from PE? + // Tracked by https://github.com/dotnet/roslyn/issues/52233 internal override bool IsRecordStruct => false; public override Accessibility DeclaredAccessibility diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index b3b07e3547598..61abd5c0761ec 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -1331,7 +1331,6 @@ public sealed override ImmutableArray GetMembers(string name) return ImmutableArray.Empty; } - // PROTOTYPE(record-structs): update for record structs /// /// For source symbols, there can only be a valid clone method if this is a record, which is a /// simple syntax check. This will need to change when we generalize cloning, but it's a good @@ -1512,7 +1511,6 @@ internal void AssertMemberExposure(Symbol member, bool forDiagnostics = false) var declared = Volatile.Read(ref _lazyDeclaredMembersAndInitializers); Debug.Assert(declared != DeclaredMembersAndInitializers.UninitializedSentinel); - // PROTOTYPE(record-structs): update for record structs if ((declared is object && (declared.NonTypeMembers.Contains(m => m == (object)member) || declared.RecordPrimaryConstructor == (object)member)) || Volatile.Read(ref _lazyMembersAndInitializers)?.NonTypeMembers.Contains(m => m == (object)member) == true) { @@ -2969,7 +2967,8 @@ private void AddDeclaredNontypeMembers(DeclaredMembersAndInitializersBuilder bui noteRecordParameters(recordStructDecl, parameterList, builder, diagnostics); AddNonTypeMembers(builder, recordStructDecl.Members, diagnostics); - // PROTOTYPE(record-structs): we will allow declaring parameterless constructors + // We will allow declaring parameterless constructors + // Tracking issue https://github.com/dotnet/roslyn/issues/52240 if (parameterList?.ParameterCount == 0) { diagnostics.Add(ErrorCode.ERR_StructsCantContainDefaultConstructor, parameterList.Location); diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs index fec1b54059fae..45ddc2aad48d9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs @@ -2253,7 +2253,6 @@ ITypeSymbol ITypeSymbolInternal.GetITypeSymbol() return GetITypeSymbol(DefaultNullableAnnotation); } - // PROTOTYPE(record-structs): rename to IsRecordClass? internal abstract bool IsRecord { get; } internal abstract bool IsRecordStruct { get; } diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs index 8fff4a2b59b9d..cf3e7aec7d779 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs @@ -795,7 +795,6 @@ public static SyntaxKind GetTypeDeclarationKind(SyntaxKind kind) case SyntaxKind.InterfaceKeyword: return SyntaxKind.InterfaceDeclaration; case SyntaxKind.RecordKeyword: - // PROTOTYPE(record-structs): anything we can do? return SyntaxKind.RecordDeclaration; default: return SyntaxKind.None; diff --git a/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs b/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs index 83abcd5258a67..ace34efc486da 100644 --- a/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs +++ b/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs @@ -63,7 +63,6 @@ private static SyntaxKind GetTypeDeclarationKeywordKind(SyntaxKind kind) return SyntaxKind.InterfaceKeyword; case SyntaxKind.RecordDeclaration: return SyntaxKind.RecordKeyword; - // PROTOTYPE(record-structs): update for record structs default: throw ExceptionUtilities.UnexpectedValue(kind); } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index 7b77466ee8cab..f62a20273cfd7 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -3,10 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; @@ -32,32 +35,29 @@ private CompilationVerifier CompileAndVerify( // init-only is unverifiable verify: Verification.Skipped); - [Fact(Skip = "PROTOTYPE(record-structs)")] + [Fact] public void StructRecord1() { var src = @" -data struct Point(int X, int Y);"; +record struct Point(int X, int Y);"; var verifier = CompileAndVerify(src).VerifyDiagnostics(); verifier.VerifyIL("Point.Equals(object)", @" { - // Code size 26 (0x1a) + // Code size 23 (0x17) .maxstack 2 - .locals init (Point V_0) IL_0000: ldarg.1 IL_0001: isinst ""Point"" - IL_0006: brtrue.s IL_000a - IL_0008: ldc.i4.0 - IL_0009: ret - IL_000a: ldarg.0 - IL_000b: ldarg.1 - IL_000c: unbox.any ""Point"" - IL_0011: stloc.0 - IL_0012: ldloca.s V_0 - IL_0014: call ""bool Point.Equals(in Point)"" - IL_0019: ret + IL_0006: brfalse.s IL_0015 + IL_0008: ldarg.0 + IL_0009: ldarg.1 + IL_000a: unbox.any ""Point"" + IL_000f: call ""bool Point.Equals(Point)"" + IL_0014: ret + IL_0015: ldc.i4.0 + IL_0016: ret }"); - verifier.VerifyIL("Point.Equals(in Point)", @" + verifier.VerifyIL("Point.Equals(Point)", @" { // Code size 49 (0x31) .maxstack 3 @@ -80,12 +80,12 @@ .maxstack 3 }"); } - [Fact(Skip = "PROTOTYPE(record-structs)")] + [Fact] public void StructRecord2() { var src = @" using System; -data struct S(int X, int Y) +record struct S(int X, int Y) { public static void Main() { @@ -103,27 +103,29 @@ public static void Main() False").VerifyDiagnostics(); } - [Fact(Skip = "PROTOTYPE(record-structs)")] + [Fact] public void StructRecord3() { var src = @" using System; -data struct S(int X, int Y) +record struct S(int X, int Y) { public bool Equals(S s) => false; public static void Main() { var s1 = new S(0, 1); Console.WriteLine(s1.Equals(s1)); - Console.WriteLine(s1.Equals(in s1)); } }"; - var verifier = CompileAndVerify(src, expectedOutput: @"False -True").VerifyDiagnostics(); + var verifier = CompileAndVerify(src, expectedOutput: @"False") + .VerifyDiagnostics( + // (5,17): warning CS8851: 'S' defines 'Equals' but not 'GetHashCode' + // public bool Equals(S s) => false; + Diagnostic(ErrorCode.WRN_RecordEqualsWithoutGetHashCode, "Equals").WithArguments("S").WithLocation(5, 17)); verifier.VerifyIL("S.Main", @" { - // Code size 37 (0x25) + // Code size 23 (0x17) .maxstack 3 .locals init (S V_0) //s1 IL_0000: ldloca.s V_0 @@ -134,52 +136,20 @@ .locals init (S V_0) //s1 IL_000b: ldloc.0 IL_000c: call ""bool S.Equals(S)"" IL_0011: call ""void System.Console.WriteLine(bool)"" - IL_0016: ldloca.s V_0 - IL_0018: ldloca.s V_0 - IL_001a: call ""bool S.Equals(in S)"" - IL_001f: call ""void System.Console.WriteLine(bool)"" - IL_0024: ret + IL_0016: ret }"); } - [Fact(Skip = "PROTOTYPE(record-structs)")] - public void StructRecord4() - { - var src = @" -using System; -data struct S(int X, int Y) -{ - public override bool Equals(object o) - { - Console.WriteLine(""obj""); - return true; - } - public bool Equals(in S s) - { - Console.WriteLine(""s""); - return true; - } - public static void Main() - { - var s1 = new S(0, 1); - s1.Equals((object)s1); - s1.Equals(s1); - } -}"; - var verifier = CompileAndVerify(src, expectedOutput: @"obj -s").VerifyDiagnostics(); - } - - [Fact(Skip = "PROTOTYPE(record-structs)")] + [Fact] public void StructRecord5() { var src = @" using System; -data struct S(int X, int Y) +record struct S(int X, int Y) { - public bool Equals(in S s) + public bool Equals(S s) { - Console.WriteLine(""s""); + Console.Write(""s""); return true; } public static void Main() @@ -189,15 +159,19 @@ public static void Main() s1.Equals(s1); } }"; - var verifier = CompileAndVerify(src, expectedOutput: @"s -s").VerifyDiagnostics(); + CompileAndVerify(src, expectedOutput: @"ss") + .VerifyDiagnostics( + // (5,17): warning CS8851: 'S' defines 'Equals' but not 'GetHashCode' + // public bool Equals(S s) + Diagnostic(ErrorCode.WRN_RecordEqualsWithoutGetHashCode, "Equals").WithArguments("S").WithLocation(5, 17)); } - [Fact(Skip = "PROTOTYPE(record-structs)")] + [Fact] public void StructRecordDefaultCtor() { const string src = @" -public data struct S(int X);"; +public record struct S(int X);"; + const string src2 = @" class C { @@ -211,12 +185,13 @@ class C comp2.VerifyDiagnostics(); } - [Fact(Skip = "PROTOTYPE(record-structs)")] + [Fact] public void Equality_01() { var source = @"using static System.Console; -data struct S; +record struct S; + class Program { static void Main() @@ -227,106 +202,38 @@ static void Main() WriteLine(((object)x).Equals(y)); } }"; - var comp = CreateCompilation(source, parseOptions: TestOptions.Regular9, options: TestOptions.ReleaseExe); - var verifier = CompileAndVerify(comp, expectedOutput: -@"True + var comp = CreateCompilation(source, parseOptions: TestOptions.RegularPreview, options: TestOptions.ReleaseExe); + + var verifier = CompileAndVerify(comp, expectedOutput: @"True True").VerifyDiagnostics(); - verifier.VerifyIL("S.Equals(in S)", -@"{ - // Code size 23 (0x17) - .maxstack 2 - .locals init (S V_0) - IL_0000: ldarg.0 - IL_0001: call ""System.Type S.EqualityContract.get"" - IL_0006: ldarg.1 - IL_0007: ldobj ""S"" - IL_000c: stloc.0 - IL_000d: ldloca.s V_0 - IL_000f: call ""System.Type S.EqualityContract.get"" - IL_0014: ceq - IL_0016: ret + verifier.VerifyIL("S.Equals(S)", @" +{ + // Code size 2 (0x2) + .maxstack 1 + IL_0000: ldc.i4.1 + IL_0001: ret }"); - verifier.VerifyIL("S.Equals(object)", -@"{ - // Code size 26 (0x1a) + verifier.VerifyIL("S.Equals(object)", @" +{ + // Code size 23 (0x17) .maxstack 2 - .locals init (S V_0) IL_0000: ldarg.1 IL_0001: isinst ""S"" - IL_0006: brtrue.s IL_000a - IL_0008: ldc.i4.0 - IL_0009: ret - IL_000a: ldarg.0 - IL_000b: ldarg.1 - IL_000c: unbox.any ""S"" - IL_0011: stloc.0 - IL_0012: ldloca.s V_0 - IL_0014: call ""bool S.Equals(in S)"" - IL_0019: ret -}"); - } - - [Fact(Skip = "PROTOTYPE(record-structs)")] - public void RecordClone4_0() - { - var comp = CreateCompilation(@" -using System; -public data struct S(int x, int y) -{ - public event Action E; - public int Z; -}"); - comp.VerifyDiagnostics( - // (3,21): error CS0171: Field 'S.E' must be fully assigned before control is returned to the caller - // public data struct S(int x, int y) - Diagnostic(ErrorCode.ERR_UnassignedThis, "(int x, int y)").WithArguments("S.E").WithLocation(3, 21), - // (3,21): error CS0171: Field 'S.Z' must be fully assigned before control is returned to the caller - // public data struct S(int x, int y) - Diagnostic(ErrorCode.ERR_UnassignedThis, "(int x, int y)").WithArguments("S.Z").WithLocation(3, 21), - // (5,25): warning CS0067: The event 'S.E' is never used - // public event Action E; - Diagnostic(ErrorCode.WRN_UnreferencedEvent, "E").WithArguments("S.E").WithLocation(5, 25) - ); - - var s = comp.GlobalNamespace.GetTypeMember("S"); - var clone = s.GetMethod(WellKnownMemberNames.CloneMethodName); - Assert.Equal(0, clone.Arity); - Assert.Equal(0, clone.ParameterCount); - Assert.Equal(s, clone.ReturnType); - - var ctor = (MethodSymbol)s.GetMembers(".ctor")[1]; - Assert.Equal(1, ctor.ParameterCount); - Assert.True(ctor.Parameters[0].Type.Equals(s, TypeCompareKind.ConsiderEverything)); - } - - [Fact(Skip = "PROTOTYPE(record-structs)")] - public void RecordClone4_1() - { - var comp = CreateCompilation(@" -using System; -public data struct S(int x, int y) -{ - public event Action E = null; - public int Z = 0; + IL_0006: brfalse.s IL_0015 + IL_0008: ldarg.0 + IL_0009: ldarg.1 + IL_000a: unbox.any ""S"" + IL_000f: call ""bool S.Equals(S)"" + IL_0014: ret + IL_0015: ldc.i4.0 + IL_0016: ret }"); - comp.VerifyDiagnostics( - // (5,25): error CS0573: 'S': cannot have instance property or field initializers in structs - // public event Action E = null; - Diagnostic(ErrorCode.ERR_FieldInitializerInStruct, "E").WithArguments("S").WithLocation(5, 25), - // (5,25): warning CS0414: The field 'S.E' is assigned but its value is never used - // public event Action E = null; - Diagnostic(ErrorCode.WRN_UnreferencedFieldAssg, "E").WithArguments("S.E").WithLocation(5, 25), - // (6,16): error CS0573: 'S': cannot have instance property or field initializers in structs - // public int Z = 0; - Diagnostic(ErrorCode.ERR_FieldInitializerInStruct, "Z").WithArguments("S").WithLocation(6, 16) - ); } [Fact] public void RecordStructLanguageVersion() { - // PROTOTYPE(record-structs): can we improve the error recovery, maybe treating this as a record struct with missing `record`? var src1 = @" struct Point(int x, int y); "; @@ -851,7 +758,8 @@ public record struct S public S() { } } "; - // PROTOTYPE(record-structs): this will be allowed in C# 10 + // This will be allowed in C# 10 + // Tracking issue https://github.com/dotnet/roslyn/issues/52240 var comp = CreateCompilation(src); comp.VerifyDiagnostics( // (4,12): error CS0568: Structs cannot contain explicit parameterless constructors @@ -870,7 +778,8 @@ public record struct S public int Property { get; set; } = 43; } "; - // PROTOTYPE(record-structs): this will be allowed in C# 10, or we need to improve the message + // This will be allowed in C# 10, or we need to improve the message + // Tracking issue https://github.com/dotnet/roslyn/issues/52240 var comp = CreateCompilation(src); comp.VerifyDiagnostics( // (4,16): error CS0573: 'S': cannot have instance property or field initializers in structs @@ -1498,7 +1407,8 @@ .maxstack 2 [Fact] public void RecordProperties_01_EmptyParameterList() { - // PROTOTYPE(record-structs): we will allow declaring parameterless constructors + // We will allow declaring parameterless constructors + // Tracking issue https://github.com/dotnet/roslyn/issues/52240 var src = @" using System; record struct C() @@ -2857,6 +2767,44 @@ public static class IsExternalInit Assert.Equal("", property.GetDocumentationCommentXml()); } + [Fact] + public void XmlDoc_Cref() + { + var src = @" +/// Summary +/// Description for +public record struct C(int I1) +{ + /// Summary + /// Description for + public void M(int x) { } +} + +namespace System.Runtime.CompilerServices +{ + /// Ignored + public static class IsExternalInit + { + } +} +"; + + var comp = CreateCompilation(src, parseOptions: TestOptions.RegularWithDocumentationComments.WithLanguageVersion(LanguageVersion.Preview)); + comp.VerifyDiagnostics( + // (7,52): warning CS1574: XML comment has cref attribute 'x' that could not be resolved + // /// Description for + Diagnostic(ErrorCode.WRN_BadXMLRef, "x").WithArguments("x").WithLocation(7, 52) + ); + + var tree = comp.SyntaxTrees.Single(); + var docComments = tree.GetCompilationUnitRoot().DescendantTrivia().Select(trivia => trivia.GetStructure()).OfType(); + var cref = docComments.First().DescendantNodes().OfType().First().Cref; + Assert.Equal("I1", cref.ToString()); + + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + Assert.Equal(SymbolKind.Property, model.GetSymbolInfo(cref).Symbol!.Kind); + } + [Fact] public void Deconstruct_Simple() { @@ -3625,6 +3573,24 @@ public bool Equals(B other) "); } + [Fact] + public void RecordEquals_01_NoInParameters() + { + var source = @" +var a1 = new B(); +var a2 = new B(); +System.Console.WriteLine(a1.Equals(in a2)); + +record struct B; +"; + var comp = CreateCompilation(source); + comp.VerifyEmitDiagnostics( + // (4,39): error CS1615: Argument 1 may not be passed with the 'in' keyword + // System.Console.WriteLine(a1.Equals(in a2)); + Diagnostic(ErrorCode.ERR_BadArgExtraRef, "a2").WithArguments("1", "in").WithLocation(4, 39) + ); + } + [Theory] [InlineData("protected")] [InlineData("private protected")] @@ -5190,5 +5156,959 @@ record struct C1 Diagnostic(ErrorCode.ERR_StaticAPIInRecord, "PrintMembers").WithArguments("C1.PrintMembers(System.Text.StringBuilder)").WithLocation(4, 25) ); } + + [Fact] + public void AmbigCtor_WithFieldInitializer() + { + var src = @" +record struct R(R X) +{ + public R X { get; init; } = X; +} +"; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (4,14): error CS0523: Struct member 'R.X' of type 'R' causes a cycle in the struct layout + // public R X { get; init; } = X; + Diagnostic(ErrorCode.ERR_StructLayoutCycle, "X").WithArguments("R.X", "R").WithLocation(4, 14) + ); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var parameterSyntax = tree.GetRoot().DescendantNodes().OfType().Single(); + var parameter = model.GetDeclaredSymbol(parameterSyntax)!; + Assert.Equal("R X", parameter.ToTestDisplayString()); + Assert.Equal(SymbolKind.Parameter, parameter.Kind); + Assert.Equal("R..ctor(R X)", parameter.ContainingSymbol.ToTestDisplayString()); + + var initializerSyntax = tree.GetRoot().DescendantNodes().OfType().Single(); + var initializer = model.GetSymbolInfo(initializerSyntax.Value).Symbol!; + Assert.Equal("R X", initializer.ToTestDisplayString()); + Assert.Equal(SymbolKind.Parameter, initializer.Kind); + Assert.Equal("R..ctor(R X)", initializer.ContainingSymbol.ToTestDisplayString()); + } + + [Fact] + public void GetDeclaredSymbolOnFieldInitializer() + { + var src = @" +record struct R(int I) +{ + public int I { get; init; } = M(out int i); + static int M(out int i) => throw null; +} +"; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (2,21): warning CS8907: Parameter 'I' is unread. Did you forget to use it to initialize the property with that name? + // record struct R(int I) + Diagnostic(ErrorCode.WRN_UnreadRecordParameter, "I").WithArguments("I").WithLocation(2, 21) + ); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var outVarSyntax = tree.GetRoot().DescendantNodes().OfType().Single(); + var outVar = model.GetDeclaredSymbol(outVarSyntax)!; + Assert.Equal("System.Int32 i", outVar.ToTestDisplayString()); + Assert.Equal(SymbolKind.Local, outVar.Kind); + Assert.Equal("System.Int32 R.k__BackingField", outVar.ContainingSymbol.ToTestDisplayString()); + } + + [Fact] + public void AnalyzerActions_01() + { + // Test RegisterSyntaxNodeAction + var text1 = @" +record struct A([Attr1]int X = 0) : I1 +{ + private int M() => 3; +} + +interface I1 {} + +class Attr1 : System.Attribute {} +"; + var analyzer = new AnalyzerActions_01_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount0); + Assert.Equal(1, analyzer.FireCountRecordStructDeclarationA); + Assert.Equal(1, analyzer.FireCountRecordStructDeclarationACtor); + Assert.Equal(1, analyzer.FireCount3); + Assert.Equal(1, analyzer.FireCountSimpleBaseTypeI1onA); + Assert.Equal(1, analyzer.FireCount5); + Assert.Equal(1, analyzer.FireCountParameterListAPrimaryCtor); + Assert.Equal(1, analyzer.FireCount7); + } + + private class AnalyzerActions_01_Analyzer : DiagnosticAnalyzer + { + public int FireCount0; + public int FireCountRecordStructDeclarationA; + public int FireCountRecordStructDeclarationACtor; + public int FireCount3; + public int FireCountSimpleBaseTypeI1onA; + public int FireCount5; + public int FireCountParameterListAPrimaryCtor; + public int FireCount7; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(Handle1, SyntaxKind.NumericLiteralExpression); + context.RegisterSyntaxNodeAction(Handle2, SyntaxKind.EqualsValueClause); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.BaseConstructorInitializer); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ConstructorDeclaration); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.PrimaryConstructorBaseType); + context.RegisterSyntaxNodeAction(Handle6, SyntaxKind.RecordStructDeclaration); + context.RegisterSyntaxNodeAction(Handle7, SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(Handle8, SyntaxKind.SimpleBaseType); + context.RegisterSyntaxNodeAction(Handle9, SyntaxKind.ParameterList); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ArgumentList); + } + + protected void Handle1(SyntaxNodeAnalysisContext context) + { + var literal = (LiteralExpressionSyntax)context.Node; + + switch (literal.ToString()) + { + case "0": + Interlocked.Increment(ref FireCount0); + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + break; + case "3": + Interlocked.Increment(ref FireCount7); + Assert.Equal("System.Int32 A.M()", context.ContainingSymbol.ToTestDisplayString()); + break; + default: + Assert.True(false); + break; + } + + Assert.Same(literal.SyntaxTree, context.ContainingSymbol!.DeclaringSyntaxReferences.Single().SyntaxTree); + } + + protected void Handle2(SyntaxNodeAnalysisContext context) + { + var equalsValue = (EqualsValueClauseSyntax)context.Node; + + switch (equalsValue.ToString()) + { + case "= 0": + Interlocked.Increment(ref FireCount3); + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + break; + default: + Assert.True(false); + break; + } + + Assert.Same(equalsValue.SyntaxTree, context.ContainingSymbol!.DeclaringSyntaxReferences.Single().SyntaxTree); + } + + protected void Fail(SyntaxNodeAnalysisContext context) + { + Assert.True(false); + } + + protected void Handle6(SyntaxNodeAnalysisContext context) + { + var record = (RecordStructDeclarationSyntax)context.Node; + + switch (context.ContainingSymbol.ToTestDisplayString()) + { + case "A": + Interlocked.Increment(ref FireCountRecordStructDeclarationA); + break; + case "A..ctor([System.Int32 X = 0])": + Interlocked.Increment(ref FireCountRecordStructDeclarationACtor); + break; + default: + Assert.True(false); + break; + } + + Assert.Same(record.SyntaxTree, context.ContainingSymbol!.DeclaringSyntaxReferences.Single().SyntaxTree); + } + + protected void Handle7(SyntaxNodeAnalysisContext context) + { + var identifier = (IdentifierNameSyntax)context.Node; + + switch (identifier.Identifier.ValueText) + { + case "Attr1": + Interlocked.Increment(ref FireCount5); + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + break; + } + } + + protected void Handle8(SyntaxNodeAnalysisContext context) + { + var baseType = (SimpleBaseTypeSyntax)context.Node; + + switch (baseType.ToString()) + { + case "I1": + switch (context.ContainingSymbol.ToTestDisplayString()) + { + case "A": + Interlocked.Increment(ref FireCountSimpleBaseTypeI1onA); + break; + default: + Assert.True(false); + break; + } + break; + + case "System.Attribute": + break; + + default: + Assert.True(false); + break; + } + } + + protected void Handle9(SyntaxNodeAnalysisContext context) + { + var parameterList = (ParameterListSyntax)context.Node; + + switch (parameterList.ToString()) + { + case "([Attr1]int X = 0)": + Interlocked.Increment(ref FireCountParameterListAPrimaryCtor); + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + break; + case "()": + break; + default: + Assert.True(false); + break; + } + } + } + + [Fact] + public void AnalyzerActions_02() + { + // Test RegisterSymbolAction + var text1 = @" +record struct A(int X = 0) +{} + +record struct C +{ + C(int Z = 4) + {} +} +"; + + var analyzer = new AnalyzerActions_02_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount1); + Assert.Equal(1, analyzer.FireCount2); + Assert.Equal(1, analyzer.FireCount3); + Assert.Equal(1, analyzer.FireCount4); + Assert.Equal(1, analyzer.FireCount5); + Assert.Equal(1, analyzer.FireCount6); + Assert.Equal(1, analyzer.FireCount7); + } + + private class AnalyzerActions_02_Analyzer : DiagnosticAnalyzer + { + public int FireCount1; + public int FireCount2; + public int FireCount3; + public int FireCount4; + public int FireCount5; + public int FireCount6; + public int FireCount7; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction(Handle, SymbolKind.Method); + context.RegisterSymbolAction(Handle, SymbolKind.Property); + context.RegisterSymbolAction(Handle, SymbolKind.Parameter); + context.RegisterSymbolAction(Handle, SymbolKind.NamedType); + } + + private void Handle(SymbolAnalysisContext context) + { + switch (context.Symbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + Interlocked.Increment(ref FireCount1); + break; + case "System.Int32 A.X { get; set; }": + Interlocked.Increment(ref FireCount2); + break; + case "[System.Int32 X = 0]": + Interlocked.Increment(ref FireCount3); + break; + case "C..ctor([System.Int32 Z = 4])": + Interlocked.Increment(ref FireCount4); + break; + case "[System.Int32 Z = 4]": + Interlocked.Increment(ref FireCount5); + break; + case "A": + Interlocked.Increment(ref FireCount6); + break; + case "C": + Interlocked.Increment(ref FireCount7); + break; + case "System.Runtime.CompilerServices.IsExternalInit": + break; + default: + Assert.True(false); + break; + } + } + } + + [Fact] + public void AnalyzerActions_03() + { + // Test RegisterSymbolStartAction + var text1 = @" +readonly record struct A(int X = 0) +{} + +readonly record struct C +{ + C(int Z = 4) + {} +} +"; + + var analyzer = new AnalyzerActions_03_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount1); + Assert.Equal(1, analyzer.FireCount2); + Assert.Equal(0, analyzer.FireCount3); + Assert.Equal(1, analyzer.FireCount4); + Assert.Equal(0, analyzer.FireCount5); + Assert.Equal(1, analyzer.FireCount6); + Assert.Equal(1, analyzer.FireCount7); + Assert.Equal(1, analyzer.FireCount8); + Assert.Equal(1, analyzer.FireCount9); + Assert.Equal(1, analyzer.FireCount10); + Assert.Equal(1, analyzer.FireCount11); + Assert.Equal(1, analyzer.FireCount12); + } + + private class AnalyzerActions_03_Analyzer : DiagnosticAnalyzer + { + public int FireCount1; + public int FireCount2; + public int FireCount3; + public int FireCount4; + public int FireCount5; + public int FireCount6; + public int FireCount7; + public int FireCount8; + public int FireCount9; + public int FireCount10; + public int FireCount11; + public int FireCount12; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolStartAction(Handle1, SymbolKind.Method); + context.RegisterSymbolStartAction(Handle1, SymbolKind.Property); + context.RegisterSymbolStartAction(Handle1, SymbolKind.Parameter); + context.RegisterSymbolStartAction(Handle1, SymbolKind.NamedType); + } + + private void Handle1(SymbolStartAnalysisContext context) + { + switch (context.Symbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + Interlocked.Increment(ref FireCount1); + context.RegisterSymbolEndAction(Handle2); + break; + case "System.Int32 A.X { get; init; }": + Interlocked.Increment(ref FireCount2); + context.RegisterSymbolEndAction(Handle3); + break; + case "[System.Int32 X = 0]": + Interlocked.Increment(ref FireCount3); + break; + case "C..ctor([System.Int32 Z = 4])": + Interlocked.Increment(ref FireCount4); + context.RegisterSymbolEndAction(Handle4); + break; + case "[System.Int32 Z = 4]": + Interlocked.Increment(ref FireCount5); + break; + case "A": + Interlocked.Increment(ref FireCount9); + + Assert.Equal(0, FireCount1); + Assert.Equal(0, FireCount2); + Assert.Equal(0, FireCount6); + Assert.Equal(0, FireCount7); + + context.RegisterSymbolEndAction(Handle5); + break; + case "C": + Interlocked.Increment(ref FireCount10); + + Assert.Equal(0, FireCount4); + Assert.Equal(0, FireCount8); + + context.RegisterSymbolEndAction(Handle6); + break; + case "System.Runtime.CompilerServices.IsExternalInit": + break; + default: + Assert.True(false); + break; + } + } + + private void Handle2(SymbolAnalysisContext context) + { + Assert.Equal("A..ctor([System.Int32 X = 0])", context.Symbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount6); + } + + private void Handle3(SymbolAnalysisContext context) + { + Assert.Equal("System.Int32 A.X { get; init; }", context.Symbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount7); + } + + private void Handle4(SymbolAnalysisContext context) + { + Assert.Equal("C..ctor([System.Int32 Z = 4])", context.Symbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount8); + } + + private void Handle5(SymbolAnalysisContext context) + { + Assert.Equal("A", context.Symbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount11); + + Assert.Equal(1, FireCount1); + Assert.Equal(1, FireCount2); + Assert.Equal(1, FireCount6); + Assert.Equal(1, FireCount7); + } + + private void Handle6(SymbolAnalysisContext context) + { + Assert.Equal("C", context.Symbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount12); + + Assert.Equal(1, FireCount4); + Assert.Equal(1, FireCount8); + } + } + + [Fact] + public void AnalyzerActions_04() + { + // Test RegisterOperationAction + var text1 = @" +record struct A([Attr1(100)]int X = 0) : I1 +{} + +interface I1 {} +"; + + var analyzer = new AnalyzerActions_04_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(0, analyzer.FireCount1); + Assert.Equal(1, analyzer.FireCount6); + Assert.Equal(1, analyzer.FireCount7); + Assert.Equal(1, analyzer.FireCount14); + } + + private class AnalyzerActions_04_Analyzer : DiagnosticAnalyzer + { + public int FireCount1; + public int FireCount6; + public int FireCount7; + public int FireCount14; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationAction(HandleConstructorBody, OperationKind.ConstructorBody); + context.RegisterOperationAction(HandleInvocation, OperationKind.Invocation); + context.RegisterOperationAction(HandleLiteral, OperationKind.Literal); + context.RegisterOperationAction(HandleParameterInitializer, OperationKind.ParameterInitializer); + context.RegisterOperationAction(Fail, OperationKind.PropertyInitializer); + context.RegisterOperationAction(Fail, OperationKind.FieldInitializer); + } + + protected void HandleConstructorBody(OperationAnalysisContext context) + { + switch (context.ContainingSymbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + Interlocked.Increment(ref FireCount1); + Assert.Equal(SyntaxKind.RecordDeclaration, context.Operation.Syntax.Kind()); + VerifyOperationTree((CSharpCompilation)context.Compilation, context.Operation, @""); + + break; + default: + Assert.True(false); + break; + } + } + + protected void HandleInvocation(OperationAnalysisContext context) + { + Assert.True(false); + } + + protected void HandleLiteral(OperationAnalysisContext context) + { + switch (context.Operation.Syntax.ToString()) + { + case "100": + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount6); + break; + case "0": + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount7); + break; + default: + Assert.True(false); + break; + } + } + + protected void HandleParameterInitializer(OperationAnalysisContext context) + { + switch (context.Operation.Syntax.ToString()) + { + case "= 0": + Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); + Interlocked.Increment(ref FireCount14); + break; + default: + Assert.True(false); + break; + } + } + + protected void Fail(OperationAnalysisContext context) + { + Assert.True(false); + } + } + + [Fact] + public void AnalyzerActions_05() + { + // Test RegisterOperationBlockAction + var text1 = @" +record struct A([Attr1(100)]int X = 0) : I1 +{} + +interface I1 {} +"; + + var analyzer = new AnalyzerActions_05_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount1); + } + + private class AnalyzerActions_05_Analyzer : DiagnosticAnalyzer + { + public int FireCount1; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterOperationBlockAction(Handle); + } + + private void Handle(OperationBlockAnalysisContext context) + { + switch (context.OwningSymbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + Interlocked.Increment(ref FireCount1); + Assert.Equal(2, context.OperationBlocks.Length); + + Assert.Equal(OperationKind.ParameterInitializer, context.OperationBlocks[0].Kind); + Assert.Equal("= 0", context.OperationBlocks[0].Syntax.ToString()); + + Assert.Equal(OperationKind.None, context.OperationBlocks[1].Kind); + Assert.Equal("Attr1(100)", context.OperationBlocks[1].Syntax.ToString()); + + break; + default: + Assert.True(false); + break; + } + } + } + + [Fact] + public void AnalyzerActions_07() + { + // Test RegisterCodeBlockAction + var text1 = @" +record struct A([Attr1(100)]int X = 0) : I1 +{ + int M() => 3; +} + +interface I1 {} +"; + var analyzer = new AnalyzerActions_07_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount1); + Assert.Equal(1, analyzer.FireCount4); + } + + private class AnalyzerActions_07_Analyzer : DiagnosticAnalyzer + { + public int FireCount1; + public int FireCount4; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterCodeBlockAction(Handle); + } + + private void Handle(CodeBlockAnalysisContext context) + { + switch (context.OwningSymbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + + switch (context.CodeBlock) + { + case RecordStructDeclarationSyntax { Identifier: { ValueText: "A" } }: + Interlocked.Increment(ref FireCount1); + break; + default: + Assert.True(false); + break; + } + break; + case "System.Int32 A.M()": + switch (context.CodeBlock) + { + case MethodDeclarationSyntax { Identifier: { ValueText: "M" } }: + Interlocked.Increment(ref FireCount4); + break; + default: + Assert.True(false); + break; + } + break; + default: + Assert.True(false); + break; + } + } + } + + [Fact] + public void AnalyzerActions_08() + { + // Test RegisterCodeBlockStartAction + var text1 = @" +record struct A([Attr1]int X = 0) : I1 +{ + private int M() => 3; +} + +interface I1 {} +"; + + var analyzer = new AnalyzerActions_08_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount100); + Assert.Equal(1, analyzer.FireCount400); + + Assert.Equal(1, analyzer.FireCount0); + Assert.Equal(0, analyzer.FireCountRecordStructDeclarationA); + Assert.Equal(0, analyzer.FireCountRecordStructDeclarationACtor); + Assert.Equal(1, analyzer.FireCount3); + Assert.Equal(0, analyzer.FireCountSimpleBaseTypeI1onA); + Assert.Equal(1, analyzer.FireCount5); + Assert.Equal(0, analyzer.FireCountParameterListAPrimaryCtor); + Assert.Equal(1, analyzer.FireCount7); + + Assert.Equal(1, analyzer.FireCount1000); + Assert.Equal(1, analyzer.FireCount4000); + } + + private class AnalyzerActions_08_Analyzer : AnalyzerActions_01_Analyzer + { + public int FireCount100; + public int FireCount400; + public int FireCount1000; + public int FireCount4000; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterCodeBlockStartAction(Handle); + } + + private void Handle(CodeBlockStartAnalysisContext context) + { + switch (context.OwningSymbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + + switch (context.CodeBlock) + { + case RecordStructDeclarationSyntax { Identifier: { ValueText: "A" } }: + Interlocked.Increment(ref FireCount100); + break; + default: + Assert.True(false); + break; + } + break; + case "System.Int32 A.M()": + switch (context.CodeBlock) + { + case MethodDeclarationSyntax { Identifier: { ValueText: "M" } }: + Interlocked.Increment(ref FireCount400); + break; + default: + Assert.True(false); + break; + } + break; + default: + Assert.True(false); + break; + } + + context.RegisterSyntaxNodeAction(Handle1, SyntaxKind.NumericLiteralExpression); + context.RegisterSyntaxNodeAction(Handle2, SyntaxKind.EqualsValueClause); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.BaseConstructorInitializer); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ConstructorDeclaration); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.PrimaryConstructorBaseType); + context.RegisterSyntaxNodeAction(Handle6, SyntaxKind.RecordStructDeclaration); + context.RegisterSyntaxNodeAction(Handle7, SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(Handle8, SyntaxKind.SimpleBaseType); + context.RegisterSyntaxNodeAction(Handle9, SyntaxKind.ParameterList); + context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ArgumentList); + + context.RegisterCodeBlockEndAction(Handle11); + } + + private void Handle11(CodeBlockAnalysisContext context) + { + switch (context.OwningSymbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + + switch (context.CodeBlock) + { + case RecordStructDeclarationSyntax { Identifier: { ValueText: "A" } }: + Interlocked.Increment(ref FireCount1000); + break; + default: + Assert.True(false); + break; + } + break; + case "System.Int32 A.M()": + switch (context.CodeBlock) + { + case MethodDeclarationSyntax { Identifier: { ValueText: "M" } }: + Interlocked.Increment(ref FireCount4000); + break; + default: + Assert.True(false); + break; + } + break; + default: + Assert.True(false); + break; + } + } + } + + [Fact] + public void AnalyzerActions_09() + { + // PROTOTYPE(record-structs): ported + var text1 = @" +record A([Attr1(100)]int X = 0) : I1 +{} + +record B([Attr2(200)]int Y = 1) : A(2), I1 +{ + int M() => 3; +} + +record C : A, I1 +{ + C([Attr3(300)]int Z = 4) : base(5) + {} +} + +interface I1 {} +"; + + var analyzer = new AnalyzerActions_09_Analyzer(); + var comp = CreateCompilation(text1); + comp.GetAnalyzerDiagnostics(new[] { analyzer }, null).Verify(); + + Assert.Equal(1, analyzer.FireCount1); + Assert.Equal(1, analyzer.FireCount2); + Assert.Equal(1, analyzer.FireCount3); + Assert.Equal(1, analyzer.FireCount4); + Assert.Equal(1, analyzer.FireCount5); + Assert.Equal(1, analyzer.FireCount6); + Assert.Equal(1, analyzer.FireCount7); + Assert.Equal(1, analyzer.FireCount8); + Assert.Equal(1, analyzer.FireCount9); + } + + private class AnalyzerActions_09_Analyzer : DiagnosticAnalyzer + { + public int FireCount1; + public int FireCount2; + public int FireCount3; + public int FireCount4; + public int FireCount5; + public int FireCount6; + public int FireCount7; + public int FireCount8; + public int FireCount9; + + private static readonly DiagnosticDescriptor Descriptor = + new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction(Handle1, SymbolKind.Method); + context.RegisterSymbolAction(Handle2, SymbolKind.Property); + context.RegisterSymbolAction(Handle3, SymbolKind.Parameter); + } + + private void Handle1(SymbolAnalysisContext context) + { + switch (context.Symbol.ToTestDisplayString()) + { + case "A..ctor([System.Int32 X = 0])": + Interlocked.Increment(ref FireCount1); + break; + case "B..ctor([System.Int32 Y = 1])": + Interlocked.Increment(ref FireCount2); + break; + case "C..ctor([System.Int32 Z = 4])": + Interlocked.Increment(ref FireCount3); + break; + case "System.Int32 B.M()": + Interlocked.Increment(ref FireCount4); + break; + default: + Assert.True(false); + break; + } + } + + private void Handle2(SymbolAnalysisContext context) + { + switch (context.Symbol.ToTestDisplayString()) + { + case "System.Int32 A.X { get; init; }": + Interlocked.Increment(ref FireCount5); + break; + case "System.Int32 B.Y { get; init; }": + Interlocked.Increment(ref FireCount6); + break; + default: + Assert.True(false); + break; + } + } + + private void Handle3(SymbolAnalysisContext context) + { + switch (context.Symbol.ToTestDisplayString()) + { + case "[System.Int32 X = 0]": + Interlocked.Increment(ref FireCount7); + break; + case "[System.Int32 Y = 1]": + Interlocked.Increment(ref FireCount8); + break; + case "[System.Int32 Z = 4]": + Interlocked.Increment(ref FireCount9); + break; + default: + Assert.True(false); + break; + } + } + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index d7562ba36955e..c671becaf547a 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -487,6 +487,7 @@ record R3(R3 x) : Base [Fact, WorkItem(49628, "https://github.com/dotnet/roslyn/issues/49628")] public void AmbigCtor_WithFieldInitializer() { + // PROTOTYPE(record-structs): ported var src = @" record R(R X) { @@ -515,6 +516,32 @@ record R(R X) Assert.Equal("R..ctor(R X)", initializer.ContainingSymbol.ToTestDisplayString()); } + [Fact] + public void GetDeclaredSymbolOnFieldInitializer() + { + var src = @" +record R(int I) +{ + public int I { get; init; } = M(out int i) ? i : 0; + static bool M(out int i) => throw null; +} +"; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (2,14): warning CS8907: Parameter 'I' is unread. Did you forget to use it to initialize the property with that name? + // record R(int I) + Diagnostic(ErrorCode.WRN_UnreadRecordParameter, "I").WithArguments("I").WithLocation(2, 14) + ); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var outVarSyntax = tree.GetRoot().DescendantNodes().OfType().Single(); + var outVar = model.GetDeclaredSymbol(outVarSyntax)!; + Assert.Equal("System.Int32 i", outVar.ToTestDisplayString()); + Assert.Equal(SymbolKind.Local, outVar.Kind); + Assert.Equal("System.Int32 R.k__BackingField", outVar.ContainingSymbol.ToTestDisplayString()); + } + [Fact, WorkItem(46123, "https://github.com/dotnet/roslyn/issues/46123")] public void IncompletePositionalRecord() { @@ -25841,6 +25868,7 @@ public record A; [Fact] public void AnalyzerActions_01() { + // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1]int X = 0) : I1 {} @@ -26234,6 +26262,7 @@ protected void Handle10(SyntaxNodeAnalysisContext context) [Fact] public void AnalyzerActions_02() { + // PROTOTYPE(record-structs): ported var text1 = @" record A(int X = 0) {} @@ -26319,6 +26348,7 @@ private void Handle(SymbolAnalysisContext context) [Fact] public void AnalyzerActions_03() { + // PROTOTYPE(record-structs): ported var text1 = @" record A(int X = 0) {} @@ -26467,6 +26497,7 @@ private void Handle6(SymbolAnalysisContext context) [Fact] public void AnalyzerActions_04() { + // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1(100)]int X = 0) : I1 {} @@ -26683,6 +26714,7 @@ protected void Handle5(OperationAnalysisContext context) [Fact] public void AnalyzerActions_05() { + // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1(100)]int X = 0) : I1 {} @@ -26997,6 +27029,7 @@ private void Handle6(OperationBlockAnalysisContext context) [Fact] public void AnalyzerActions_07() { + // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1(100)]int X = 0) : I1 {} @@ -27102,6 +27135,7 @@ private void Handle(CodeBlockAnalysisContext context) [Fact] public void AnalyzerActions_08() { + // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1]int X = 0) : I1 {} @@ -27318,6 +27352,7 @@ private void Handle11(CodeBlockAnalysisContext context) [Fact] public void AnalyzerActions_09() { + // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1(100)]int X = 0) : I1 {} @@ -27824,6 +27859,7 @@ public static class IsExternalInit [WorkItem(44571, "https://github.com/dotnet/roslyn/issues/44571")] public void XmlDoc_Cref() { + // PROTOTYPE(record-structs): ported var src = @" /// Summary /// Description for diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/RecordParsing.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/RecordParsing.cs index b1215b3ffc198..5e9081dd8c219 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/RecordParsing.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/RecordParsing.cs @@ -2983,7 +2983,6 @@ void verifyParsedAsRecord() [Fact, CompilerTrait(CompilerFeature.RecordStructs)] public void RecordInterfaceParsing() { - // PROTOTYPE(record-structs): should we treat this as a binding error instead, for better error recovery? var text = "record interface C(int X, int Y);"; UsingTree(text, options: TestOptions.RegularPreview, // (1,8): error CS1001: Identifier expected From 6ef4144a191e0b20fc6d182a9412efbfb950cfcd Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 31 Mar 2021 11:21:46 -0700 Subject: [PATCH 2/8] Address feedback --- .../Portable/Compilation/SyntaxTreeSemanticModel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs index bcc126d547985..7dc80d42e2600 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs @@ -1094,7 +1094,12 @@ private MemberSemanticModel CreateMemberModel(CSharpSyntaxNode node) case SyntaxKind.RecordDeclaration: { SynthesizedRecordConstructor symbol = TryGetSynthesizedRecordConstructor((RecordDeclarationSyntax)node); - return symbol is null ? null : createMethodBodySemanticModel(node, symbol); + if (symbol is null) + { + return null; + } + + return createMethodBodySemanticModel(node, symbol); } case SyntaxKind.GetAccessorDeclaration: @@ -2420,7 +2425,6 @@ internal override Func GetSyntaxNodesToAnalyzeFilter(SyntaxNod case SymbolKind.NamedType: Debug.Assert((object)declaredSymbol.GetSymbol() == (object)ctor.ContainingSymbol); // Accept nodes that do not match a 'parameter list'/'base arguments list'. - //return (node) => true; return (node) => node != recordDeclaration.ParameterList && !(node.Kind() == SyntaxKind.ArgumentList && node == recordDeclaration.PrimaryConstructorBaseType?.ArgumentList); From 92a6a9c8227bee20ae39b917f90892d375402d9e Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 31 Mar 2021 12:17:34 -0700 Subject: [PATCH 3/8] Add public API --- .../Compilation/SyntaxTreeSemanticModel.cs | 1 + .../Symbols/Metadata/PE/PENamedTypeSymbol.cs | 3 +- .../Symbols/PublicModel/TypeSymbol.cs | 2 +- .../Semantic/Semantics/RecordStructTests.cs | 174 +++++++++++++++++- 4 files changed, 169 insertions(+), 11 deletions(-) diff --git a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs index 7dc80d42e2600..0f927761f852d 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs @@ -1094,6 +1094,7 @@ private MemberSemanticModel CreateMemberModel(CSharpSyntaxNode node) case SyntaxKind.RecordDeclaration: { SynthesizedRecordConstructor symbol = TryGetSynthesizedRecordConstructor((RecordDeclarationSyntax)node); + if (symbol is null) { return null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs index b08517dc3976b..9031d7e2a0c23 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs @@ -565,8 +565,7 @@ internal override bool IsRecord } } - // Is there a way to recognize a record struct from PE? - // Tracked by https://github.com/dotnet/roslyn/issues/52233 + // Record structs get erased when emitted to metadata internal override bool IsRecordStruct => false; public override Accessibility DeclaredAccessibility diff --git a/src/Compilers/CSharp/Portable/Symbols/PublicModel/TypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/PublicModel/TypeSymbol.cs index 5b63a486582ca..476a5444dc0df 100644 --- a/src/Compilers/CSharp/Portable/Symbols/PublicModel/TypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/PublicModel/TypeSymbol.cs @@ -172,6 +172,6 @@ ImmutableArray ITypeSymbol.ToMinimalDisplayParts(SemanticMode bool ITypeSymbol.IsReadOnly => UnderlyingTypeSymbol.IsReadOnly; - bool ITypeSymbol.IsRecord => UnderlyingTypeSymbol.IsRecord; + bool ITypeSymbol.IsRecord => UnderlyingTypeSymbol.IsRecord || UnderlyingTypeSymbol.IsRecordStruct; } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index f62a20273cfd7..96dac0264f6ee 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Symbols.Metadata.PE; +using Microsoft.CodeAnalysis.CSharp.Symbols.Retargeting; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Diagnostics; @@ -423,19 +425,175 @@ public void TypeDeclaration_IsStruct() var src = @" record struct Point(int x, int y); "; + var comp = CreateCompilation(src); + comp.VerifyDiagnostics(); + + CompileAndVerify(comp, symbolValidator: validateModule, sourceSymbolValidator: validateModule); + + static void validateModule(ModuleSymbol module) + { + var isSourceSymbol = module is SourceModuleSymbol; + + var point = module.GlobalNamespace.GetTypeMember("Point"); + Assert.True(point.IsValueType); + Assert.False(point.IsReferenceType); + Assert.False(point.IsRecord); + Assert.Equal(TypeKind.Struct, point.TypeKind); + Assert.Equal(SpecialType.System_ValueType, point.BaseTypeNoUseSiteDiagnostics.SpecialType); + Assert.True(SyntaxFacts.IsTypeDeclaration(SyntaxKind.RecordStructDeclaration)); + + if (isSourceSymbol) + { + Assert.True(point is SourceNamedTypeSymbol); + Assert.True(point.IsRecordStruct); + Assert.True(point.GetPublicSymbol().IsRecord); + } + else + { + Assert.True(point is PENamedTypeSymbol); + Assert.False(point.IsRecordStruct); + Assert.False(point.GetPublicSymbol().IsRecord); + } + } + } + [Fact] + public void IsRecord_Generic() + { + var src = @" +record struct Point(T x, T y); +"; var comp = CreateCompilation(src); comp.VerifyDiagnostics(); - var point = comp.GlobalNamespace.GetTypeMember("Point"); - Assert.True(point.IsValueType); - Assert.False(point.IsReferenceType); - Assert.False(point.IsRecord); + CompileAndVerify(comp, symbolValidator: validateModule, sourceSymbolValidator: validateModule); + + static void validateModule(ModuleSymbol module) + { + var isSourceSymbol = module is SourceModuleSymbol; + + var point = module.GlobalNamespace.GetTypeMember("Point"); + Assert.True(point.IsValueType); + Assert.False(point.IsReferenceType); + Assert.False(point.IsRecord); + Assert.Equal(TypeKind.Struct, point.TypeKind); + Assert.Equal(SpecialType.System_ValueType, point.BaseTypeNoUseSiteDiagnostics.SpecialType); + Assert.True(SyntaxFacts.IsTypeDeclaration(SyntaxKind.RecordStructDeclaration)); + + if (isSourceSymbol) + { + Assert.True(point is SourceNamedTypeSymbol); + Assert.True(point.IsRecordStruct); + Assert.True(point.GetPublicSymbol().IsRecord); + } + else + { + Assert.True(point is PENamedTypeSymbol); + Assert.False(point.IsRecordStruct); + Assert.False(point.GetPublicSymbol().IsRecord); + } + } + } + + [Fact] + public void IsRecord_Retargeting() + { + var src = @" +public record struct Point(int x, int y); +"; + var comp = CreateCompilation(src, targetFramework: TargetFramework.Mscorlib40); + var comp2 = CreateCompilation("", targetFramework: TargetFramework.Mscorlib46, references: new[] { comp.ToMetadataReference() }); + var point = comp2.GlobalNamespace.GetTypeMember("Point"); + + Assert.Equal("Point", point.ToTestDisplayString()); + Assert.IsType(point); Assert.True(point.IsRecordStruct); - Assert.Equal(TypeKind.Struct, point.TypeKind); - Assert.Equal(SpecialType.System_ValueType, point.BaseTypeNoUseSiteDiagnostics.SpecialType); + Assert.True(point.GetPublicSymbol().IsRecord); + } + + [Fact] + public void IsRecord_AnonymousType() + { + var src = @" +class C +{ + void M() + { + var x = new { X = 1 }; + } +} +"; + var comp = CreateCompilation(src); + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var creation = tree.GetRoot().DescendantNodes().OfType().Single(); + var type = model.GetTypeInfo(creation).Type!; + + Assert.Equal("", type.ToTestDisplayString()); + Assert.IsType(((Symbols.PublicModel.NonErrorNamedTypeSymbol)type).UnderlyingNamedTypeSymbol); + Assert.False(type.IsRecord); + } + + [Fact] + public void IsRecord_ErrorType() + { + var src = @" +class C +{ + Error M() => throw null; +} +"; + var comp = CreateCompilation(src); + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var method = tree.GetRoot().DescendantNodes().OfType().Single(); + var type = model.GetDeclaredSymbol(method)!.ReturnType; + + Assert.Equal("Error", type.ToTestDisplayString()); + Assert.IsType(((Symbols.PublicModel.ErrorTypeSymbol)type).UnderlyingNamedTypeSymbol); + Assert.False(type.IsRecord); + } + + [Fact] + public void IsRecord_Pointer() + { + var src = @" +class C +{ + int* M() => throw null; +} +"; + var comp = CreateCompilation(src, options: TestOptions.UnsafeReleaseDll); + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var method = tree.GetRoot().DescendantNodes().OfType().Single(); + var type = model.GetDeclaredSymbol(method)!.ReturnType; + + Assert.Equal("System.Int32*", type.ToTestDisplayString()); + Assert.IsType(((Symbols.PublicModel.PointerTypeSymbol)type).UnderlyingTypeSymbol); + Assert.False(type.IsRecord); + } + + [Fact] + public void IsRecord_Dynamic() + { + var src = @" +class C +{ + void M(dynamic d) + { + } +} +"; + var comp = CreateCompilation(src); + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree, ignoreAccessibility: false); + var method = tree.GetRoot().DescendantNodes().OfType().Single(); + var type = model.GetDeclaredSymbol(method)!.GetParameterType(0); - Assert.True(SyntaxFacts.IsTypeDeclaration(SyntaxKind.RecordStructDeclaration)); + Assert.Equal("dynamic", type.ToTestDisplayString()); + Assert.IsType(((Symbols.PublicModel.DynamicTypeSymbol)type).UnderlyingTypeSymbol); + Assert.False(type.IsRecord); } [Fact] @@ -5189,7 +5347,7 @@ record struct R(R X) } [Fact] - public void GetDeclaredSymbolOnFieldInitializer() + public void GetDeclaredSymbolOnPropertyInitializer() { var src = @" record struct R(int I) From 8700ac6b9ec761ea98075423f5acd41bfd10612f Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 31 Mar 2021 13:06:04 -0700 Subject: [PATCH 4/8] Address feedback --- .../Semantic/Semantics/RecordStructTests.cs | 102 ++++++++++++++++-- .../Test/Semantic/Semantics/RecordTests.cs | 4 +- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index 96dac0264f6ee..6fc0503ea9311 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -5316,7 +5316,7 @@ record struct C1 } [Fact] - public void AmbigCtor_WithFieldInitializer() + public void AmbigCtor_WithPropertyInitializer() { var src = @" record struct R(R X) @@ -5380,6 +5380,7 @@ public void AnalyzerActions_01() record struct A([Attr1]int X = 0) : I1 { private int M() => 3; + A(string S) : this(4) => throw null; } interface I1 {} @@ -5398,6 +5399,11 @@ class Attr1 : System.Attribute {} Assert.Equal(1, analyzer.FireCount5); Assert.Equal(1, analyzer.FireCountParameterListAPrimaryCtor); Assert.Equal(1, analyzer.FireCount7); + Assert.Equal(1, analyzer.FireCountConstructorDeclaration); + Assert.Equal(1, analyzer.FireCountStringParameterList); + Assert.Equal(1, analyzer.FireCountThisConstructorInitializer); + Assert.Equal(1, analyzer.FireCount11); + Assert.Equal(1, analyzer.FireCount12); } private class AnalyzerActions_01_Analyzer : DiagnosticAnalyzer @@ -5410,6 +5416,11 @@ private class AnalyzerActions_01_Analyzer : DiagnosticAnalyzer public int FireCount5; public int FireCountParameterListAPrimaryCtor; public int FireCount7; + public int FireCountConstructorDeclaration; + public int FireCountStringParameterList; + public int FireCountThisConstructorInitializer; + public int FireCount11; + public int FireCount12; private static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); @@ -5422,13 +5433,14 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(Handle1, SyntaxKind.NumericLiteralExpression); context.RegisterSyntaxNodeAction(Handle2, SyntaxKind.EqualsValueClause); context.RegisterSyntaxNodeAction(Fail, SyntaxKind.BaseConstructorInitializer); - context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ConstructorDeclaration); + context.RegisterSyntaxNodeAction(Handle3, SyntaxKind.ThisConstructorInitializer); + context.RegisterSyntaxNodeAction(Handle4, SyntaxKind.ConstructorDeclaration); context.RegisterSyntaxNodeAction(Fail, SyntaxKind.PrimaryConstructorBaseType); context.RegisterSyntaxNodeAction(Handle6, SyntaxKind.RecordStructDeclaration); context.RegisterSyntaxNodeAction(Handle7, SyntaxKind.IdentifierName); context.RegisterSyntaxNodeAction(Handle8, SyntaxKind.SimpleBaseType); context.RegisterSyntaxNodeAction(Handle9, SyntaxKind.ParameterList); - context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ArgumentList); + context.RegisterSyntaxNodeAction(Handle10, SyntaxKind.ArgumentList); } protected void Handle1(SyntaxNodeAnalysisContext context) @@ -5445,6 +5457,10 @@ protected void Handle1(SyntaxNodeAnalysisContext context) Interlocked.Increment(ref FireCount7); Assert.Equal("System.Int32 A.M()", context.ContainingSymbol.ToTestDisplayString()); break; + case "4": + Interlocked.Increment(ref FireCount12); + Assert.Equal("A..ctor(System.String S)", context.ContainingSymbol.ToTestDisplayString()); + break; default: Assert.True(false); break; @@ -5471,6 +5487,30 @@ protected void Handle2(SyntaxNodeAnalysisContext context) Assert.Same(equalsValue.SyntaxTree, context.ContainingSymbol!.DeclaringSyntaxReferences.Single().SyntaxTree); } + protected void Handle3(SyntaxNodeAnalysisContext context) + { + var initializer = (ConstructorInitializerSyntax)context.Node; + + switch (initializer.ToString()) + { + case ": this(4)": + Interlocked.Increment(ref FireCountThisConstructorInitializer); + Assert.Equal("A..ctor(System.String S)", context.ContainingSymbol.ToTestDisplayString()); + break; + default: + Assert.True(false); + break; + } + + Assert.Same(initializer.SyntaxTree, context.ContainingSymbol!.DeclaringSyntaxReferences.Single().SyntaxTree); + } + + protected void Handle4(SyntaxNodeAnalysisContext context) + { + Interlocked.Increment(ref FireCountConstructorDeclaration); + Assert.Equal("A..ctor(System.String S)", context.ContainingSymbol.ToTestDisplayString()); + } + protected void Fail(SyntaxNodeAnalysisContext context) { Assert.True(false); @@ -5546,6 +5586,10 @@ protected void Handle9(SyntaxNodeAnalysisContext context) Interlocked.Increment(ref FireCountParameterListAPrimaryCtor); Assert.Equal("A..ctor([System.Int32 X = 0])", context.ContainingSymbol.ToTestDisplayString()); break; + case "(string S)": + Interlocked.Increment(ref FireCountStringParameterList); + Assert.Equal("A..ctor(System.String S)", context.ContainingSymbol.ToTestDisplayString()); + break; case "()": break; default: @@ -5553,6 +5597,22 @@ protected void Handle9(SyntaxNodeAnalysisContext context) break; } } + + protected void Handle10(SyntaxNodeAnalysisContext context) + { + var argumentList = (ArgumentListSyntax)context.Node; + + switch (argumentList.ToString()) + { + case "(4)": + Interlocked.Increment(ref FireCount11); + Assert.Equal("A..ctor(System.String S)", context.ContainingSymbol.ToTestDisplayString()); + break; + default: + Assert.True(false); + break; + } + } } [Fact] @@ -6026,6 +6086,7 @@ public void AnalyzerActions_08() record struct A([Attr1]int X = 0) : I1 { private int M() => 3; + A(string S) : this(4) => throw null; } interface I1 {} @@ -6037,6 +6098,7 @@ interface I1 {} Assert.Equal(1, analyzer.FireCount100); Assert.Equal(1, analyzer.FireCount400); + Assert.Equal(1, analyzer.FireCount500); Assert.Equal(1, analyzer.FireCount0); Assert.Equal(0, analyzer.FireCountRecordStructDeclarationA); @@ -6046,17 +6108,24 @@ interface I1 {} Assert.Equal(1, analyzer.FireCount5); Assert.Equal(0, analyzer.FireCountParameterListAPrimaryCtor); Assert.Equal(1, analyzer.FireCount7); - + Assert.Equal(0, analyzer.FireCountConstructorDeclaration); + Assert.Equal(0, analyzer.FireCountStringParameterList); + Assert.Equal(0, analyzer.FireCountThisConstructorInitializer); + Assert.Equal(1, analyzer.FireCount11); + Assert.Equal(1, analyzer.FireCount12); Assert.Equal(1, analyzer.FireCount1000); Assert.Equal(1, analyzer.FireCount4000); + Assert.Equal(1, analyzer.FireCount5000); } private class AnalyzerActions_08_Analyzer : AnalyzerActions_01_Analyzer { public int FireCount100; public int FireCount400; + public int FireCount500; public int FireCount1000; public int FireCount4000; + public int FireCount5000; private static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor("XY0000", "Test", "Test", "Test", DiagnosticSeverity.Warning, true, "Test", "Test"); @@ -6074,7 +6143,6 @@ private void Handle(CodeBlockStartAnalysisContext context) switch (context.OwningSymbol.ToTestDisplayString()) { case "A..ctor([System.Int32 X = 0])": - switch (context.CodeBlock) { case RecordStructDeclarationSyntax { Identifier: { ValueText: "A" } }: @@ -6096,6 +6164,17 @@ private void Handle(CodeBlockStartAnalysisContext context) break; } break; + case "A..ctor(System.String S)": + switch (context.CodeBlock) + { + case ConstructorDeclarationSyntax { Identifier: { ValueText: "A" } }: + Interlocked.Increment(ref FireCount500); + break; + default: + Assert.True(false); + break; + } + break; default: Assert.True(false); break; @@ -6110,7 +6189,7 @@ private void Handle(CodeBlockStartAnalysisContext context) context.RegisterSyntaxNodeAction(Handle7, SyntaxKind.IdentifierName); context.RegisterSyntaxNodeAction(Handle8, SyntaxKind.SimpleBaseType); context.RegisterSyntaxNodeAction(Handle9, SyntaxKind.ParameterList); - context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ArgumentList); + context.RegisterSyntaxNodeAction(Handle10, SyntaxKind.ArgumentList); context.RegisterCodeBlockEndAction(Handle11); } @@ -6142,6 +6221,17 @@ private void Handle11(CodeBlockAnalysisContext context) break; } break; + case "A..ctor(System.String S)": + switch (context.CodeBlock) + { + case ConstructorDeclarationSyntax { Identifier: { ValueText: "A" } }: + Interlocked.Increment(ref FireCount5000); + break; + default: + Assert.True(false); + break; + } + break; default: Assert.True(false); break; diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index c671becaf547a..2383050eecda3 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -485,7 +485,7 @@ record R3(R3 x) : Base } [Fact, WorkItem(49628, "https://github.com/dotnet/roslyn/issues/49628")] - public void AmbigCtor_WithFieldInitializer() + public void AmbigCtor_WithPropertyInitializer() { // PROTOTYPE(record-structs): ported var src = @" @@ -517,7 +517,7 @@ record R(R X) } [Fact] - public void GetDeclaredSymbolOnFieldInitializer() + public void GetDeclaredSymbolOnPropertyInitializer() { var src = @" record R(int I) From 1299dbaabbef920ea29a7bf8a56e608f4e4506b4 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 31 Mar 2021 13:47:34 -0700 Subject: [PATCH 5/8] nits --- .../CSharp/Test/Semantic/Semantics/RecordStructTests.cs | 3 ++- src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index 6fc0503ea9311..30bfd44ecb964 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -5318,6 +5318,7 @@ record struct C1 [Fact] public void AmbigCtor_WithPropertyInitializer() { + // Scenario causes ambiguous ctor for record class, but not record struct var src = @" record struct R(R X) { @@ -5347,7 +5348,7 @@ record struct R(R X) } [Fact] - public void GetDeclaredSymbolOnPropertyInitializer() + public void GetDeclaredSymbolInPropertyInitializer() { var src = @" record struct R(int I) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index 2383050eecda3..4868149945af5 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -517,7 +517,7 @@ record R(R X) } [Fact] - public void GetDeclaredSymbolOnPropertyInitializer() + public void GetDeclaredSymbolInPropertyInitializer() { var src = @" record R(int I) From 67e324a34931f66e6d400cba538eff1785eef0ae Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 31 Mar 2021 13:50:12 -0700 Subject: [PATCH 6/8] nits --- .../CSharp/Test/Semantic/Semantics/RecordStructTests.cs | 2 +- src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index 30bfd44ecb964..d6e23e48e8af5 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -5348,7 +5348,7 @@ record struct R(R X) } [Fact] - public void GetDeclaredSymbolInPropertyInitializer() + public void GetDeclaredSymbolOnAnOutLocalInPropertyInitializer() { var src = @" record struct R(int I) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index 4868149945af5..a0c21b7a11e32 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -517,7 +517,7 @@ record R(R X) } [Fact] - public void GetDeclaredSymbolInPropertyInitializer() + public void GetDeclaredSymbolOnAnOutLocalInPropertyInitializer() { var src = @" record R(int I) From aa8933870e386432cb9c8f0492f87f33e6ed39f8 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Wed, 31 Mar 2021 14:07:45 -0700 Subject: [PATCH 7/8] Fix test --- .../CSharp/Test/Semantic/Semantics/RecordStructTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index d6e23e48e8af5..44f2c6a0ab5bd 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -6111,7 +6111,7 @@ interface I1 {} Assert.Equal(1, analyzer.FireCount7); Assert.Equal(0, analyzer.FireCountConstructorDeclaration); Assert.Equal(0, analyzer.FireCountStringParameterList); - Assert.Equal(0, analyzer.FireCountThisConstructorInitializer); + Assert.Equal(1, analyzer.FireCountThisConstructorInitializer); Assert.Equal(1, analyzer.FireCount11); Assert.Equal(1, analyzer.FireCount12); Assert.Equal(1, analyzer.FireCount1000); @@ -6184,7 +6184,8 @@ private void Handle(CodeBlockStartAnalysisContext context) context.RegisterSyntaxNodeAction(Handle1, SyntaxKind.NumericLiteralExpression); context.RegisterSyntaxNodeAction(Handle2, SyntaxKind.EqualsValueClause); context.RegisterSyntaxNodeAction(Fail, SyntaxKind.BaseConstructorInitializer); - context.RegisterSyntaxNodeAction(Fail, SyntaxKind.ConstructorDeclaration); + context.RegisterSyntaxNodeAction(Handle3, SyntaxKind.ThisConstructorInitializer); + context.RegisterSyntaxNodeAction(Handle4, SyntaxKind.ConstructorDeclaration); context.RegisterSyntaxNodeAction(Fail, SyntaxKind.PrimaryConstructorBaseType); context.RegisterSyntaxNodeAction(Handle6, SyntaxKind.RecordStructDeclaration); context.RegisterSyntaxNodeAction(Handle7, SyntaxKind.IdentifierName); @@ -6243,7 +6244,6 @@ private void Handle11(CodeBlockAnalysisContext context) [Fact] public void AnalyzerActions_09() { - // PROTOTYPE(record-structs): ported var text1 = @" record A([Attr1(100)]int X = 0) : I1 {} From ac54980186266802d3e46a9478fa0d5cd9ee3887 Mon Sep 17 00:00:00 2001 From: Julien Couvreur Date: Fri, 2 Apr 2021 12:55:12 -0700 Subject: [PATCH 8/8] Address feedback --- .../Test/Semantic/Semantics/RecordStructTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index 44f2c6a0ab5bd..c7d685c07389f 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -429,6 +429,7 @@ record struct Point(int x, int y); comp.VerifyDiagnostics(); CompileAndVerify(comp, symbolValidator: validateModule, sourceSymbolValidator: validateModule); + Assert.True(SyntaxFacts.IsTypeDeclaration(SyntaxKind.RecordStructDeclaration)); static void validateModule(ModuleSymbol module) { @@ -440,7 +441,6 @@ static void validateModule(ModuleSymbol module) Assert.False(point.IsRecord); Assert.Equal(TypeKind.Struct, point.TypeKind); Assert.Equal(SpecialType.System_ValueType, point.BaseTypeNoUseSiteDiagnostics.SpecialType); - Assert.True(SyntaxFacts.IsTypeDeclaration(SyntaxKind.RecordStructDeclaration)); if (isSourceSymbol) { @@ -5345,6 +5345,16 @@ record struct R(R X) Assert.Equal("R X", initializer.ToTestDisplayString()); Assert.Equal(SymbolKind.Parameter, initializer.Kind); Assert.Equal("R..ctor(R X)", initializer.ContainingSymbol.ToTestDisplayString()); + + var src2 = @" +record struct R(R X); +"; + var comp2 = CreateCompilation(src2); + comp2.VerifyEmitDiagnostics( + // (2,19): error CS0523: Struct member 'R.X' of type 'R' causes a cycle in the struct layout + // record struct R(R X); + Diagnostic(ErrorCode.ERR_StructLayoutCycle, "X").WithArguments("R.X", "R").WithLocation(2, 19) + ); } [Fact]