Skip to content

Commit 1df576f

Browse files
IeuanWalkerTheCodeTravelerCopilot
authored
Fix: .UseMauiCommunityToolkit() Analyzer when wrapped in preprocessor directives (#2769)
* Improve tests and enhance analyzer detection logic Refactor `UseCommunityToolkitInitializationAnalyzerTests.cs` for better formatting and readability. Added a new test method to check for `UseMauiCommunityToolkit` usage within preprocessor directives. Updated the `VerifyMauiToolkitAnalyzer` method to include a new expected type. In `UseCommunityToolkitInitializationAnalyzer.cs`, modified the `AnalyzeNode` method to directly check the method's text for `UseMauiCommunityToolkit`, improving detection reliability even within preprocessor directives. * Update dotnet-build.yml * Update dotnet-build.yml * Update UseCommunityToolkitInitializationAnalyzerTests.cs * Update src/CommunityToolkit.Maui.Analyzers/UseCommunityToolkitInitializationAnalyzer.cs Co-authored-by: Copilot <[email protected]> * Refactor UseCommunityToolkitInitializationAnalyzer - Removed the `category` constant from the analyzer class. - Updated `AnalyzeNode` to retrieve `methodDeclaration` using `invocationExpression.Ancestors()`. - Added a check to ensure `methodDeclaration` is not null and verifies the absence of `UseMauiCommunityToolkit` calls. - Introduced `HasUseMauiCommunityToolkitCall` method to traverse the syntax tree for `UseMauiCommunityToolkit` calls. - Added `UseMauiCommunityToolkitVisitor` class to efficiently check for invocation expressions. - Improved diagnostic reporting logic to only report when `UseMauiCommunityToolkit` is not found. * Update analyzer tests and simplify method logic - Added necessary `using` directives for testing in `UseCommunityToolkitInitializationAnalyzerTests.cs`. - Refactored `HasUseMauiCommunityToolkitCall` in `UseCommunityToolkitInitializationAnalyzer.cs` to remove the visitor pattern, improving memory efficiency by directly checking method text for the specified method name. * Improve memory efficiency in HasUseMauiCommunityToolkitCall Refactor the method to check for the presence of the method name by iterating through each line of the method's text instead of retrieving the entire text span. This change reduces memory usage and enhances performance by only examining relevant lines that overlap with the method's span. * Optimize HasUseMauiCommunityToolkitCall method Refactor the `HasUseMauiCommunityToolkitCall` method to enhance efficiency by searching for the method name character by character within the method's span. This change eliminates unnecessary string allocations and improves performance. The new implementation uses a boolean flag to quickly determine if a match is found, returning `true` immediately upon a match and `false` if no match is found after checking all characters. * Add check for UseMauiCommunityToolkit calls in analyzer Updated `AnalyzeNode` to report diagnostics if a method contains a call to `UseMauiCommunityToolkit`. Introduced `HasUseMauiCommunityToolkitCall` to efficiently search for the method call in the source text without string allocations. * Refactor tests and improve analyzer efficiency Reformatted `UseCommunityToolkitInitializationAnalyzerTests.cs` for consistency and readability, adding a new test method `VerifyErrorsWhenUseMauiCommunityToolkitIsCommentedOut`. Updated the `VerifyMauiToolkitAnalyzer` method to include this new test case. Enhanced the logic in `UseCommunityToolkitInitializationAnalyzer.cs` to check the syntax tree before performing character-by-character searches, improving analysis efficiency. * Update Camera + MediaElement Analyzers * `dotnet format` * Remove empty line * Improve Performance * Use Static Anonymous Functions --------- Co-authored-by: Brandon Minnick <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Brandon Minnick <[email protected]>
1 parent 28e030c commit 1df576f

File tree

6 files changed

+223
-22
lines changed

6 files changed

+223
-22
lines changed

src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitCameraInitializationAnalyzerTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,43 @@ public static MauiApp CreateMauiApp()
118118
await VerifyCameraToolkitAnalyzer(source, Diagnostic().WithSpan(12, 4, 12, 61).WithSeverity(DiagnosticSeverity.Error));
119119
}
120120

