Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Rules/MA0204.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ The `partial` modifier can be removed when there is no other declaration to merg

Classes inheriting from common WPF XAML base types (`System.Windows.Controls.UserControl`, `System.Windows.Controls.Page`, `System.Windows.Window`, and `System.Windows.Application`) are excluded.

The rule also does not report classes whose `partial` modifier can be required by CsWinRT AOT/WinRT source generation, for instance when CsWinRT support or .NET MAUI types are present in the compilation and a class implements WinRT-mapped interfaces such as `IDisposable`.

## Non-compliant code

````csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,35 @@ public override void Initialize(AnalysisContext context)
context.Compilation.GetBestTypeByMetadataName("System.Windows.Controls.Page"),
context.Compilation.GetBestTypeByMetadataName("System.Windows.Window"),
context.Compilation.GetBestTypeByMetadataName("System.Windows.Application"));
var csWinRTCustomMappedInterfaces = ImmutableArray.Create(
context.Compilation.GetBestTypeByMetadataName("System.IDisposable"),
context.Compilation.GetBestTypeByMetadataName("System.IServiceProvider"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.IEnumerable"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.IList"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerator`1"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Generic.IList`1"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Generic.IDictionary`2"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Generic.IReadOnlyList`1"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Generic.IReadOnlyDictionary`2"),
context.Compilation.GetBestTypeByMetadataName("System.Collections.Specialized.INotifyCollectionChanged"),
context.Compilation.GetBestTypeByMetadataName("System.ComponentModel.INotifyDataErrorInfo"),
context.Compilation.GetBestTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"),
context.Compilation.GetBestTypeByMetadataName("System.Windows.Input.ICommand"));
var hasCsWinRTAotSupport =
context.Compilation.GetBestTypeByMetadataName("WinRT.WindowsRuntimeTypeAttribute") is not null &&
context.Compilation.GetBestTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute") is not null &&
context.Compilation.GetBestTypeByMetadataName("WinRT.GeneratedWinRTExposedTypeAttribute") is not null &&
context.Compilation.GetBestTypeByMetadataName("WinRT.WinRTExposedTypeAttribute") is not null;
var isMauiCompilation =
context.Compilation.GetBestTypeByMetadataName("Microsoft.Maui.Controls.Application") is not null &&
context.Compilation.GetBestTypeByMetadataName("Microsoft.Maui.Controls.BindableObject") is not null;

context.RegisterSymbolAction(context => AnalyzeNamedTypeSymbol(context, excludedBaseTypes), SymbolKind.NamedType);
context.RegisterSymbolAction(context => AnalyzeNamedTypeSymbol(context, excludedBaseTypes, csWinRTCustomMappedInterfaces, hasCsWinRTAotSupport, isMauiCompilation), SymbolKind.NamedType);
});
}

private static void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context, ImmutableArray<INamedTypeSymbol?> excludedBaseTypes)
private static void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context, ImmutableArray<INamedTypeSymbol?> excludedBaseTypes, ImmutableArray<INamedTypeSymbol?> csWinRTCustomMappedInterfaces, bool hasCsWinRTAotSupport, bool isMauiCompilation)
{
var symbol = (INamedTypeSymbol)context.Symbol;
if (symbol.TypeKind is not (TypeKind.Class or TypeKind.Struct or TypeKind.Interface))
Expand All @@ -55,6 +78,9 @@ private static void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context, Immuta
if (InheritsFromExcludedType(symbol, excludedBaseTypes))
return;

if (RequiresPartialForCsWinRT(symbol, csWinRTCustomMappedInterfaces, hasCsWinRTAotSupport, isMauiCompilation))
return;

context.ReportDiagnostic(Rule, partialToken.GetLocation());
}

Expand All @@ -68,4 +94,54 @@ private static bool InheritsFromExcludedType(INamedTypeSymbol symbol, ImmutableA

return false;
}

private static bool RequiresPartialForCsWinRT(INamedTypeSymbol symbol, ImmutableArray<INamedTypeSymbol?> csWinRTCustomMappedInterfaces, bool hasCsWinRTAotSupport, bool isMauiCompilation)
{
if (!hasCsWinRTAotSupport && !isMauiCompilation)
return false;

return ContainsTypeRequiringCsWinRTPartial(symbol, csWinRTCustomMappedInterfaces);
}

private static bool ContainsTypeRequiringCsWinRTPartial(INamedTypeSymbol symbol, ImmutableArray<INamedTypeSymbol?> csWinRTCustomMappedInterfaces)
{
if (IsTypeRequiringCsWinRTPartial(symbol, csWinRTCustomMappedInterfaces))
return true;

foreach (var nestedType in symbol.GetTypeMembers())
{
if (ContainsTypeRequiringCsWinRTPartial(nestedType, csWinRTCustomMappedInterfaces))
return true;
}

return false;
}

private static bool IsTypeRequiringCsWinRTPartial(INamedTypeSymbol symbol, ImmutableArray<INamedTypeSymbol?> csWinRTCustomMappedInterfaces)
{
if (symbol.TypeKind != TypeKind.Class || symbol.IsAbstract || symbol.IsStatic)
return false;

foreach (var interfaceType in symbol.AllInterfaces)
{
if (IsCsWinRTCustomMappedInterface(interfaceType, csWinRTCustomMappedInterfaces))
return true;
}

return false;
}

private static bool IsCsWinRTCustomMappedInterface(INamedTypeSymbol interfaceType, ImmutableArray<INamedTypeSymbol?> csWinRTCustomMappedInterfaces)
{
foreach (var csWinRTCustomMappedInterface in csWinRTCustomMappedInterfaces)
{
if (csWinRTCustomMappedInterface is not null &&
SymbolEqualityComparer.Default.Equals(interfaceType.OriginalDefinition, csWinRTCustomMappedInterface))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,109 @@ partial class Nested
return test.RunAsync();
}

[Fact]
public Task PartialClass_ImplementingDisposable_WithCsWinRTSupport_NoDiagnostic()
{
var test = CreateTest();
test.TestCode = """
using System;

namespace WinRT
{
sealed class WindowsRuntimeTypeAttribute : Attribute
{
}

sealed class GeneratedBindableCustomPropertyAttribute : Attribute
{
}

sealed class GeneratedWinRTExposedTypeAttribute : Attribute
{
}

sealed class WinRTExposedTypeAttribute : Attribute
{
}
}

partial class Sample : IDisposable
{
public void Dispose()
{
}
}
""";

return test.RunAsync();
}

[Fact]
public Task PartialClass_ImplementingDisposable_WithoutCsWinRTOrMauiSupport_ReportsDiagnostic()
{
var test = CreateTest();
test.TestCode = """
using System;

{|MA0204:partial|} class Sample : IDisposable
{
public void Dispose()
{
}
}
""";
test.FixedCode = """
using System;
class Sample : IDisposable
{
public void Dispose()
{
}
}
""";

return test.RunAsync();
}

[Fact]
public Task PartialContainingClass_WithNestedDisposableType_InMauiCompilation_NoDiagnostic()
{
var test = CreateTest();
test.TestCode = """
using System;

namespace Microsoft.Maui.Controls
{
class BindableObject
{
}

class Application : BindableObject
{
}
}

partial class Sample
{
partial class Nested : IDisposable
{
public void Dispose()
{
}
}
}

partial class Sample
{
partial class Nested
{
}
}
""";

return test.RunAsync();
}

[Fact]
public Task PartialClass_InheritingFromWpfUserControl_NoDiagnostic()
{
Expand Down