diff --git a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs index cbfd81891bc1e..dcbc91df9b4b9 100644 --- a/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs +++ b/src/Features/Core/Portable/DesignerAttribute/DesignerAttributeDiscoveryService.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Composition; @@ -30,6 +31,17 @@ namespace Microsoft.CodeAnalysis.DesignerAttribute; [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed partial class DesignerAttributeDiscoveryService() : IDesignerAttributeDiscoveryService { + /// + /// Ugly, but sufficient hack. During times where we're missing global usings (which may not always be available + /// while the sdk is regenerating/restoring on things like a tfm switch), we hardcode in knowledge we have about + /// which namespaces the core designable types are in. That way we can still make a solid guess about what the base + /// type is, even if we can't resolve it at this moment. + /// + private static readonly ImmutableArray s_wellKnownDesignerNamespaces = [ + "System.Windows.Forms.Form", + "System.Windows.Forms.Design", + "System.ComponentModel"]; + /// /// Cache from the individual references a project has, to a boolean specifying if reference knows about the /// System.ComponentModel.DesignerCategoryAttribute attribute. @@ -244,7 +256,7 @@ private static async ValueTask HasDesignerCategoryTypeAsync(Project projec } hasDesignerCategoryType ??= await HasDesignerCategoryTypeAsync(project, cancellationToken).ConfigureAwait(false); - var data = await ComputeDesignerAttributeDataAsync(project, documentId, filePath, hasDesignerCategoryType.Value).ConfigureAwait(false); + var data = await ComputeDesignerAttributeDataAsync(project, documentId, filePath, hasDesignerCategoryType.Value, existingInfo.category).ConfigureAwait(false); if (data.Category != existingInfo.category) results.Add((data, projectVersion)); } @@ -252,13 +264,13 @@ private static async ValueTask HasDesignerCategoryTypeAsync(Project projec return results.ToImmutableAndClear(); async Task ComputeDesignerAttributeDataAsync( - Project project, DocumentId documentId, string filePath, bool hasDesignerCategoryType) + Project project, DocumentId documentId, string filePath, bool hasDesignerCategoryType, string? existingCategory) { // We either haven't computed the designer info, or our data was out of date. We need // So recompute here. Figure out what the current category is, and if that's different // from what we previously stored. var category = await ComputeDesignerAttributeCategoryAsync( - hasDesignerCategoryType, project, documentId, cancellationToken).ConfigureAwait(false); + hasDesignerCategoryType, project, documentId, existingCategory, cancellationToken).ConfigureAwait(false); return new DesignerAttributeData { @@ -270,7 +282,7 @@ private static async ValueTask HasDesignerCategoryTypeAsync(Project projec } public static async Task ComputeDesignerAttributeCategoryAsync( - bool hasDesignerCategoryType, Project project, DocumentId documentId, CancellationToken cancellationToken) + bool hasDesignerCategoryType, Project project, DocumentId documentId, string? existingCategory, CancellationToken cancellationToken) { // simple case. If there's no DesignerCategory type in this compilation, then there's definitely no // designable types. @@ -292,10 +304,17 @@ private static async ValueTask HasDesignerCategoryTypeAsync(Project projec var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var firstClassType = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(firstClass, cancellationToken); - foreach (var type in firstClassType.GetBaseTypesAndThis()) + foreach (var type in GetBaseTypesAndThis(semanticModel.Compilation, firstClassType)) { cancellationToken.ThrowIfCancellationRequested(); + // If we hit an error type while walking up, then preserve the existing category. We do want a temporary + // invalid base type to not cause us to lose the existing category, causing a designable type to revert to + // an undesignable one. The designer can still support scenarios like this as it is itself error tolerant, + // falling back to the prior category in a case like this. + if (type is IErrorTypeSymbol errorType) + return existingCategory; + // See if it has the designer attribute on it. Use symbol-equivalence instead of direct equality // as the symbol we have var attribute = type.GetAttributes().FirstOrDefault(d => IsDesignerAttribute(d.AttributeClass)); @@ -305,6 +324,33 @@ private static async ValueTask HasDesignerCategoryTypeAsync(Project projec return null; + static IEnumerable GetBaseTypesAndThis(Compilation compilation, INamedTypeSymbol firstType) + { + var current = firstType; + while (current != null) + { + yield return current; + current = current.BaseType; + + if (current is IErrorTypeSymbol errorType) + current = TryMapToNonErrorType(compilation, errorType); + } + } + + static INamedTypeSymbol? TryMapToNonErrorType(Compilation compilation, IErrorTypeSymbol errorType) + { + foreach (var wellKnownNamespace in s_wellKnownDesignerNamespaces) + { + var wellKnownType = compilation.GetTypeByMetadataName($"{wellKnownNamespace}.{errorType.Name}"); + if (wellKnownType != null) + return wellKnownType; + } + + // Couldn't find a match. Just return the error type as is. Caller will handle this case and try to + // preserve the existing category. + return errorType; + } + static bool IsDesignerAttribute(INamedTypeSymbol? attributeClass) => attributeClass is { diff --git a/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs b/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs index f5e6e411f4b2f..49ac948899936 100644 --- a/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs +++ b/src/VisualStudio/CSharp/Test/DesignerAttribute/DesignerAttributeServiceTests.cs @@ -11,85 +11,119 @@ using Roslyn.Utilities; using Xunit; -namespace Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.DesignerAttributes +namespace Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.DesignerAttributes; + +[UseExportProvider] +public sealed class DesignerAttributeServiceTests { - [UseExportProvider] - public class DesignerAttributeServiceTests + [Fact] + public async Task NoDesignerTest1() { - [Fact] - public async Task NoDesignerTest1() - { - var code = @"class Test { }"; - - await TestAsync(code, category: null); - } - - [Fact] - public async Task NoDesignerOnSecondClass() - { - - await TestAsync( -@"class Test1 { } + await TestAsync(@"class Test { }", category: null); + } -[System.ComponentModel.DesignerCategory(""Form"")] -class Test2 { }", category: null); - } + [Fact] + public async Task NoDesignerOnSecondClass() + { + await TestAsync( + """ + class Test1 { } - [Fact] - public async Task NoDesignerOnStruct() - { + [System.ComponentModel.DesignerCategory("Form")] + class Test2 { } + """, category: null); + } - await TestAsync( -@" -[System.ComponentModel.DesignerCategory(""Form"")] -struct Test1 { }", category: null); - } + [Fact] + public async Task NoDesignerOnStruct() + { + await TestAsync( + """ - [Fact] - public async Task NoDesignerOnNestedClass() - { + [System.ComponentModel.DesignerCategory("Form")] + struct Test1 { } + """, category: null); + } - await TestAsync( -@"class Test1 -{ - [System.ComponentModel.DesignerCategory(""Form"")] - class Test2 { } -}", category: null); - } + [Fact] + public async Task NoDesignerOnNestedClass() + { + await TestAsync( + """ + class Test1 + { + [System.ComponentModel.DesignerCategory("Form")] + class Test2 { } + } + """, category: null); + } - [Fact] - public async Task SimpleDesignerTest() - { + [Fact] + public async Task SimpleDesignerTest() + { + await TestAsync( + """ + [System.ComponentModel.DesignerCategory("Form")] + class Test { } + """, "Form"); + } - await TestAsync( -@"[System.ComponentModel.DesignerCategory(""Form"")] -class Test { }", "Form"); - } + [Fact] + public async Task SimpleDesignerTest2() + { + await TestAsync( + """ + using System.ComponentModel; - [Fact] - public async Task SimpleDesignerTest2() - { + [DesignerCategory("Form")] + class Test { } + """, "Form"); + } - await TestAsync( -@"using System.ComponentModel; + [Theory] + [InlineData(null)] + [InlineData("Form")] + [InlineData("Form1")] + public async Task TestUnboundBase1(string? existingCategory) + { + await TestAsync( + """ + namespace System.Windows.Forms + { + [System.ComponentModel.DesignerCategory("Form")] + public class Form { } + } + + // The base type won't bind. That's ok. We should fallback to looking it up in a particular namespace. + // This should always work and not be impacted by the existing category. + class Test : Form { } + """, "Form", existingCategory); + } -[DesignerCategory(""Form"")] -class Test { }", "Form"); - } + [Theory] + [InlineData(null)] + [InlineData("Form")] + public async Task TestUnboundBaseUseOldValueIfNotFound(string? category) + { + await TestAsync( + """ + // The base type won't bind. Return existing category if we have one. + class Test : Form { } + """, category: category, existingCategory: category); + } - private static async Task TestAsync(string codeWithMarker, string? category) - { - using var workspace = TestWorkspace.CreateCSharp(codeWithMarker, openDocuments: false); + private static async Task TestAsync(string codeWithMarker, string? category, string? existingCategory = null) + { + using var workspace = TestWorkspace.CreateCSharp(codeWithMarker, openDocuments: false); - var hostDocument = workspace.Documents.First(); - var documentId = hostDocument.Id; - var document = workspace.CurrentSolution.GetRequiredDocument(documentId); + var hostDocument = workspace.Documents.First(); + var documentId = hostDocument.Id; + var document = workspace.CurrentSolution.GetRequiredDocument(documentId); - var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None); - var actual = await DesignerAttributeDiscoveryService.ComputeDesignerAttributeCategoryAsync( - compilation.DesignerCategoryAttributeType() != null, document.Project, document.Id, CancellationToken.None); + var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None); + var actual = await DesignerAttributeDiscoveryService.ComputeDesignerAttributeCategoryAsync( + compilation.DesignerCategoryAttributeType() != null, document.Project, document.Id, existingCategory, CancellationToken.None); - Assert.Equal(category, actual); - } + Assert.Equal(category, actual); } }