diff --git a/Avalonia.sln b/Avalonia.sln
index f33b782479..acbf1e6bbb 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -233,7 +233,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\R
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\Avalonia.Analyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
ProjectSection(SolutionItems) = preProject
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
index 7d021d051f..dffd3098c3 100644
--- a/build/DevAnalyzers.props
+++ b/build/DevAnalyzers.props
@@ -5,7 +5,7 @@
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
-
+
diff --git a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
similarity index 97%
rename from src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj
rename to src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
index 31b8d08541..39eaab1289 100644
--- a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj
+++ b/src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
@@ -11,7 +11,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
similarity index 100%
rename from src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
rename to src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs
similarity index 99%
rename from src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs
rename to src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs
index d1d9071d17..d1ffd82f99 100644
--- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs
+++ b/src/tools/Avalonia.Analyzers/AvaloniaPropertyAnalyzer.cs
@@ -14,7 +14,6 @@ using Microsoft.CodeAnalysis.Operations;
namespace Avalonia.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
-[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
{
private const string Category = "AvaloniaProperty";
diff --git a/src/tools/Avalonia.Analyzers/GlobalSuppressions.cs b/src/tools/Avalonia.Analyzers/GlobalSuppressions.cs
new file mode 100644
index 0000000000..9428b904b8
--- /dev/null
+++ b/src/tools/Avalonia.Analyzers/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
diff --git a/src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs b/src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs
new file mode 100644
index 0000000000..6fbfe28bd8
--- /dev/null
+++ b/src/tools/Avalonia.Analyzers/OnPropertyChangedOverrideAnalyzer.cs
@@ -0,0 +1,59 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Avalonia.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class OnPropertyChangedOverrideAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "AVA2001";
+
+ private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId,
+ "Missing invoke base.OnPropertyChanged",
+ "Method '{0}' do not invoke base.{0}",
+ "Potential issue",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "The OnPropertyChanged of the base class was not invoked in the override method declaration, which could lead to unwanted behavior.");
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeMethod, SyntaxKind.MethodDeclaration);
+ }
+
+ private static void AnalyzeMethod(SyntaxNodeAnalysisContext context)
+ {
+ var method = (MethodDeclarationSyntax)context.Node;
+ if (context.SemanticModel.GetDeclaredSymbol(method, context.CancellationToken) is IMethodSymbol currentMethod
+ && currentMethod.Name == "OnPropertyChanged"
+ && currentMethod.OverriddenMethod is IMethodSymbol originalMethod)
+ {
+ var baseInvocations = method.Body?.DescendantNodes().OfType();
+ if (baseInvocations?.Any() == true)
+ {
+ foreach (var baseInvocation in baseInvocations)
+ {
+ if (baseInvocation.Parent is SyntaxNode parent)
+ {
+ var targetSymbol = context.SemanticModel.GetSymbolInfo(parent, context.CancellationToken);
+ if (SymbolEqualityComparer.Default.Equals(targetSymbol.Symbol, originalMethod))
+ {
+ return;
+ }
+ }
+ }
+ }
+ context.ReportDiagnostic(Diagnostic.Create(Rule, currentMethod.Locations[0], currentMethod.Name));
+ }
+ }
+
+}