From ca746292f44f48589e5f637f57dfbcdfe6671684 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 14 Apr 2026 10:56:15 -0700 Subject: [PATCH 1/2] Add new analyzer to detect invalid Frame.Navigate calls --- .gitignore | 1 + docs/rules/WinUIEx1003.md | 27 +++++++ src/Directory.Build.targets | 2 +- .../WinUIExFrameNavigateAnalyzerTests.cs | 71 +++++++++++++++++ .../AnalyzerReleases.Shipped.md | 3 +- .../WinUIEx.Analyzers/Resources.Designer.cs | 27 +++++++ .../WinUIEx.Analyzers/Resources.resx | 11 ++- .../WinUIExFrameNavigateAnalyzer.cs | 79 +++++++++++++++++++ src/WinUIEx/WinUIEx.csproj | 3 +- 9 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 docs/rules/WinUIEx1003.md create mode 100644 src/WinUIEx.Analyzers/WinUIEx.Analyzers.Test/WinUIExFrameNavigateAnalyzerTests.cs create mode 100644 src/WinUIEx.Analyzers/WinUIEx.Analyzers/WinUIExFrameNavigateAnalyzer.cs diff --git a/.gitignore b/.gitignore index 9119864d..4de78ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -350,3 +350,4 @@ MigrationBackup/ .ionide/ .tools docs/memberpage.2.58.0 +.dotnet diff --git a/docs/rules/WinUIEx1003.md b/docs/rules/WinUIEx1003.md new file mode 100644 index 00000000..7628d6f6 --- /dev/null +++ b/docs/rules/WinUIEx1003.md @@ -0,0 +1,27 @@ +## WinUIEX1003: Frame.Navigate target must inherit from Page. + +`Frame.Navigate(Type)` only supports types that inherit from `Microsoft.UI.Xaml.Controls.Page`. Passing any other type will fail at runtime, so the analyzer reports this as a build error. + +|Item|Value| +|-|-| +|Category|Usage| +|Enabled|True| +|Severity|Error| +|CodeFix|False| +--- + +### Example + +This will trigger the analyzer: +```cs +frame.Navigate(typeof(MyViewModel)); +``` + +Use a page type instead: +```cs +frame.Navigate(typeof(MyPage)); +``` + +### References + + - [Frame.Navigate(Type) Method](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.frame.navigate) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 7b053627..50fca1a0 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,7 +17,7 @@ Morten Nielsen - https://xaml.dev Morten Nielsen - https://xaml.dev logo.png - 2.9.0 + 2.9.1 diff --git a/src/WinUIEx.Analyzers/WinUIEx.Analyzers.Test/WinUIExFrameNavigateAnalyzerTests.cs b/src/WinUIEx.Analyzers/WinUIEx.Analyzers.Test/WinUIExFrameNavigateAnalyzerTests.cs new file mode 100644 index 00000000..5b0702b2 --- /dev/null +++ b/src/WinUIEx.Analyzers/WinUIEx.Analyzers.Test/WinUIExFrameNavigateAnalyzerTests.cs @@ -0,0 +1,71 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading.Tasks; + +namespace WinUIEx.Analyzers.Test +{ + [TestClass] + public class WinUIExFrameNavigateAnalyzerTests : BaseAnalyzersUnitTest + { + [TestMethod] + public async Task Frame_Navigate_With_NonPage_Type() + { + var testCode = @" + using System; + using Microsoft.UI.Xaml.Controls; + namespace ConsoleApplication1 + { + class MyViewModel { } + + class MyClass + { + public void MethodName(Frame frame) + { + frame.Navigate({|#0:typeof(MyViewModel)|}); + } + } + }"; + var expected = Diagnostic("WinUIEx1003").WithLocation(0).WithArguments("ConsoleApplication1.MyViewModel", "Microsoft.UI.Xaml.Controls.Page"); + await VerifyAnalyzerAsync(testCode, expected); + } + + [TestMethod] + public async Task Frame_Navigate_With_Page_Subclass() + { + var testCode = @" + using System; + using Microsoft.UI.Xaml.Controls; + namespace ConsoleApplication1 + { + class MyPage : Page { } + + class MyClass + { + public void MethodName(Frame frame) + { + frame.Navigate(typeof(MyPage)); + } + } + }"; + await VerifyAnalyzerAsync(testCode); + } + + [TestMethod] + public async Task Frame_Navigate_With_Page_Base_Type() + { + var testCode = @" + using System; + using Microsoft.UI.Xaml.Controls; + namespace ConsoleApplication1 + { + class MyClass + { + public void MethodName(Frame frame) + { + frame.Navigate(typeof(Page)); + } + } + }"; + await VerifyAnalyzerAsync(testCode); + } + } +} diff --git a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/AnalyzerReleases.Shipped.md b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/AnalyzerReleases.Shipped.md index 6af735d0..f58691a9 100644 --- a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/AnalyzerReleases.Shipped.md @@ -5,4 +5,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- WinUIEx1001 | Usage | Warning | The member will always be null, [Documentation](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1001.html) -WinUIEx1002 | Usage | Warning | Dispatcher must be replaced with DispatcherQueue, [Documentation](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1002.html) \ No newline at end of file +WinUIEx1002 | Usage | Warning | Dispatcher must be replaced with DispatcherQueue, [Documentation](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1002.html) +WinUIEx1003 | Usage | Error | Frame.Navigate target must inherit from Page, [Documentation](https://dotmorten.github.io/WinUIEx/rules/WinUIEx1003.html) diff --git a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.Designer.cs b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.Designer.cs index 7de89087..212989cd 100644 --- a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.Designer.cs +++ b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.Designer.cs @@ -113,5 +113,32 @@ internal static string DispatcherTitle { return ResourceManager.GetString("DispatcherTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to Frame.Navigate only supports page types that inherit from Microsoft.UI.Xaml.Controls.Page.. + /// + internal static string NavigateTypeDescription { + get { + return ResourceManager.GetString("NavigateTypeDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type '{0}' cannot be used with Frame.Navigate because it does not inherit from '{1}'.. + /// + internal static string NavigateTypeMessageFormat { + get { + return ResourceManager.GetString("NavigateTypeMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Frame.Navigate target must inherit from Page. + /// + internal static string NavigateTypeTitle { + get { + return ResourceManager.GetString("NavigateTypeTitle", resourceCulture); + } + } } } diff --git a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.resx b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.resx index db7b2699..698d4148 100644 --- a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.resx +++ b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/Resources.resx @@ -135,4 +135,13 @@ Dispatcher must be replaced with DispatcherQueue - \ No newline at end of file + + Frame.Navigate only supports page types that inherit from Microsoft.UI.Xaml.Controls.Page. + + + Type '{0}' cannot be used with Frame.Navigate because it does not inherit from '{1}'. + + + Frame.Navigate target must inherit from Page + + diff --git a/src/WinUIEx.Analyzers/WinUIEx.Analyzers/WinUIExFrameNavigateAnalyzer.cs b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/WinUIExFrameNavigateAnalyzer.cs new file mode 100644 index 00000000..708dbd82 --- /dev/null +++ b/src/WinUIEx.Analyzers/WinUIEx.Analyzers/WinUIExFrameNavigateAnalyzer.cs @@ -0,0 +1,79 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using System.Collections.Immutable; + +namespace WinUIEx.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class WinUIExFrameNavigateAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId1003 = "WinUIEx1003"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.NavigateTypeTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.NavigateTypeMessageFormat), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.NavigateTypeDescription), Resources.ResourceManager, typeof(Resources)); + private const string Category = "Usage"; + + private static readonly DiagnosticDescriptor NavigateTypeRule = new DiagnosticDescriptor( + DiagnosticId1003, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: Description, + helpLinkUri: "https://dotmorten.github.io/WinUIEx/rules/WinUIEx1003.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(NavigateTypeRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeInvocationOperation, OperationKind.Invocation); + } + + private void AnalyzeInvocationOperation(OperationAnalysisContext context) + { + var invocation = context.Operation as IInvocationOperation; + if (invocation == null || !IsFrameNavigateWithTypeParameter(invocation.TargetMethod) || invocation.Arguments.Length == 0) + return; + + var typeOfArgument = invocation.Arguments[0].Value as ITypeOfOperation; + if (typeOfArgument == null || typeOfArgument.TypeOperand == null) + return; + + var pageType = context.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Controls.Page"); + if (pageType == null || InheritsFrom(typeOfArgument.TypeOperand, pageType)) + return; + + var diagnostic = Diagnostic.Create( + NavigateTypeRule, + invocation.Arguments[0].Syntax.GetLocation(), + typeOfArgument.TypeOperand.ToDisplayString(), + pageType.ToDisplayString()); + context.ReportDiagnostic(diagnostic); + } + + private static bool IsFrameNavigateWithTypeParameter(IMethodSymbol method) + { + return method != null && + method.Name == "Navigate" && + method.ContainingType?.ToDisplayString() == "Microsoft.UI.Xaml.Controls.Frame" && + method.Parameters.Length > 0 && + method.Parameters[0].Type.ToDisplayString() == "System.Type"; + } + + private static bool InheritsFrom(ITypeSymbol type, ITypeSymbol baseType) + { + for (var current = type; current != null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current, baseType)) + return true; + } + + return false; + } + } +} diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index e3f2d82b..876d816f 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -25,8 +25,7 @@ WinUIEx WinUI Extensions - - Added support for WinUI 3 based flyouts in the system tray. - - Added extension method for assigning transparent regions to a window (Issue #235). + - Added a new analyzer to call out when invalid type as a parameter for `Frame.Navigate`. #260 From 0b1e5ac71458303a13a51c6baf15485c1d5cbb7c Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 14 Apr 2026 11:03:28 -0700 Subject: [PATCH 2/2] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WinUIEx/WinUIEx.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index 876d816f..49edd037 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -25,7 +25,7 @@ WinUIEx WinUI Extensions - - Added a new analyzer to call out when invalid type as a parameter for `Frame.Navigate`. #260 + - Added a new analyzer to call out when an invalid type is passed to `Frame.Navigate`. #260