121+
[Fact]
122+
public async Task VerifyNoErrorsWhenUseMauiCommunityToolkitCameraWrapInPreprocessorDirectives()
123+
{
124+
const string source =
125+
/* language=C#-test */
126+
//lang=csharp
127+
"""
128+
namespace CommunityToolkit.Maui.Analyzers.UnitTests
129+
{
130+
using Microsoft.Maui.Controls.Hosting;
131+
using Microsoft.Maui.Hosting;
132+
using CommunityToolkit.Maui;
133+
134+
public static class MauiProgram
135+
{
136+
public static MauiApp CreateMauiApp()
137+
{
138+
var builder = MauiApp.CreateBuilder();
139+
builder.UseMauiApp<Microsoft.Maui.Controls.Application>()
140+
#if ANDROID || IOS
141+
.UseMauiCommunityToolkitCamera()
142+
#endif
143+
.ConfigureFonts(fonts =>
144+
{
145+
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
146+
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
147+
});
148+
149+
return builder.Build();
150+
}
151+
}
152+
}
153+
""";
154+
155+
await VerifyCameraToolkitAnalyzer(source);
156+
}
157+
121158
static Task VerifyCameraToolkitAnalyzer(string source, params IReadOnlyList<DiagnosticResult> diagnosticResults)
122159
{
123160
return VerifyAnalyzerAsync(

src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitInitializationAnalyzerTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,79 @@ public static MauiApp CreateMauiApp()
118118
await VerifyMauiToolkitAnalyzer(source, Diagnostic().WithSpan(12, 4, 12, 61).WithSeverity(DiagnosticSeverity.Error));
119119
}
120120

121+
[Fact]
122+
public async Task VerifyNoErrorsWhenUseMauiCommunityToolkitWrapInPreprocessorDirectives()
123+
{
124+
const string source =
125+
/* language=C#-test */
126+
//lang=csharp
127+
"""
128+
namespace CommunityToolkit.Maui.Analyzers.UnitTests
129+
{
130+
using Microsoft.Maui.Controls.Hosting;
131+
using Microsoft.Maui.Hosting;
132+
using CommunityToolkit.Maui;
133+
134+
public static class MauiProgram
135+
{
136+
public static MauiApp CreateMauiApp()
137+
{
138+
var builder = MauiApp.CreateBuilder();
139+
builder.UseMauiApp<Microsoft.Maui.Controls.Application>()
140+
#if ANDROID || IOS
141+
.UseMauiCommunityToolkit()
142+
#endif
143+
.ConfigureFonts(fonts =>
144+
{
145+
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
146+
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
147+
});
148+
149+
return builder.Build();
150+
}
151+
}
152+
}
153+
""";
154+
155+
156+
await VerifyMauiToolkitAnalyzer(source);
157+
}
158+
159+
[Fact]
160+
public async Task VerifyErrorsWhenUseMauiCommunityToolkitIsCommentedOut()
161+
{
162+
const string source =
163+
/* language=C#-test */
164+
//lang=csharp
165+
"""
166+
namespace CommunityToolkit.Maui.Analyzers.UnitTests
167+
{
168+
using Microsoft.Maui.Controls.Hosting;
169+
using Microsoft.Maui.Hosting;
170+
using CommunityToolkit.Maui;
171+
172+
public static class MauiProgram
173+
{
174+
public static MauiApp CreateMauiApp()
175+
{
176+
var builder = MauiApp.CreateBuilder();
177+
builder.UseMauiApp<Microsoft.Maui.Controls.Application>()
178+
//.UseMauiCommunityToolkit()
179+
.ConfigureFonts(fonts =>
180+
{
181+
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
182+
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
183+
});
184+
185+
return builder.Build();
186+
}
187+
}
188+
}
189+
""";
190+
191+
await VerifyMauiToolkitAnalyzer(source, Diagnostic().WithSpan(12, 4, 12, 61).WithSeverity(DiagnosticSeverity.Error));
192+
}
193+
121194
static Task VerifyMauiToolkitAnalyzer(string source, params IReadOnlyList<DiagnosticResult> expected)
122195
{
123196
return VerifyAnalyzerAsync(

src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitMediaElementInitializationAnalyzerTests.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CommunityToolkit.Maui.MediaElement.Analyzers;
1+
using CommunityToolkit.Maui.Core;
2+
using CommunityToolkit.Maui.MediaElement.Analyzers;
23
using Microsoft.CodeAnalysis;
34
using Microsoft.CodeAnalysis.Testing;
45
using Xunit;
@@ -118,12 +119,49 @@ public static MauiApp CreateMauiApp()
118119
await VerifyMediaElementToolkitAnalyzer(source, Diagnostic().WithSpan(12, 4, 12, 61).WithSeverity(DiagnosticSeverity.Error));
119120
}
120121

122+
[Fact]
123+
public async Task VerifyNoErrorsWhenUseMauiCommunityToolkitMediaElementWrapInPreprocessorDirectives()
124+
{
125+
const string source =
126+
/* language=C#-test */
127+
//lang=csharp
128+
"""
129+
namespace CommunityToolkit.Maui.Analyzers.UnitTests
130+
{
131+
using Microsoft.Maui.Controls.Hosting;
132+
using Microsoft.Maui.Hosting;
133+
using CommunityToolkit.Maui;
134+
135+
public static class MauiProgram
136+
{
137+
public static MauiApp CreateMauiApp()
138+
{
139+
var builder = MauiApp.CreateBuilder();
140+
builder.UseMauiApp<Microsoft.Maui.Controls.Application>()
141+
#if ANDROID || IOS
142+
.UseMauiCommunityToolkitMediaElement()
143+
#endif
144+
.ConfigureFonts(fonts =>
145+
{
146+
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
147+
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
148+
});
149+
150+
return builder.Build();
151+
}
152+
}
153+
}
154+
""";
155+
156+
await VerifyMediaElementToolkitAnalyzer(source);
157+
}
158+
121159
static Task VerifyMediaElementToolkitAnalyzer(string source, params IReadOnlyList<DiagnosticResult> diagnosticResults)
122160
{
123161
return VerifyAnalyzerAsync(
124162
source,
125163
[
126-
typeof(Views.MediaElement) // CommunityToolkit.Maui.MediaElement
164+
typeof(MediaElementOptions) // CommunityToolkit.Maui.MediaElement
127165
],
128166
diagnosticResults);
129167
}

src/CommunityToolkit.Maui.Analyzers/UseCommunityToolkitInitializationAnalyzer.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@ namespace CommunityToolkit.Maui.Analyzers;
1010
public class UseCommunityToolkitInitializationAnalyzer : DiagnosticAnalyzer
1111
{
1212
public const string DiagnosticId = "MCT001";
13-
1413
const string category = "Initialization";
1514
const string useMauiAppMethodName = "UseMauiApp";
1615
const string useMauiCommunityToolkitMethodName = "UseMauiCommunityToolkit";
1716

1817
static readonly LocalizableString title = new LocalizableResourceString(nameof(Resources.InitializationErrorTitle), Resources.ResourceManager, typeof(Resources));
1918
static readonly LocalizableString messageFormat = new LocalizableResourceString(nameof(Resources.InitalizationMessageFormat), Resources.ResourceManager, typeof(Resources));
2019
static readonly LocalizableString description = new LocalizableResourceString(nameof(Resources.InitializationErrorMessage), Resources.ResourceManager, typeof(Resources));
21-
2220
static readonly DiagnosticDescriptor rule = new(DiagnosticId, title, messageFormat, category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: description);
2321

2422
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [rule];
@@ -32,21 +30,42 @@ public override void Initialize(AnalysisContext context)
3230

3331
static void AnalyzeNode(SyntaxNodeAnalysisContext context)
3432
{
35-
if (context.Node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax { Name.Identifier.ValueText: useMauiAppMethodName } } invocationExpression)
33+
if (context.Node is InvocationExpressionSyntax invocationExpression
34+
&& invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression
35+
&& memberAccessExpression.Name.Identifier.ValueText == useMauiAppMethodName)
3636
{
37-
var root = invocationExpression.SyntaxTree.GetRoot();
38-
var methodDeclaration = root.FindNode(invocationExpression.FullSpan)
37+
var methodDeclaration = invocationExpression
3938
.Ancestors()
4039
.OfType<MethodDeclarationSyntax>()
4140
.FirstOrDefault();
4241

43-
if (methodDeclaration is not null
44-
&& !methodDeclaration.DescendantNodes().OfType<InvocationExpressionSyntax>().Any(static n =>
45-
n.Expression is MemberAccessExpressionSyntax { Name.Identifier.ValueText: useMauiCommunityToolkitMethodName }))
42+
if (methodDeclaration is not null && !HasUseMauiCommunityToolkitCall(methodDeclaration))
4643
{
4744
var diagnostic = Diagnostic.Create(rule, invocationExpression.GetLocation());
4845
context.ReportDiagnostic(diagnostic);
4946
}
5047
}
5148
}
49+
50+
static bool HasUseMauiCommunityToolkitCall(MethodDeclarationSyntax methodDeclaration)
51+
{
52+
// Check syntax nodes first (handles active code)
53+
var hasInSyntaxTree = methodDeclaration
54+
.DescendantNodes()
55+
.OfType<InvocationExpressionSyntax>()
56+
.Any(static invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess
57+
&& memberAccess.Name.Identifier.ValueText == useMauiCommunityToolkitMethodName);
58+
59+
if (hasInSyntaxTree)
60+
{
61+
return true;
62+
}
63+
64+
// Check trivia (comments, preprocessor directives, disabled code)
65+
return methodDeclaration
66+
.DescendantTrivia()
67+
.Any(static trivia =>
68+
trivia.IsKind(SyntaxKind.DisabledTextTrivia) &&
69+
trivia.ToString().Contains(useMauiCommunityToolkitMethodName, StringComparison.Ordinal));
70+
}
5271
}

src/CommunityToolkit.Maui.Camera.Analyzers/UseCommunityToolkitCameraInitializationAnalyzer.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,37 @@ static void AnalyzeNode(SyntaxNodeAnalysisContext context)
3636
&& invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression
3737
&& memberAccessExpression.Name.Identifier.ValueText == useMauiAppMethodName)
3838
{
39-
var root = invocationExpression.SyntaxTree.GetRoot();
40-
var methodDeclaration = root.FindNode(invocationExpression.FullSpan)
39+
var methodDeclaration = invocationExpression
4140
.Ancestors()
4241
.OfType<MethodDeclarationSyntax>()
4342
.FirstOrDefault();
4443

45-
if (methodDeclaration is not null
46-
&& !methodDeclaration.DescendantNodes().OfType<InvocationExpressionSyntax>().Any(static n =>
47-
n.Expression is MemberAccessExpressionSyntax m &&
48-
m.Name.Identifier.ValueText == useMauiCommunityToolkitCameraMethodName))
44+
if (methodDeclaration is not null && !HasUseMauiCommunityToolkitCameraCall(methodDeclaration))
4945
{
5046
var diagnostic = Diagnostic.Create(rule, invocationExpression.GetLocation());
5147
context.ReportDiagnostic(diagnostic);
5248
}
5349
}
5450
}
51+
52+
static bool HasUseMauiCommunityToolkitCameraCall(MethodDeclarationSyntax methodDeclaration)
53+
{
54+
// Check syntax nodes first (handles active code)
55+
var hasInSyntaxTree = methodDeclaration
56+
.DescendantNodes()
57+
.OfType<InvocationExpressionSyntax>()
58+
.Any(static invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess
59+
&& memberAccess.Name.Identifier.ValueText == useMauiCommunityToolkitCameraMethodName);
60+
61+
if (hasInSyntaxTree)
62+
{
63+
return true;
64+
}
65+
66+
// Check trivia (comments, preprocessor directives, disabled code)
67+
return methodDeclaration
68+
.DescendantTrivia()
69+
.Any(static trivia => trivia.IsKind(SyntaxKind.DisabledTextTrivia)
70+
&& trivia.ToString().Contains(useMauiCommunityToolkitCameraMethodName, StringComparison.Ordinal));
71+
}
5572
}

