diff --git a/README.md b/README.md
index 94809d2f..c6ccf261 100755
--- a/README.md
+++ b/README.md
@@ -222,6 +222,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md)|Usage|Do not use zero-valued enum flags in flag checks|⚠️|✔️|❌|
|[MA0202](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0202.md)|Design|Conditional compilation branches have identical code|⚠️|✔️|✔️|
|[MA0203](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0203.md)|Design|Do not use return tag for void method|⚠️|✔️|❌|
+|[MA0204](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0204.md)|Design|Remove unnecessary partial modifier|ℹ️|✔️|✔️|
diff --git a/docs/README.md b/docs/README.md
index 59543821..fa035adb 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -202,6 +202,7 @@
|[MA0201](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0201.md)|Usage|Do not use zero-valued enum flags in flag checks|⚠️|✔️|❌|
|[MA0202](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0202.md)|Design|Conditional compilation branches have identical code|⚠️|✔️|✔️|
|[MA0203](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0203.md)|Design|Do not use return tag for void method|⚠️|✔️|❌|
+|[MA0204](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0204.md)|Design|Remove unnecessary partial modifier|ℹ️|✔️|✔️|
|Id|Suppressed rule|Justification|
|--|---------------|-------------|
diff --git a/docs/Rules/MA0204.md b/docs/Rules/MA0204.md
new file mode 100644
index 00000000..72103b0f
--- /dev/null
+++ b/docs/Rules/MA0204.md
@@ -0,0 +1,24 @@
+# MA0204 - Remove unnecessary partial modifier
+
+Sources: [RemoveUnnecessaryPartialModifierAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/RemoveUnnecessaryPartialModifierAnalyzer.cs), [RemoveUnnecessaryPartialModifierFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveUnnecessaryPartialModifierFixer.cs)
+
+
+This rule reports a `partial` modifier on a type when the type has a single declaration and no partial members.
+
+The `partial` modifier can be removed when there is no other declaration to merge with.
+
+## Non-compliant code
+
+````csharp
+partial class Sample
+{
+}
+````
+
+## Compliant code
+
+````csharp
+class Sample
+{
+}
+````
diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveUnnecessaryPartialModifierFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveUnnecessaryPartialModifierFixer.cs
new file mode 100644
index 00000000..c05f1c69
--- /dev/null
+++ b/src/Meziantou.Analyzer.CodeFixers/Rules/RemoveUnnecessaryPartialModifierFixer.cs
@@ -0,0 +1,81 @@
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.CodeAnalysis.Formatting;
+
+namespace Meziantou.Analyzer.Rules;
+
+[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
+public sealed class RemoveUnnecessaryPartialModifierFixer : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.RemoveUnnecessaryPartialModifier);
+
+ public override FixAllProvider GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ return;
+
+ var partialKeyword = root.FindToken(context.Span.Start);
+ if (!partialKeyword.IsKind(SyntaxKind.PartialKeyword))
+ return;
+
+ var typeDeclaration = partialKeyword.Parent?.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (typeDeclaration is null || !typeDeclaration.Modifiers.Contains(partialKeyword))
+ return;
+
+ var title = "Remove partial modifier";
+ var codeAction = CodeAction.Create(
+ title,
+ ct => RemovePartialModifierAsync(context.Document, partialKeyword, ct),
+ equivalenceKey: title);
+
+ context.RegisterCodeFix(codeAction, context.Diagnostics);
+ }
+
+ private static async Task RemovePartialModifierAsync(Document document, SyntaxToken partialKeyword, CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null)
+ return document;
+
+ var currentPartialKeyword = root.FindToken(partialKeyword.SpanStart);
+ var typeDeclaration = currentPartialKeyword.Parent?.AncestorsAndSelf().OfType().FirstOrDefault();
+ if (typeDeclaration is null || !typeDeclaration.Modifiers.Contains(currentPartialKeyword))
+ return document;
+
+ var newTypeDeclaration = RemovePartialModifier(typeDeclaration, currentPartialKeyword);
+ return document.WithSyntaxRoot(root.ReplaceNode(typeDeclaration, newTypeDeclaration));
+ }
+
+ private static TypeDeclarationSyntax RemovePartialModifier(TypeDeclarationSyntax typeDeclaration, SyntaxToken partialKeyword)
+ {
+ var modifiers = typeDeclaration.Modifiers;
+ var partialKeywordIndex = modifiers.IndexOf(partialKeyword);
+ if (partialKeywordIndex < 0)
+ return typeDeclaration;
+
+ var nonspaceTrivia = partialKeyword.LeadingTrivia.Concat(partialKeyword.TrailingTrivia).Where(t => !t.IsKind(SyntaxKind.WhitespaceTrivia) && !t.IsKind(SyntaxKind.EndOfLineTrivia)).ToList();
+
+ if (partialKeywordIndex + 1 < modifiers.Count)
+ {
+ var nextModifier = modifiers[partialKeywordIndex + 1];
+ modifiers = modifiers.Replace(nextModifier, nextModifier.WithLeadingTrivia(nonspaceTrivia.Concat(nextModifier.LeadingTrivia)));
+ return typeDeclaration.WithModifiers(modifiers.Remove(partialKeyword));
+ }
+
+ return typeDeclaration
+ .WithModifiers(modifiers.Remove(partialKeyword))
+ .WithKeyword(typeDeclaration.Keyword.WithLeadingTrivia(nonspaceTrivia.Concat(typeDeclaration.Keyword.LeadingTrivia)));
+ }
+}
diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig
index 4910b616..93fd358c 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/all-errors.editorconfig
@@ -604,3 +604,6 @@ dotnet_diagnostic.MA0202.severity = error
# MA0203: Do not use return tag for void method
dotnet_diagnostic.MA0203.severity = error
+
+# MA0204: Remove unnecessary partial modifier
+dotnet_diagnostic.MA0204.severity = error
diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig
index 95bc2894..e1f4e48d 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/all-suggestions.editorconfig
@@ -604,3 +604,6 @@ dotnet_diagnostic.MA0202.severity = suggestion
# MA0203: Do not use return tag for void method
dotnet_diagnostic.MA0203.severity = suggestion
+
+# MA0204: Remove unnecessary partial modifier
+dotnet_diagnostic.MA0204.severity = suggestion
diff --git a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig
index fcd8960a..a2514576 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/all-warnings.editorconfig
@@ -604,3 +604,6 @@ dotnet_diagnostic.MA0202.severity = warning
# MA0203: Do not use return tag for void method
dotnet_diagnostic.MA0203.severity = warning
+
+# MA0204: Remove unnecessary partial modifier
+dotnet_diagnostic.MA0204.severity = warning
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index f3e2995a..850b7838 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -604,3 +604,6 @@ dotnet_diagnostic.MA0202.severity = warning
# MA0203: Do not use return tag for void method
dotnet_diagnostic.MA0203.severity = warning
+
+# MA0204: Remove unnecessary partial modifier
+dotnet_diagnostic.MA0204.severity = suggestion
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index 75e27a31..ae54568d 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -604,3 +604,6 @@ dotnet_diagnostic.MA0202.severity = none
# MA0203: Do not use return tag for void method
dotnet_diagnostic.MA0203.severity = none
+
+# MA0204: Remove unnecessary partial modifier
+dotnet_diagnostic.MA0204.severity = none
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index f85ae919..e1e42b9a 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -203,6 +203,7 @@ internal static class RuleIdentifiers
public const string DoNotUseZeroValuedEnumFlagsInFlagChecks = "MA0201";
public const string ConditionalCompilationBranchesAreIdentical = "MA0202";
public const string DoNotUseReturnTagForVoidMethod = "MA0203";
+ public const string RemoveUnnecessaryPartialModifier = "MA0204";
public static string GetHelpUri(string identifier)
{
diff --git a/src/Meziantou.Analyzer/Rules/RemoveUnnecessaryPartialModifierAnalyzer.cs b/src/Meziantou.Analyzer/Rules/RemoveUnnecessaryPartialModifierAnalyzer.cs
new file mode 100644
index 00000000..2ba1946f
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/RemoveUnnecessaryPartialModifierAnalyzer.cs
@@ -0,0 +1,51 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Meziantou.Analyzer.Rules;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class RemoveUnnecessaryPartialModifierAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor Rule = new(
+ RuleIdentifiers.RemoveUnnecessaryPartialModifier,
+ title: "Remove unnecessary partial modifier",
+ messageFormat: "Remove unnecessary partial modifier",
+ RuleCategories.Design,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.RemoveUnnecessaryPartialModifier));
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
+
+ context.RegisterSymbolAction(AnalyzeNamedTypeSymbol, SymbolKind.NamedType);
+ }
+
+ private static void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context)
+ {
+ var symbol = (INamedTypeSymbol)context.Symbol;
+ if (symbol.TypeKind is not (TypeKind.Class or TypeKind.Struct or TypeKind.Interface))
+ return;
+
+ if (symbol.DeclaringSyntaxReferences.Length != 1)
+ return;
+
+ var typeDeclaration = symbol.DeclaringSyntaxReferences[0].GetSyntax(context.CancellationToken) as TypeDeclarationSyntax;
+ if (typeDeclaration is null)
+ return;
+
+ var partialToken = typeDeclaration.Modifiers.FirstOrDefault(modifier => modifier.IsKind(SyntaxKind.PartialKeyword));
+ if (partialToken == default)
+ return;
+
+ context.ReportDiagnostic(Diagnostic.Create(Rule, partialToken.GetLocation()));
+ }
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/RemoveUnnecessaryPartialModifierAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/RemoveUnnecessaryPartialModifierAnalyzerTests.cs
new file mode 100644
index 00000000..40b512a7
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/RemoveUnnecessaryPartialModifierAnalyzerTests.cs
@@ -0,0 +1,225 @@
+using Meziantou.Analyzer.Rules;
+using TestHelper;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class RemoveUnnecessaryPartialModifierAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer()
+ .WithCodeFixProvider();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithSingleDeclaration_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ [|partial|] class Sample
+ {
+ }
+ """;
+
+ const string CodeFix = """
+ class Sample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithSingleDeclaration_PreserveComments_Keyword_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ /*sample*/[|partial|] class Sample
+ {
+ }
+ """;
+
+ const string CodeFix = """
+ /*sample*/class Sample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithSingleDeclaration_PreserveComments_Modifier_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ static /*sample*/[|partial|] class Sample
+ {
+ }
+ """;
+
+ const string CodeFix = """
+ static /*sample*/class Sample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithOtherModifiers_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ public sealed [|partial|] class Sample
+ {
+ }
+ """;
+
+ const string CodeFix = """
+ public sealed class Sample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialRecord_WithSingleDeclaration_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ [|partial|] record Sample;
+ """;
+
+ const string CodeFix = """
+ record Sample;
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialStruct_WithSingleDeclaration_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ [|partial|] struct Sample
+ {
+ }
+ """;
+
+ const string CodeFix = """
+ struct Sample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialInterface_WithSingleDeclaration_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ [|partial|] interface ISample
+ {
+ }
+ """;
+
+ const string CodeFix = """
+ interface ISample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithMultipleDeclarations_NoDiagnostic()
+ {
+ const string SourceCode = """
+ partial class Sample
+ {
+ }
+
+ partial class Sample
+ {
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithPartialMethod_NoDiagnostic()
+ {
+ const string SourceCode = """
+ [|partial|] class Sample
+ {
+ partial void M();
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task PartialClass_WithNestedPartialType_ReportsDiagnostic()
+ {
+ const string SourceCode = """
+ [|partial|] class Sample
+ {
+ partial class Nested
+ {
+ }
+
+ partial class Nested
+ {
+ }
+ }
+ """;
+
+ const string CodeFix = """
+ class Sample
+ {
+ partial class Nested
+ {
+ }
+
+ partial class Nested
+ {
+ }
+ }
+ """;
+
+ await CreateProjectBuilder()
+ .WithSourceCode(SourceCode)
+ .ShouldFixCodeWith(CodeFix)
+ .ValidateAsync();
+ }
+}