diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index 17f7d6a470246..926b46346b6a8 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ *REMOVED*Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Name.get -> Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax! +*REMOVED*static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.RecordDeclaration(Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax! typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax! parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax! baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxList members) -> Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax! +static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.RecordDeclaration(Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax? typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax? parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax? baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxList members) -> Microsoft.CodeAnalysis.CSharp.Syntax.RecordDeclarationSyntax! static Microsoft.CodeAnalysis.CSharpExtensions.ContainsDirective(this Microsoft.CodeAnalysis.SyntaxNode! node, Microsoft.CodeAnalysis.CSharp.SyntaxKind kind) -> bool Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.Name.get -> Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax? Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax.NamespaceOrType.get -> Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax! diff --git a/src/Compilers/CSharp/Portable/Syntax/RecordDeclarationSyntax.cs b/src/Compilers/CSharp/Portable/Syntax/RecordDeclarationSyntax.cs index d5e6554b9eaa5..2c41983144feb 100644 --- a/src/Compilers/CSharp/Portable/Syntax/RecordDeclarationSyntax.cs +++ b/src/Compilers/CSharp/Portable/Syntax/RecordDeclarationSyntax.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Syntax { @@ -32,15 +33,19 @@ public static RecordDeclarationSyntax RecordDeclaration(SyntaxList attributeLists, SyntaxTokenList modifiers, SyntaxToken keyword, SyntaxToken identifier, - TypeParameterListSyntax typeParameterList, ParameterListSyntax parameterList, BaseListSyntax baseList, SyntaxList constraintClauses, SyntaxList members) + TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, SyntaxList constraintClauses, SyntaxList members) { + var semicolonToken = members.Count == 0 ? Token(SyntaxKind.SemicolonToken) : default; + var openBraceToken = members.Count == 0 ? default : Token(SyntaxKind.OpenBraceToken); + var closeBraceToken = members.Count == 0 ? default : Token(SyntaxKind.CloseBraceToken); + return RecordDeclaration(SyntaxKind.RecordDeclaration, attributeLists, modifiers, keyword, classOrStructKeyword: default, identifier, - typeParameterList, parameterList, baseList, constraintClauses, openBraceToken: default, members, closeBraceToken: default, semicolonToken: default); + typeParameterList, parameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); } public static RecordDeclarationSyntax RecordDeclaration(SyntaxToken keyword, string identifier) { - return RecordDeclaration(keyword, SyntaxFactory.Identifier(identifier)); + return RecordDeclaration(keyword, Identifier(identifier)); } public static RecordDeclarationSyntax RecordDeclaration(SyntaxToken keyword, SyntaxToken identifier) diff --git a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxFactoryTests.cs b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxFactoryTests.cs index 12f587a03e812..7b014369bfcd8 100644 --- a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxFactoryTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxFactoryTests.cs @@ -585,6 +585,25 @@ public void TestParenthesizedLambdaNoParameterList_ExpressionBody() Assert.Equal(fullySpecified.ToFullString(), lambda.ToFullString()); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67335")] + public void TestCreateRecordWithoutMembers() + { + var record = SyntaxFactory.RecordDeclaration( + default, default, SyntaxFactory.Token(SyntaxKind.RecordKeyword), SyntaxFactory.Identifier("R"), null, null, null, default, default); + Assert.NotNull(record); + Assert.Equal("record R;", record.NormalizeWhitespace().ToFullString()); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67335")] + public void TestCreateRecordWithMembers() + { + var record = SyntaxFactory.RecordDeclaration( + default, default, SyntaxFactory.Token(SyntaxKind.RecordKeyword), SyntaxFactory.Identifier("R"), null, null, null, default, + SyntaxFactory.SingletonList(SyntaxFactory.ParseMemberDeclaration("private int i;"))); + Assert.NotNull(record); + Assert.Equal("record R\r\n{\r\n private int i;\r\n}", record.NormalizeWhitespace().ToFullString()); + } + [Fact] public void TestParseNameWithOptions() { diff --git a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs index 2b20979596de4..417904d737b70 100644 --- a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs +++ b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs @@ -670,6 +670,7 @@ private static AccessorDeclarationSyntax WithoutBody(AccessorDeclarationSyntax a => accessor.Body != null ? accessor.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)).WithBody(null) : accessor; private protected override SyntaxNode ClassDeclaration( + bool isRecord, string name, IEnumerable? typeParameters, Accessibility accessibility, @@ -678,35 +679,26 @@ private protected override SyntaxNode ClassDeclaration( IEnumerable? interfaceTypes, IEnumerable? members) { - List? baseTypes = null; + using var _ = ArrayBuilder.GetInstance(out var baseTypes); if (baseType != null || interfaceTypes != null) { - baseTypes = new List(); - if (baseType != null) - { baseTypes.Add(SyntaxFactory.SimpleBaseType((TypeSyntax)baseType)); - } if (interfaceTypes != null) - { baseTypes.AddRange(interfaceTypes.Select(i => SyntaxFactory.SimpleBaseType((TypeSyntax)i))); - } - - if (baseTypes.Count == 0) - { - baseTypes = null; - } } - return SyntaxFactory.ClassDeclaration( - default, - AsModifierList(accessibility, modifiers, SyntaxKind.ClassDeclaration), - name.ToIdentifierToken(), - AsTypeParameterList(typeParameters), - baseTypes != null ? SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(baseTypes)) : null, - default, - this.AsClassMembers(name, members)); + var kind = isRecord ? SyntaxKind.RecordDeclaration : SyntaxKind.ClassDeclaration; + var modifierList = AsModifierList(accessibility, modifiers, kind); + var nameToken = name.ToIdentifierToken(); + var typeParameterList = AsTypeParameterList(typeParameters); + var baseTypeList = baseTypes.Count > 0 ? SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(baseTypes)) : null; + var typeMembers = this.AsClassMembers(name, members); + + return isRecord + ? SyntaxFactory.RecordDeclaration(default, modifierList, SyntaxFactory.Token(SyntaxKind.RecordKeyword), nameToken, typeParameterList, null, baseTypeList, default, typeMembers) + : SyntaxFactory.ClassDeclaration(default, modifierList, nameToken, typeParameterList, baseTypeList, default, typeMembers); } private SyntaxList AsClassMembers(string className, IEnumerable? members) @@ -734,6 +726,7 @@ private SyntaxList AsClassMembers(string className, IEn } private protected override SyntaxNode StructDeclaration( + bool isRecord, string name, IEnumerable? typeParameters, Accessibility accessibility, @@ -742,19 +735,17 @@ private protected override SyntaxNode StructDeclaration( IEnumerable? members) { var itypes = interfaceTypes?.Select(i => (BaseTypeSyntax)SyntaxFactory.SimpleBaseType((TypeSyntax)i)).ToList(); - if (itypes?.Count == 0) - { - itypes = null; - } - return SyntaxFactory.StructDeclaration( - default, - AsModifierList(accessibility, modifiers, SyntaxKind.StructDeclaration), - name.ToIdentifierToken(), - AsTypeParameterList(typeParameters), - itypes != null ? SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(itypes)) : null, - default, - this.AsClassMembers(name, members)); + var kind = isRecord ? SyntaxKind.RecordStructDeclaration : SyntaxKind.StructDeclaration; + var modifierList = AsModifierList(accessibility, modifiers, kind); + var nameToken = name.ToIdentifierToken(); + var typeParameterList = AsTypeParameterList(typeParameters); + var baseTypeList = itypes?.Count > 0 ? SyntaxFactory.BaseList(SyntaxFactory.SeparatedList(itypes)) : null; + var structMembers = this.AsClassMembers(name, members); + + return isRecord + ? SyntaxFactory.RecordDeclaration(default, modifierList, SyntaxFactory.Token(SyntaxKind.RecordKeyword), nameToken, typeParameterList, null, baseTypeList, default, structMembers).WithClassOrStructKeyword(SyntaxFactory.Token(SyntaxKind.StructKeyword)) + : SyntaxFactory.StructDeclaration(default, modifierList, nameToken, typeParameterList, baseTypeList, default, structMembers); } private protected override SyntaxNode InterfaceDeclaration( diff --git a/src/Workspaces/CSharpTest/CodeGeneration/SyntaxGeneratorTests.cs b/src/Workspaces/CSharpTest/CodeGeneration/SyntaxGeneratorTests.cs index 1402a6f376c24..1f41f5ba11292 100644 --- a/src/Workspaces/CSharpTest/CodeGeneration/SyntaxGeneratorTests.cs +++ b/src/Workspaces/CSharpTest/CodeGeneration/SyntaxGeneratorTests.cs @@ -4482,6 +4482,82 @@ public class C : IDisposable Assert.Equal(expected, elasticOnlyFormatted); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67335")] + public void TestGenerateRecordClass1() + { + var comp = Compile( +@"public record class R;"); + + var symbolR = (INamedTypeSymbol)comp.GlobalNamespace.GetMembers("R").First(); + + VerifySyntax( + Generator.Declaration(symbolR), + """ + public record R : global::System.Object, global::System.IEquatable; + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67335")] + public void TestGenerateRecordClass2() + { + var comp = Compile( +@"public record class R(int i) { public int I => i; }"); + + var symbolR = (INamedTypeSymbol)comp.GlobalNamespace.GetMembers("R").First(); + + VerifySyntax( + Generator.Declaration(symbolR), + """ + public record R : global::System.Object, global::System.IEquatable + { + public R(global::System.Int32 i) + { + } + + public global::System.Int32 i { get; set; } + public global::System.Int32 I { get; } + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67335")] + public void TestGenerateRecordStruct1() + { + var comp = Compile( +@"public record struct R;"); + + var symbolR = (INamedTypeSymbol)comp.GlobalNamespace.GetMembers("R").First(); + + VerifySyntax( + Generator.Declaration(symbolR), + """ + public record struct R : global::System.IEquatable; + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/67335")] + public void TestGenerateRecordStruct2() + { + var comp = Compile( +@"public readonly record struct R(int i) { public int I => i; }"); + + var symbolR = (INamedTypeSymbol)comp.GlobalNamespace.GetMembers("R").First(); + + VerifySyntax( + Generator.Declaration(symbolR), + """ + public readonly record struct R : global::System.IEquatable + { + public R(global::System.Int32 i) + { + } + + public global::System.Int32 i { get; set; } + public global::System.Int32 I { get; } + } + """); + } + #endregion #region DeclarationModifiers diff --git a/src/Workspaces/Core/Portable/Editing/SyntaxGenerator.cs b/src/Workspaces/Core/Portable/Editing/SyntaxGenerator.cs index 531103cb892d5..971974d921da6 100644 --- a/src/Workspaces/Core/Portable/Editing/SyntaxGenerator.cs +++ b/src/Workspaces/Core/Portable/Editing/SyntaxGenerator.cs @@ -611,10 +611,11 @@ public SyntaxNode ClassDeclaration( IEnumerable? members = null) { return ClassDeclaration( - name, typeParameters?.Select(n => TypeParameter(n)), accessibility, modifiers, baseType, interfaceTypes, members); + isRecord: false, name, typeParameters?.Select(TypeParameter), accessibility, modifiers, baseType, interfaceTypes, members); } private protected abstract SyntaxNode ClassDeclaration( + bool isRecord, string name, IEnumerable? typeParameters, Accessibility accessibility, @@ -635,10 +636,11 @@ public SyntaxNode StructDeclaration( IEnumerable? members = null) { return StructDeclaration( - name, typeParameters?.Select(n => TypeParameter(n)), accessibility, modifiers, interfaceTypes, members); + isRecord: false, name, typeParameters?.Select(TypeParameter), accessibility, modifiers, interfaceTypes, members); } private protected abstract SyntaxNode StructDeclaration( + bool isRecord, string name, IEnumerable? typeParameters, Accessibility accessibility, @@ -764,63 +766,53 @@ public SyntaxNode Declaration(ISymbol symbol) case SymbolKind.NamedType: var type = (INamedTypeSymbol)symbol; - SyntaxNode? declaration = null; - switch (type.TypeKind) + var declaration = type.TypeKind switch { - case TypeKind.Class: - declaration = ClassDeclaration( + TypeKind.Class => ClassDeclaration( + type.IsRecord, + type.Name, + type.TypeParameters.Select(TypeParameter), + accessibility: type.DeclaredAccessibility, + modifiers: DeclarationModifiers.From(type), + baseType: type.BaseType != null ? TypeExpression(type.BaseType) : null, + interfaceTypes: type.Interfaces.Select(TypeExpression), + members: type.GetMembers().Where(CanBeDeclared).Select(Declaration)), + TypeKind.Struct => StructDeclaration( + type.IsRecord, + type.Name, + type.TypeParameters.Select(TypeParameter), + accessibility: type.DeclaredAccessibility, + modifiers: DeclarationModifiers.From(type), + interfaceTypes: type.Interfaces.Select(TypeExpression), + members: type.GetMembers().Where(CanBeDeclared).Select(Declaration)), + TypeKind.Interface => InterfaceDeclaration( + type.Name, + type.TypeParameters.Select(TypeParameter), + accessibility: type.DeclaredAccessibility, + interfaceTypes: type.Interfaces.Select(TypeExpression), + members: type.GetMembers().Where(CanBeDeclared).Select(Declaration)), + TypeKind.Enum => EnumDeclaration( + type.Name, + underlyingType: type.EnumUnderlyingType is null or { SpecialType: SpecialType.System_Int32 } + ? null + : TypeExpression(type.EnumUnderlyingType.SpecialType), + accessibility: type.DeclaredAccessibility, + members: type.GetMembers().Where(s => s.Kind == SymbolKind.Field).Select(Declaration)), + TypeKind.Delegate => type.GetMembers(WellKnownMemberNames.DelegateInvokeName) is [IMethodSymbol invoke, ..] + ? DelegateDeclaration( type.Name, - type.TypeParameters.Select(p => TypeParameter(p)), + typeParameters: type.TypeParameters.Select(TypeParameter), + parameters: invoke.Parameters.Select(p => ParameterDeclaration(p)), + returnType: invoke.ReturnsVoid ? null : TypeExpression(invoke.ReturnType), accessibility: type.DeclaredAccessibility, - modifiers: DeclarationModifiers.From(type), - baseType: (type.BaseType != null) ? TypeExpression(type.BaseType) : null, - interfaceTypes: type.Interfaces.Select(TypeExpression), - members: type.GetMembers().Where(CanBeDeclared).Select(Declaration)); - break; - case TypeKind.Struct: - declaration = StructDeclaration( - type.Name, - type.TypeParameters.Select(p => TypeParameter(p)), - accessibility: type.DeclaredAccessibility, - modifiers: DeclarationModifiers.From(type), - interfaceTypes: type.Interfaces.Select(TypeExpression), - members: type.GetMembers().Where(CanBeDeclared).Select(Declaration)); - break; - case TypeKind.Interface: - declaration = InterfaceDeclaration( - type.Name, - type.TypeParameters.Select(p => TypeParameter(p)), - accessibility: type.DeclaredAccessibility, - interfaceTypes: type.Interfaces.Select(TypeExpression), - members: type.GetMembers().Where(CanBeDeclared).Select(Declaration)); - break; - case TypeKind.Enum: - declaration = EnumDeclaration( - type.Name, - underlyingType: (type.EnumUnderlyingType == null || type.EnumUnderlyingType.SpecialType == SpecialType.System_Int32) ? null : TypeExpression(type.EnumUnderlyingType.SpecialType), - accessibility: type.DeclaredAccessibility, - members: type.GetMembers().Where(s => s.Kind == SymbolKind.Field).Select(Declaration)); - break; - case TypeKind.Delegate: - if (type.GetMembers("Invoke") is [IMethodSymbol invoke, ..]) - { - declaration = DelegateDeclaration( - type.Name, - typeParameters: type.TypeParameters.Select(p => TypeParameter(p)), - parameters: invoke.Parameters.Select(p => ParameterDeclaration(p)), - returnType: invoke.ReturnsVoid ? null : TypeExpression(invoke.ReturnType), - accessibility: type.DeclaredAccessibility, - modifiers: DeclarationModifiers.From(type)); - } - - break; - } + modifiers: DeclarationModifiers.From(type)) + : null, + _ => null, + }; if (declaration != null) - { return WithTypeParametersAndConstraints(declaration, type.TypeParameters); - } break; } @@ -830,13 +822,21 @@ public SyntaxNode Declaration(ISymbol symbol) private static bool CanBeDeclared(ISymbol symbol) { + // Skip implicitly declared members from a record. No need to synthesize those as the compiler will do it + // anyways. + if (symbol.ContainingType?.IsRecord is true) + { + if (symbol.IsImplicitlyDeclared) + return false; + } + switch (symbol.Kind) { case SymbolKind.Field: case SymbolKind.Property: case SymbolKind.Event: case SymbolKind.Parameter: - return true; + return symbol.CanBeReferencedByName; case SymbolKind.Method: var method = (IMethodSymbol)symbol; @@ -844,8 +844,9 @@ private static bool CanBeDeclared(ISymbol symbol) { case MethodKind.Constructor: case MethodKind.SharedConstructor: - case MethodKind.Ordinary: return true; + case MethodKind.Ordinary: + return method.CanBeReferencedByName; } break; @@ -859,7 +860,7 @@ private static bool CanBeDeclared(ISymbol symbol) case TypeKind.Interface: case TypeKind.Enum: case TypeKind.Delegate: - return true; + return type.CanBeReferencedByName; } break; diff --git a/src/Workspaces/VisualBasic/Portable/CodeGeneration/VisualBasicSyntaxGenerator.vb b/src/Workspaces/VisualBasic/Portable/CodeGeneration/VisualBasicSyntaxGenerator.vb index 8e3345b2d0549..167939982fd74 100644 --- a/src/Workspaces/VisualBasic/Portable/CodeGeneration/VisualBasicSyntaxGenerator.vb +++ b/src/Workspaces/VisualBasic/Portable/CodeGeneration/VisualBasicSyntaxGenerator.vb @@ -1366,13 +1366,14 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.CodeGeneration End Function Private Protected Overrides Function ClassDeclaration( - name As String, - typeParameters As IEnumerable(Of SyntaxNode), - accessibility As Accessibility, - modifiers As DeclarationModifiers, - baseType As SyntaxNode, - interfaceTypes As IEnumerable(Of SyntaxNode), - members As IEnumerable(Of SyntaxNode)) As SyntaxNode + isRecord As Boolean, + name As String, + typeParameters As IEnumerable(Of SyntaxNode), + accessibility As Accessibility, + modifiers As DeclarationModifiers, + baseType As SyntaxNode, + interfaceTypes As IEnumerable(Of SyntaxNode), + members As IEnumerable(Of SyntaxNode)) As SyntaxNode Dim itypes = If(interfaceTypes IsNot Nothing, interfaceTypes.Cast(Of TypeSyntax), Nothing) If itypes IsNot Nothing AndAlso itypes.Count = 0 Then @@ -1403,12 +1404,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.CodeGeneration End Function Private Protected Overrides Function StructDeclaration( - name As String, - typeParameters As IEnumerable(Of SyntaxNode), - accessibility As Accessibility, - modifiers As DeclarationModifiers, - interfaceTypes As IEnumerable(Of SyntaxNode), - members As IEnumerable(Of SyntaxNode)) As SyntaxNode + isRecord As Boolean, + name As String, + typeParameters As IEnumerable(Of SyntaxNode), + accessibility As Accessibility, + modifiers As DeclarationModifiers, + interfaceTypes As IEnumerable(Of SyntaxNode), + members As IEnumerable(Of SyntaxNode)) As SyntaxNode Dim itypes = If(interfaceTypes IsNot Nothing, interfaceTypes.Cast(Of TypeSyntax), Nothing) If itypes IsNot Nothing AndAlso itypes.Count = 0 Then