diff --git a/src/Compilers/CSharp/Portable/SymbolDisplay/SymbolDisplayVisitor.Types.cs b/src/Compilers/CSharp/Portable/SymbolDisplay/SymbolDisplayVisitor.Types.cs index 171b6e60eb90e8a2685b586a8f1642fadb266c76..fda4e4baa458c35452b6c9c9a721c7fb3f28ab67 100644 --- a/src/Compilers/CSharp/Portable/SymbolDisplay/SymbolDisplayVisitor.Types.cs +++ b/src/Compilers/CSharp/Portable/SymbolDisplay/SymbolDisplayVisitor.Types.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; @@ -658,6 +659,11 @@ private void AddTypeKind(INamedTypeSymbol symbol) { switch (symbol.TypeKind) { + case TypeKind.Class when FindValidCloneMethod(symbol) is object: + AddKeyword(SyntaxKind.RecordKeyword); + AddSpace(); + break; + case TypeKind.Module: case TypeKind.Class: AddKeyword(SyntaxKind.ClassKeyword); @@ -700,6 +706,61 @@ private void AddTypeKind(INamedTypeSymbol symbol) } } + /// + /// Copy of + /// + private static IMethodSymbol FindValidCloneMethod(ITypeSymbol containingType) + { + IMethodSymbol candidate = null; + + foreach (var member in containingType.GetMembers(WellKnownMemberNames.CloneMethodName)) + { + if (member is IMethodSymbol + { + DeclaredAccessibility: Accessibility.Public, + IsStatic: false, + Parameters: { Length: 0 }, + Arity: 0 + } method) + { + if (candidate is object) + { + // An ambiguity case, can come from metadata, treat as an error for simplicity. + return null; + } + + candidate = method; + } + } + + if (candidate is null || + !(containingType.IsSealed || candidate.IsOverride || candidate.IsVirtual || candidate.IsAbstract) || + !isEqualToOrDerivedFrom( + containingType, + candidate.ReturnType)) + { + return null; + } + + return candidate; + + static bool isEqualToOrDerivedFrom(ITypeSymbol one, ITypeSymbol other) + { + do + { + if (one.Equals(other, SymbolEqualityComparer.IgnoreAll)) + { + return true; + } + + one = one.BaseType; + } + while (one != null); + + return false; + } + } + private void AddTypeParameterVarianceIfRequired(ITypeParameterSymbol symbol) { if (format.GenericsOptions.IncludesOption(SymbolDisplayGenericsOptions.IncludeVariance)) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs index 1570f202e5056f7830e11897bfc0725abd28d594..a07a42912c8770c1c05526f823235f98c4d3f259 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordClone.cs @@ -139,6 +139,7 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, throw ExceptionUtilities.Unreachable; } + // Note: this method was replicated in SymbolDisplayVisitor.FindValidCloneMethod internal static MethodSymbol? FindValidCloneMethod(TypeSymbol containingType, ref HashSet? useSiteDiagnostics) { MethodSymbol? candidate = null; @@ -155,7 +156,7 @@ internal override void GenerateMethodBody(TypeCompilationState compilationState, { if (candidate is object) { - // An ammbiguity case, can come from metadata, treat as an error for simplicity. + // An ambiguity case, can come from metadata, treat as an error for simplicity. return null; } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs index d4d053833a8fe5d85bdc8810abe27f43d9a8a1f5..6d02e6c64b59ca669313d0ee1647683a2d6f6965 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordTests.cs @@ -1730,6 +1730,9 @@ public void Clone_01() Assert.True(clone.ContainingType.IsSealed); Assert.True(clone.ContainingType.IsAbstract); + + Assert.Equal("record C1", comp.GlobalNamespace.GetTypeMember("C1") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] @@ -1756,6 +1759,9 @@ public void Clone_02() Assert.True(clone.ContainingType.IsSealed); Assert.True(clone.ContainingType.IsAbstract); + + Assert.Equal("record C1", comp.GlobalNamespace.GetTypeMember("C1") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] @@ -1810,10 +1816,13 @@ public void Clone_04() Assert.True(clone.ContainingType.IsSealed); Assert.True(clone.ContainingType.IsAbstract); + + Assert.Equal("record C1", comp.GlobalNamespace.GetTypeMember("C1") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_05() + public void Clone_05_IntReturnType_UsedAsBaseType() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -1904,10 +1913,13 @@ .maxstack 8 // public record B : A { Diagnostic(ErrorCode.ERR_BadRecordBase, "A").WithLocation(2, 19) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_06() + public void Clone_06_IntReturnType_UsedInWith() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -2004,10 +2016,13 @@ static void Main() // A x = new A() with { }; Diagnostic(ErrorCode.ERR_NoSingleCloneMethod, "new A()").WithArguments("A").WithLocation(6, 15) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_07() + public void Clone_07_Ambiguous_UsedAsBaseType() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -2108,10 +2123,13 @@ .maxstack 8 // public record B : A { Diagnostic(ErrorCode.ERR_BadRecordBase, "A").WithLocation(2, 19) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_08() + public void Clone_08_Ambiguous_UsedInWith() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -2218,16 +2236,18 @@ static void Main() // A x = new A() with { }; Diagnostic(ErrorCode.ERR_NoSingleCloneMethod, "new A()").WithArguments("A").WithLocation(6, 15) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_09() + public void Clone_09_AmbiguousReverseOrder_UsedAsBaseType() { var ilSource = @" .class public auto ansi beforefieldinit A extends System.Object { - // Methods // Methods .method public hidebysig specialname newslot virtual instance class A '" + WellKnownMemberNames.CloneMethodName + @"' () cil managed @@ -2322,10 +2342,13 @@ .maxstack 8 // public record B : A { Diagnostic(ErrorCode.ERR_BadRecordBase, "A").WithLocation(2, 19) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_10() + public void Clone_10_AmbiguousReverseOrder_UsedInWith() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -2432,6 +2455,9 @@ static void Main() // A x = new A() with { }; Diagnostic(ErrorCode.ERR_NoSingleCloneMethod, "new A()").WithArguments("A").WithLocation(6, 15) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] @@ -2731,7 +2757,7 @@ public static void Main() } [Fact] - public void Clone_17() + public void Clone_17_NonOverridable() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -2822,10 +2848,13 @@ .maxstack 8 // public record B : A { Diagnostic(ErrorCode.ERR_BadRecordBase, "A").WithLocation(2, 19) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] - public void Clone_18() + public void Clone_18_NonOverridable() { var ilSource = @" .class public auto ansi beforefieldinit A @@ -2916,6 +2945,9 @@ .maxstack 8 // public record B : A { Diagnostic(ErrorCode.ERR_BadRecordBase, "A").WithLocation(2, 19) ); + + Assert.Equal("class A", comp.GlobalNamespace.GetTypeMember("A") + .ToDisplayString(SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); } [Fact] @@ -19120,6 +19152,19 @@ public static void Main() ); } + [Fact] + public void RecordLoadedInVisualBasicDisplaysAsRecord() + { + var src = @" +public record A; +"; + var compRef = CreateCompilation(src).EmitToImageReference(); + var vbComp = CreateVisualBasicCompilation("", referencedAssemblies: new[] { compRef }); + var symbol = vbComp.GlobalNamespace.GetTypeMember("A"); + Assert.Equal("record A", + SymbolDisplay.ToDisplayString(symbol, SymbolDisplayFormat.TestFormat.AddKindOptions(SymbolDisplayKindOptions.IncludeTypeKeyword))); + } + [Fact] public void AnalyzerActions_01() { diff --git a/src/Compilers/CSharp/Test/Symbol/SymbolDisplay/SymbolDisplayTests.cs b/src/Compilers/CSharp/Test/Symbol/SymbolDisplay/SymbolDisplayTests.cs index 0a2dad7e88d6c7f07bffe9853fac7fc591ab4c61..add309d9aeba9563578fda81df28f45e93ce90fd 100644 --- a/src/Compilers/CSharp/Test/Symbol/SymbolDisplay/SymbolDisplayTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/SymbolDisplay/SymbolDisplayTests.cs @@ -7584,5 +7584,26 @@ class B method.ToDisplayParts(formatWithoutOptions), "static void F4(nint[] x, A y)"); } + + [Fact] + public void RecordDeclaration() + { + var text = @" +record Person(string First, string Last); +"; + Func findSymbol = global => global.GetTypeMembers("Person").Single(); + + var format = new SymbolDisplayFormat(memberOptions: SymbolDisplayMemberOptions.IncludeType, kindOptions: SymbolDisplayKindOptions.IncludeTypeKeyword); + + TestSymbolDescription( + text, + findSymbol, + format, + TestOptions.Regular.WithLanguageVersion(LanguageVersion.CSharp9), + "record Person", + SymbolDisplayPartKind.Keyword, + SymbolDisplayPartKind.Space, + SymbolDisplayPartKind.ClassName); + } } } diff --git a/src/Compilers/Core/Portable/Symbols/SymbolEqualityComparer.cs b/src/Compilers/Core/Portable/Symbols/SymbolEqualityComparer.cs index 0bfcbb7936d3a4f22a3cb7bac011e9b75064b822..3f1a5c49b82eb96a541190592cb39c2f9862420c 100644 --- a/src/Compilers/Core/Portable/Symbols/SymbolEqualityComparer.cs +++ b/src/Compilers/Core/Portable/Symbols/SymbolEqualityComparer.cs @@ -27,6 +27,7 @@ public sealed class SymbolEqualityComparer : IEqualityComparer // Internal only comparisons: internal readonly static SymbolEqualityComparer ConsiderEverything = new SymbolEqualityComparer(TypeCompareKind.ConsiderEverything); + internal readonly static SymbolEqualityComparer IgnoreAll = new SymbolEqualityComparer(TypeCompareKind.AllIgnoreOptions); internal TypeCompareKind CompareKind { get; } diff --git a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs index 2c3d1650060f5df043a59a83e364b19cebf322cd..b0369f4b933017d1542eac4704633009fc9174dd 100644 --- a/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs +++ b/src/EditorFeatures/CSharpTest/QuickInfo/SemanticQuickInfoSourceTests.cs @@ -7036,5 +7036,35 @@ void Goo(object o) } }"); } + + [Fact, Trait(Traits.Feature, Traits.Features.QuickInfo)] + public async Task QuickInfoRecord() + { + await TestWithOptionsAsync( + Options.Regular.WithLanguageVersion(LanguageVersion.CSharp9), +@"record Person(string First, string Last) +{ + void M($$Person p) + { + } +}", MainDescription("record Person")); + } + + [Fact, Trait(Traits.Feature, Traits.Features.QuickInfo)] + public async Task QuickInfoDerivedRecord() + { + await TestWithOptionsAsync( + Options.Regular.WithLanguageVersion(LanguageVersion.CSharp9), +@"record Person(string First, string Last) +{ +} +record Student(string Id) +{ + void M($$Student p) + { + } +} +", MainDescription("record Student")); + } } }