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