From 6752e31c8096d3a8e15db4aeef20613ac1b9bf89 Mon Sep 17 00:00:00 2001 From: Rikki Gibson Date: Fri, 1 Mar 2019 12:51:33 -0800 Subject: [PATCH] Readonly members syntax and symbol API (#32888) * Add syntax tests for readonly members * Add simple test for readonly struct method * Allow readonly modifier on methods in structs * Add simple readonly class method test * Add SourceMemberMethodSymbol.IsReadOnly * Test readonly static method * Disallow readonly accessors on static properties * Check usages of readonly keyword in property (not accessor) declarations * Add ref-returning readonly method test * Add readonly ref readonly parsing test * Allow readonly modifier on property decls in general. Add more tests. * Add indexer tests * Readonly events on structs * Remove comment * Disallow readonly on events with associated fields * Test static readonly auto properties and destructors * Disallow redundant readonly on accessor. Simplify diagnostics for static readonly property. * Add tests based on feedback * Note that feature checks and corresponding tests are needed --- .../Symbols/Source/SourceEventSymbol.cs | 15 + .../Source/SourceMemberMethodSymbol.cs | 8 + .../Source/SourceOrdinaryMethodSymbol.cs | 10 + .../Source/SourcePropertyAccessorSymbol.cs | 12 +- .../Symbols/Source/SourcePropertySymbol.cs | 12 + .../Semantics/ReadOnlyStructsTests.cs | 502 ++++++++++++++++++ .../Syntax/Parsing/DeclarationParsingTests.cs | 178 +++++++ 7 files changed, 736 insertions(+), 1 deletion(-) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs index ca678e2e5e6..66554b26a32 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs @@ -368,6 +368,11 @@ public sealed override bool IsVirtual get { return (_modifiers & DeclarationModifiers.Virtual) != 0; } } + internal bool IsReadOnly + { + get { return (_modifiers & DeclarationModifiers.ReadOnly) != 0; } + } + public sealed override Accessibility DeclaredAccessibility { get { return ModifierUtils.EffectiveAccessibility(_modifiers); } @@ -435,6 +440,11 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool expli } } + if (this.ContainingType.IsStructType()) + { + allowedModifiers |= DeclarationModifiers.ReadOnly; + } + if (!isInterface) { allowedModifiers |= DeclarationModifiers.Extern; @@ -468,6 +478,11 @@ protected void CheckModifiersAndType(DiagnosticBag diagnostics) // A static member '{0}' cannot be marked as override, virtual, or abstract diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, this); } + else if (IsReadOnly && (IsStatic || HasAssociatedField)) + { + // The modifier '{0}' is not valid for this item + diagnostics.Add(ErrorCode.ERR_BadMemberFlag, location, SyntaxFacts.GetText(SyntaxKind.ReadOnlyKeyword)); + } else if (IsOverride && (IsNew || IsVirtual)) { // A member '{0}' marked as override cannot be marked as new or virtual diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs index a110cbc67f8..07f84a46584 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs @@ -509,6 +509,14 @@ public sealed override bool IsAsync } } + internal bool IsReadOnly + { + get + { + return (this.DeclarationModifiers & DeclarationModifiers.ReadOnly) != 0; + } + } + internal sealed override Cci.CallingConvention CallingConvention { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs index 8be06acf4ac..99ff44e0f87 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceOrdinaryMethodSymbol.cs @@ -772,6 +772,11 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, MethodKind allowedModifiers |= DeclarationModifiers.Extern | DeclarationModifiers.Async; } + if (ContainingType.IsStructType()) + { + allowedModifiers |= DeclarationModifiers.ReadOnly; + } + var mods = ModifierUtils.MakeAndCheckNontypeMemberModifiers(modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors); this.CheckUnsafeModifier(mods, diagnostics); @@ -933,6 +938,11 @@ private void CheckModifiers(bool hasBody, Location location, DiagnosticBag diagn // The modifier '{0}' is not valid for this item diagnostics.Add(ErrorCode.ERR_BadMemberFlag, location, SyntaxFacts.GetText(SyntaxKind.VirtualKeyword)); } + else if (IsStatic && IsReadOnly) + { + // The modifier '{0}' is not valid for this item + diagnostics.Add(ErrorCode.ERR_BadMemberFlag, location, SyntaxFacts.GetText(SyntaxKind.ReadOnlyKeyword)); + } else if (IsAbstract && !ContainingType.IsAbstract && (ContainingType.TypeKind == TypeKind.Class || ContainingType.TypeKind == TypeKind.Submission)) { // '{0}' is abstract but it is contained in non-abstract class '{1}' diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs index ff4f0ea15b1..18f65e6d2e3 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs @@ -410,7 +410,12 @@ private DeclarationModifiers MakeModifiers(AccessorDeclarationSyntax syntax, Loc const DeclarationModifiers defaultAccess = DeclarationModifiers.None; // Check that the set of modifiers is allowed - const DeclarationModifiers allowedModifiers = DeclarationModifiers.AccessibilityMask; + DeclarationModifiers allowedModifiers = DeclarationModifiers.AccessibilityMask; + if (this.ContainingType.IsStructType() && !_property.HasReadOnlyModifier) + { + allowedModifiers |= DeclarationModifiers.ReadOnly; + } + var mods = ModifierUtils.MakeAndCheckNontypeMemberModifiers(syntax.Modifiers, defaultAccess, allowedModifiers, location, diagnostics, out modifierErrors); // For interface, check there are no accessibility modifiers. @@ -451,6 +456,11 @@ private void CheckModifiers(Location location, bool hasBody, bool isAutoProperty { diagnostics.Add(AccessCheck.GetProtectedMemberInSealedTypeError(ContainingType), location, this); } + else if (IsStatic && IsReadOnly && !_property.HasReadOnlyModifier) + { + // The modifier '{0}' is not valid for this item + diagnostics.Add(ErrorCode.ERR_BadMemberFlag, location, SyntaxFacts.GetText(SyntaxKind.ReadOnlyKeyword)); + } } /// diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs index 0cb938eb51a..0af392ff40b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs @@ -627,6 +627,8 @@ internal bool IsNew get { return (_modifiers & DeclarationModifiers.New) != 0; } } + internal bool HasReadOnlyModifier => (_modifiers & DeclarationModifiers.ReadOnly) != 0; + public override MethodSymbol GetMethod { get { return _getMethod; } @@ -813,6 +815,11 @@ private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, bool isExp } } + if (ContainingType.IsStructType()) + { + allowedModifiers |= DeclarationModifiers.ReadOnly; + } + if (!isInterface) { allowedModifiers |= @@ -890,6 +897,11 @@ private void CheckModifiers(Location location, bool isIndexer, DiagnosticBag dia // A static member '{0}' cannot be marked as override, virtual, or abstract diagnostics.Add(ErrorCode.ERR_StaticNotVirtual, location, this); } + else if (IsStatic && HasReadOnlyModifier) + { + // The modifier '{0}' is not valid for this item + diagnostics.Add(ErrorCode.ERR_BadMemberFlag, location, SyntaxFacts.GetText(SyntaxKind.ReadOnlyKeyword)); + } else if (IsOverride && (IsNew || IsVirtual)) { // A member '{0}' marked as override cannot be marked as new or virtual diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/ReadOnlyStructsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/ReadOnlyStructsTests.cs index 301eb2f3645..43a5e0360b4 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/ReadOnlyStructsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/ReadOnlyStructsTests.cs @@ -249,5 +249,507 @@ public static void Main() Diagnostic(ErrorCode.ERR_AssgReadonlyStatic2, "s.field").WithArguments("Program.s").WithLocation(8, 9) ); } + + // PROTOTYPE: readonly members features should require C# 8.0 or greater + + [Fact] + public void ReadOnlyStructMethod() + { + var csharp = @" +public struct S +{ + public int i; + public readonly int M() + { + return i; + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + + var method = (SourceMemberMethodSymbol)comp.GetMember("S").GetMember("M"); + // PROTOTYPE: add `public abstract bool IsReadOnly` to MethodSymbol and implement in subtypes? + Assert.True(method.IsReadOnly); + } + + [Fact] + public void ReadOnlyClass() + { + var csharp = @" +using System; + +public readonly class C +{ + public readonly int M() => 42; + public readonly int P { get; set; } + public readonly int this[int i] => i; + public readonly event Action E { add {} remove {} } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,23): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly class C + Diagnostic(ErrorCode.ERR_BadMemberFlag, "C").WithArguments("readonly").WithLocation(4, 23), + // (6,25): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly int M() => 42; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "M").WithArguments("readonly").WithLocation(6, 25), + // (7,25): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly int P { get; set; } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "P").WithArguments("readonly").WithLocation(7, 25), + // (8,25): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly int this[int i] => i; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "this").WithArguments("readonly").WithLocation(8, 25), + // (9,45): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly event Action E { add {} remove {} } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("readonly").WithLocation(9, 45)); + } + + [Fact] + public void ReadOnlyInterface() + { + var csharp = @" +using System; + +public readonly interface I +{ + readonly int M(); + readonly int P { get; set; } + readonly int this[int i] { get; } + readonly event Action E; +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,27): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly interface I + Diagnostic(ErrorCode.ERR_BadMemberFlag, "I").WithArguments("readonly").WithLocation(4, 27), + // (6,18): error CS0106: The modifier 'readonly' is not valid for this item + // readonly int M(); + Diagnostic(ErrorCode.ERR_BadMemberFlag, "M").WithArguments("readonly").WithLocation(6, 18), + // (7,18): error CS0106: The modifier 'readonly' is not valid for this item + // readonly int P { get; set; } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "P").WithArguments("readonly").WithLocation(7, 18), + // (8,18): error CS0106: The modifier 'readonly' is not valid for this item + // readonly int this[int i] { get; } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "this").WithArguments("readonly").WithLocation(8, 18), + // (9,38): error CS0106: The modifier 'readonly' is not valid for this item + // readonly event Action E; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("readonly").WithLocation(9, 38)); + } + + [Fact] + public void ReadOnlyEnum() + { + var csharp = @" +public readonly enum E +{ + readonly A, readonly B +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (2,22): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly enum E + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("readonly").WithLocation(2, 22), + // (3,2): error CS1041: Identifier expected; 'readonly' is a keyword + // { + Diagnostic(ErrorCode.ERR_IdentifierExpectedKW, "").WithArguments("", "readonly").WithLocation(3, 2), + // (4,17): error CS1041: Identifier expected; 'readonly' is a keyword + // readonly A, readonly B; + Diagnostic(ErrorCode.ERR_IdentifierExpectedKW, "readonly").WithArguments("", "readonly").WithLocation(4, 17)); + } + + [Fact] + public void ReadOnlyStructStaticMethod() + { + var csharp = @" +public struct S +{ + public static int i; + public static readonly int M() + { + return i; + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (5,32): error CS0106: The modifier 'readonly' is not valid for this item + // public static readonly int M() + Diagnostic(ErrorCode.ERR_BadMemberFlag, "M").WithArguments("readonly").WithLocation(5, 32)); + } + + [Fact] + public void ReadOnlyStructProperty() + { + var csharp = @" +public struct S +{ + public int i; + public int P + { + readonly get + { + return i; + } + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyStructStaticProperty() + { + var csharp = @" +public struct S +{ + public static int i; + public static int P + { + readonly get + { + return i; + } + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (7,18): error CS0106: The modifier 'readonly' is not valid for this item + // readonly get + Diagnostic(ErrorCode.ERR_BadMemberFlag, "get").WithArguments("readonly").WithLocation(7, 18)); + } + + [Fact] + public void ReadOnlyStructStaticExpressionProperty() + { + var csharp = @" +public struct S +{ + public static int i; + public static readonly int P => i; +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (5,32): error CS0106: The modifier 'readonly' is not valid for this item + // public static readonly int P => i; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "P").WithArguments("readonly").WithLocation(5, 32)); + } + + [Fact] + public void ReadOnlyStructExpressionProperty() + { + var csharp = @" +public struct S +{ + public int i; + public readonly int P => i; +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyStructBlockProperty() + { + var csharp = @" +public struct S +{ + public int i; + public readonly int P { get { return i; } } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyAutoProperty() + { + var csharp = @" +public struct S +{ + public int P1 { readonly get; } + public readonly int P2 { get; } + public int P3 { readonly get; set; } + public int P4 { readonly get; readonly set; } // PROTOTYPE: readonly set on an auto-property should give an error + public readonly int P5 { get; set; } // PROTOTYPE: readonly set on an auto-property should give an error +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyProperty_RedundantReadOnlyAccessor() + { + var csharp = @" +public struct S +{ + public readonly int P { readonly get; } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,38): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly int P { readonly get; } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "get").WithArguments("readonly").WithLocation(4, 38)); + } + + [Fact] + public void ReadOnlyStaticAutoProperty() + { + var csharp = @" +public struct S +{ + public static readonly int P1 { get; set; } + public static int P2 { readonly get; } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,32): error CS0106: The modifier 'readonly' is not valid for this item + // public static readonly int P1 { get; set; } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "P1").WithArguments("readonly").WithLocation(4, 32), + // (5,37): error CS0106: The modifier 'readonly' is not valid for this item + // public static int P2 { readonly get; } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "get").WithArguments("readonly").WithLocation(5, 37)); + } + + [Fact] + public void RefReturningReadOnlyMethod() + { + // PROTOTYPE: would be good to add some more mutation here + // as well as expected diagnostics once that part of the feature is ready. + var csharp = @" +public struct S +{ + private static int f1; + public readonly ref int M1() => ref f1; + + private static readonly int f2; + public readonly ref readonly int M2() => ref f2; + + private static readonly int f3; + public ref readonly int M3() + { + f1++; + return ref f3; + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyConstructor() + { + var csharp = @" +public struct S +{ + static readonly S() { } + public readonly S(int i) { } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,21): error CS0106: The modifier 'readonly' is not valid for this item + // static readonly S() { } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "S").WithArguments("readonly").WithLocation(4, 21), + // (5,21): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly S(int i) { } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "S").WithArguments("readonly").WithLocation(5, 21)); + } + + [Fact] + public void ReadOnlyDestructor() + { + var csharp = @" +public struct S +{ + readonly ~S() { } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,15): error CS0106: The modifier 'readonly' is not valid for this item + // readonly ~S() { } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "S").WithArguments("readonly").WithLocation(4, 15), + // (4,15): error CS0575: Only class types can contain destructors + // readonly ~S() { } + Diagnostic(ErrorCode.ERR_OnlyClassesCanContainDestructors, "S").WithArguments("S.~S()").WithLocation(4, 15)); + } + + [Fact] + public void ReadOnlyOperator() + { + var csharp = @" +public struct S +{ + public static readonly S operator +(S lhs, S rhs) => lhs; + public static readonly explicit operator int(S s) => 42; +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (4,39): error CS0106: The modifier 'readonly' is not valid for this item + // public static readonly S operator +(S lhs, S rhs) => lhs; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "+").WithArguments("readonly").WithLocation(4, 39), + // (5,46): error CS0106: The modifier 'readonly' is not valid for this item + // public static readonly explicit operator int(S s) => 42; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "int").WithArguments("readonly").WithLocation(5, 46)); + } + + [Fact] + public void ReadOnlyDelegate() + { + var csharp = @" +public readonly delegate int Del(); +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (2,30): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly delegate int Del(); + Diagnostic(ErrorCode.ERR_BadMemberFlag, "Del").WithArguments("readonly").WithLocation(2, 30)); + } + + [Fact] + public void ReadOnlyIndexer() + { + var csharp = @" +public struct S1 +{ + public readonly int this[int i] + { + get => 42; + set {} + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyExpressionIndexer() + { + var csharp = @" +public struct S1 +{ + public readonly int this[int i] => 42; +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyGetExpressionIndexer() + { + var csharp = @" +public struct S1 +{ + public int this[int i] + { + readonly get => 42; + readonly set {} + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyFieldLikeEvent() + { + var csharp = @" +using System; + +public struct S1 +{ + public readonly event Action E; + public void M() { E?.Invoke(new EventArgs()); } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (6,45): error CS0106: The modifier 'readonly' is not valid for this item + // public readonly event Action E; + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("readonly").WithLocation(6, 45)); + } + + [Fact] + public void ReadOnlyEventExplicitAddRemove() + { + var csharp = @" +using System; + +public struct S1 +{ + public readonly event Action E + { + add {} + remove {} + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics(); + } + + [Fact] + public void ReadOnlyStaticEvent() + { + var csharp = @" +using System; + +public struct S1 +{ + public static readonly event Action E + { + add {} + remove {} + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (6,52): error CS0106: The modifier 'readonly' is not valid for this item + // public static readonly event Action E + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("readonly").WithLocation(6, 52)); + } + + [Fact] + public void ReadOnlyEventReadOnlyAccessors() + { + var csharp = @" +using System; + +public struct S1 +{ + public event Action E + { + readonly add {} + readonly remove {} + } +} +"; + var comp = CreateCompilation(csharp); + comp.VerifyDiagnostics( + // (8,9): error CS1609: Modifiers cannot be placed on event accessor declarations + // readonly add {} + Diagnostic(ErrorCode.ERR_NoModifiersOnAccessor, "readonly").WithLocation(8, 9), + // (9,9): error CS1609: Modifiers cannot be placed on event accessor declarations + // readonly remove {} + Diagnostic(ErrorCode.ERR_NoModifiersOnAccessor, "readonly").WithLocation(9, 9)); + } } } diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs index d29c2237fdd..a575e293afd 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/DeclarationParsingTests.cs @@ -2729,6 +2729,184 @@ public void TestClassMethodWithPartial() Assert.Equal(SyntaxKind.None, ms.SemicolonToken.Kind()); } + [Fact] + public void TestStructMethodWithReadonly() + { + var text = "struct a { readonly void M() { } }"; + var file = this.ParseFile(text); + + Assert.NotNull(file); + Assert.Equal(1, file.Members.Count); + Assert.Equal(text, file.ToString()); + Assert.Equal(0, file.Errors().Length); + + Assert.Equal(SyntaxKind.StructDeclaration, file.Members[0].Kind()); + var structDecl = (TypeDeclarationSyntax)file.Members[0]; + Assert.Equal(0, structDecl.AttributeLists.Count); + Assert.Equal(0, structDecl.Modifiers.Count); + Assert.NotNull(structDecl.Keyword); + Assert.Equal(SyntaxKind.StructKeyword, structDecl.Keyword.Kind()); + Assert.NotNull(structDecl.Identifier); + Assert.Equal("a", structDecl.Identifier.ToString()); + Assert.Null(structDecl.BaseList); + Assert.Equal(0, structDecl.ConstraintClauses.Count); + Assert.NotNull(structDecl.OpenBraceToken); + Assert.NotNull(structDecl.CloseBraceToken); + + Assert.Equal(1, structDecl.Members.Count); + + Assert.Equal(SyntaxKind.MethodDeclaration, structDecl.Members[0].Kind()); + var ms = (MethodDeclarationSyntax)structDecl.Members[0]; + Assert.Equal(0, ms.AttributeLists.Count); + Assert.Equal(1, ms.Modifiers.Count); + Assert.Equal(SyntaxKind.ReadOnlyKeyword, ms.Modifiers[0].Kind()); + Assert.NotNull(ms.ReturnType); + Assert.Equal("void", ms.ReturnType.ToString()); + Assert.NotNull(ms.Identifier); + Assert.Equal("M", ms.Identifier.ToString()); + Assert.NotNull(ms.ParameterList.OpenParenToken); + Assert.False(ms.ParameterList.OpenParenToken.IsMissing); + Assert.Equal(0, ms.ParameterList.Parameters.Count); + Assert.NotNull(ms.ParameterList.CloseParenToken); + Assert.False(ms.ParameterList.CloseParenToken.IsMissing); + Assert.Equal(0, ms.ConstraintClauses.Count); + Assert.NotNull(ms.Body); + Assert.NotEqual(SyntaxKind.None, ms.Body.OpenBraceToken.Kind()); + Assert.NotEqual(SyntaxKind.None, ms.Body.CloseBraceToken.Kind()); + Assert.Equal(SyntaxKind.None, ms.SemicolonToken.Kind()); + } + + [Fact] + public void TestReadOnlyRefReturning() + { + var text = "struct a { readonly ref readonly int M() { } }"; + var file = this.ParseFile(text); + + Assert.NotNull(file); + Assert.Equal(1, file.Members.Count); + Assert.Equal(text, file.ToString()); + Assert.Equal(0, file.Errors().Length); + + Assert.Equal(SyntaxKind.StructDeclaration, file.Members[0].Kind()); + var structDecl = (TypeDeclarationSyntax)file.Members[0]; + Assert.Equal(0, structDecl.AttributeLists.Count); + Assert.Equal(0, structDecl.Modifiers.Count); + Assert.NotNull(structDecl.Keyword); + Assert.Equal(SyntaxKind.StructKeyword, structDecl.Keyword.Kind()); + Assert.NotNull(structDecl.Identifier); + Assert.Equal("a", structDecl.Identifier.ToString()); + Assert.Null(structDecl.BaseList); + Assert.Equal(0, structDecl.ConstraintClauses.Count); + Assert.NotNull(structDecl.OpenBraceToken); + Assert.NotNull(structDecl.CloseBraceToken); + + Assert.Equal(1, structDecl.Members.Count); + + Assert.Equal(SyntaxKind.MethodDeclaration, structDecl.Members[0].Kind()); + var ms = (MethodDeclarationSyntax)structDecl.Members[0]; + Assert.Equal(0, ms.AttributeLists.Count); + Assert.Equal(1, ms.Modifiers.Count); + Assert.Equal(SyntaxKind.ReadOnlyKeyword, ms.Modifiers[0].Kind()); + Assert.Equal(SyntaxKind.RefType, ms.ReturnType.Kind()); + var rt = (RefTypeSyntax)ms.ReturnType; + Assert.Equal(SyntaxKind.RefKeyword, rt.RefKeyword.Kind()); + Assert.Equal(SyntaxKind.ReadOnlyKeyword, rt.ReadOnlyKeyword.Kind()); + Assert.Equal("int", rt.Type.ToString()); + Assert.NotNull(ms.Identifier); + Assert.Equal("M", ms.Identifier.ToString()); + Assert.NotNull(ms.ParameterList.OpenParenToken); + Assert.False(ms.ParameterList.OpenParenToken.IsMissing); + Assert.Equal(0, ms.ParameterList.Parameters.Count); + Assert.NotNull(ms.ParameterList.CloseParenToken); + Assert.False(ms.ParameterList.CloseParenToken.IsMissing); + Assert.Equal(0, ms.ConstraintClauses.Count); + Assert.NotNull(ms.Body); + Assert.NotEqual(SyntaxKind.None, ms.Body.OpenBraceToken.Kind()); + Assert.NotEqual(SyntaxKind.None, ms.Body.CloseBraceToken.Kind()); + Assert.Equal(SyntaxKind.None, ms.SemicolonToken.Kind()); + } + + [Fact] + public void TestStructExpressionPropertyWithReadonly() + { + var text = "struct a { readonly int M => 42; }"; + var file = this.ParseFile(text); + + Assert.NotNull(file); + Assert.Equal(1, file.Members.Count); + Assert.Equal(text, file.ToString()); + Assert.Equal(0, file.Errors().Length); + + Assert.Equal(SyntaxKind.StructDeclaration, file.Members[0].Kind()); + var structDecl = (TypeDeclarationSyntax)file.Members[0]; + Assert.Equal(0, structDecl.AttributeLists.Count); + Assert.Equal(0, structDecl.Modifiers.Count); + Assert.NotNull(structDecl.Keyword); + Assert.Equal(SyntaxKind.StructKeyword, structDecl.Keyword.Kind()); + Assert.NotNull(structDecl.Identifier); + Assert.Equal("a", structDecl.Identifier.ToString()); + Assert.Null(structDecl.BaseList); + Assert.Equal(0, structDecl.ConstraintClauses.Count); + Assert.NotNull(structDecl.OpenBraceToken); + Assert.NotNull(structDecl.CloseBraceToken); + + Assert.Equal(1, structDecl.Members.Count); + + Assert.Equal(SyntaxKind.PropertyDeclaration, structDecl.Members[0].Kind()); + var propertySyntax = (PropertyDeclarationSyntax)structDecl.Members[0]; + Assert.Equal(0, propertySyntax.AttributeLists.Count); + Assert.Equal(1, propertySyntax.Modifiers.Count); + Assert.Equal(SyntaxKind.ReadOnlyKeyword, propertySyntax.Modifiers[0].Kind()); + Assert.NotNull(propertySyntax.Type); + Assert.Equal("int", propertySyntax.Type.ToString()); + Assert.NotNull(propertySyntax.Identifier); + Assert.Equal("M", propertySyntax.Identifier.ToString()); + Assert.NotNull(propertySyntax.ExpressionBody); + Assert.NotEqual(SyntaxKind.None, propertySyntax.ExpressionBody.ArrowToken.Kind()); + Assert.NotNull(propertySyntax.ExpressionBody.Expression); + Assert.Equal(SyntaxKind.SemicolonToken, propertySyntax.SemicolonToken.Kind()); + } + + [Fact] + public void TestStructGetterPropertyWithReadonly() + { + var text = "struct a { int P { readonly get { return 42; } } }"; + var file = this.ParseFile(text); + + Assert.NotNull(file); + Assert.Equal(1, file.Members.Count); + Assert.Equal(text, file.ToString()); + Assert.Equal(0, file.Errors().Length); + + Assert.Equal(SyntaxKind.StructDeclaration, file.Members[0].Kind()); + var structDecl = (TypeDeclarationSyntax)file.Members[0]; + Assert.Equal(0, structDecl.AttributeLists.Count); + Assert.Equal(0, structDecl.Modifiers.Count); + Assert.NotNull(structDecl.Keyword); + Assert.Equal(SyntaxKind.StructKeyword, structDecl.Keyword.Kind()); + Assert.NotNull(structDecl.Identifier); + Assert.Equal("a", structDecl.Identifier.ToString()); + Assert.Null(structDecl.BaseList); + Assert.Equal(0, structDecl.ConstraintClauses.Count); + Assert.NotNull(structDecl.OpenBraceToken); + Assert.NotNull(structDecl.CloseBraceToken); + + Assert.Equal(1, structDecl.Members.Count); + + Assert.Equal(SyntaxKind.PropertyDeclaration, structDecl.Members[0].Kind()); + var propertySyntax = (PropertyDeclarationSyntax)structDecl.Members[0]; + Assert.Equal(0, propertySyntax.AttributeLists.Count); + Assert.Equal(0, propertySyntax.Modifiers.Count); + Assert.NotNull(propertySyntax.Type); + Assert.Equal("int", propertySyntax.Type.ToString()); + Assert.NotNull(propertySyntax.Identifier); + Assert.Equal("P", propertySyntax.Identifier.ToString()); + var accessors = propertySyntax.AccessorList.Accessors; + Assert.Equal(1, accessors.Count); + Assert.Equal(1, accessors[0].Modifiers.Count); + Assert.Equal(SyntaxKind.ReadOnlyKeyword, accessors[0].Modifiers[0].Kind()); + } + [Fact] public void TestClassMethodWithParameter() { -- GitLab