src/CommunityToolkit.Maui.MediaElement.Analyzers/UseCommunityToolkitMediaElementInitializationAnalyzer.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,37 @@ static void AnalyzeNode(SyntaxNodeAnalysisContext context)
3636
&& invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression
3737
&& memberAccessExpression.Name.Identifier.ValueText == useMauiAppMethodName)
3838
{
39-
var root = invocationExpression.SyntaxTree.GetRoot();
40-
var methodDeclaration = root.FindNode(invocationExpression.FullSpan)
39+
var methodDeclaration = invocationExpression
4140
.Ancestors()
4241
.OfType<MethodDeclarationSyntax>()
4342
.FirstOrDefault();
4443

45-
if (methodDeclaration is not null
46-
&& !methodDeclaration.DescendantNodes().OfType<InvocationExpressionSyntax>().Any(static n =>
47-
n.Expression is MemberAccessExpressionSyntax m &&
48-
m.Name.Identifier.ValueText == useMauiCommunityToolkitMediaElementMethodName))
44+
if (methodDeclaration is not null && !HasUseMauiCommunityToolkitMediaElementCall(methodDeclaration))
4945
{
5046
var diagnostic = Diagnostic.Create(rule, invocationExpression.GetLocation());
5147
context.ReportDiagnostic(diagnostic);
5248
}
5349
}
5450
}
51+
52+
static bool HasUseMauiCommunityToolkitMediaElementCall(MethodDeclarationSyntax methodDeclaration)
53+
{
54+
// Check syntax nodes first (handles active code)
55+
var hasInSyntaxTree = methodDeclaration
56+
.DescendantNodes()
57+
.OfType<InvocationExpressionSyntax>()
58+
.Any(static invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess
59+
&& memberAccess.Name.Identifier.ValueText == useMauiCommunityToolkitMediaElementMethodName);
60+
61+
if (hasInSyntaxTree)
62+
{
63+
return true;
64+
}
65+
66+
// Check trivia (comments, preprocessor directives, disabled code)
67+
return methodDeclaration
68+
.DescendantTrivia()
69+
.Any(static trivia => trivia.IsKind(SyntaxKind.DisabledTextTrivia)
70+
&& trivia.ToString().Contains(useMauiCommunityToolkitMediaElementMethodName, StringComparison.Ordinal));
71+
}
5572
}

0 commit comments

Comments
 (0)