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