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"));
+ }
}
}