Skip to content

Commit 77ede78

Browse files
authored
Merge pull request #979 from CommunityToolkit/dev/winrt-analyzers
Add MVVM Toolkit analyzers for WinRT scenarios
2 parents 1fe18f4 + ec0a331 commit 77ede78

17 files changed

+1083
-59
lines changed

src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixPr
5757
});
5858

5959
/// <inheritdoc/>
60-
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialPropertyId);
60+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(
61+
UseObservablePropertyOnPartialPropertyId,
62+
WinRTObservablePropertyOnFieldsIsNotAotCompatibleId);
6163

6264
/// <inheritdoc/>
6365
public override FixAllProvider? GetFixAllProvider()
@@ -77,6 +79,14 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
7779
return;
7880
}
7981

82+
SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!;
83+
84+
// If the language is not preview, we cannot apply this code fix (as it would generate invalid C# code)
85+
if (!semanticModel.Compilation.IsLanguageVersionPreview())
86+
{
87+
return;
88+
}
89+
8090
// Retrieve the properties passed by the analyzer
8191
if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName ||
8292
diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName)
@@ -101,7 +111,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
101111
context.RegisterCodeFix(
102112
CodeAction.Create(
103113
title: "Use a partial property",
104-
createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, fieldName, propertyName, context.CancellationToken),
114+
createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, semanticModel, fieldName, propertyName, context.CancellationToken),
105115
equivalenceKey: "Use a partial property"),
106116
diagnostic);
107117
}
@@ -113,6 +123,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
113123
/// <param name="document">The original document being fixed.</param>
114124
/// <param name="root">The original tree root belonging to the current document.</param>
115125
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the field being updated.</param>
126+
/// <param name="semanticModel">The semantic model for <paramref name="document"/>.</param>
116127
/// <param name="fieldName">The name of the annotated field.</param>
117128
/// <param name="propertyName">The name of the generated property.</param>
118129
/// <param name="cancellationToken">The cancellation token for the operation.</param>
@@ -121,11 +132,12 @@ private static async Task<Document> ConvertToPartialProperty(
121132
Document document,
122133
SyntaxNode root,
123134
FieldDeclarationSyntax fieldDeclaration,
135+
SemanticModel semanticModel,
124136
string fieldName,
125137
string propertyName,
126138
CancellationToken cancellationToken)
127139
{
128-
SemanticModel semanticModel = (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!;
140+
await Task.CompletedTask;
129141

130142
// Try to get all necessary type symbols to process the attributes
131143
if (!semanticModel.Compilation.TryBuildNamedTypeSymbolMap(MvvmToolkitAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary<string, INamedTypeSymbol>? toolkitTypeSymbols) ||

src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,9 @@ Rule ID | Category | Severity | Notes
8484
--------|----------|----------|-------
8585
MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041
8686
MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042
87-
MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043
88-
MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043
87+
MVVMTK0043 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043
88+
MVVMTK0044 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0044
89+
MVVMTK0045 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0045
90+
MVVMTK0046 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0046
91+
MVVMTK0047 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0047
92+
MVVMTK0048 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0048

src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
4242
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
4343
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs" />
44+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs" />
45+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs" />
46+
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs" />
4447
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs" />
4548
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidTargetObservablePropertyAttributeAnalyzer.cs" />
4649
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
@@ -58,6 +61,7 @@
5861
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs" />
5962
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\DiagnosticDescriptors.cs" />
6063
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\SuppressionDescriptors.cs" />
64+
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AnalyzerConfigOptionsExtensions.cs" />
6165
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AccessibilityExtensions.cs" />
6266
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AttributeDataExtensions.cs" />
6367
<Compile Include="$(MSBuildThisFileDirectory)Extensions\CompilationExtensions.cs" />

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,9 @@ public static bool TryGetInfo(
153153

154154
token.ThrowIfCancellationRequested();
155155

156-
// Override the property changing support if explicitly disabled
157-
shouldInvokeOnPropertyChanging &= GetEnableINotifyPropertyChangingSupport(options);
156+
// Override the property changing support if explicitly disabled.
157+
// This setting is enabled by default, for backwards compatibility.
158+
shouldInvokeOnPropertyChanging &= options.GetMSBuildBooleanPropertyValue("MvvmToolkitEnableINotifyPropertyChangingSupport", defaultValue: true);
158159

159160
token.ThrowIfCancellationRequested();
160161

@@ -378,27 +379,6 @@ public static bool TryGetInfo(
378379
return true;
379380
}
380381

381-
/// <summary>
382-
/// Gets the value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property.
383-
/// </summary>
384-
/// <param name="options">The options in use for the generator.</param>
385-
/// <returns>The value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property.</returns>
386-
public static bool GetEnableINotifyPropertyChangingSupport(AnalyzerConfigOptions options)
387-
{
388-
if (options.TryGetValue("build_property.MvvmToolkitEnableINotifyPropertyChangingSupport", out string? propertyValue))
389-
{
390-
if (bool.TryParse(propertyValue, out bool enableINotifyPropertyChangingSupport))
391-
{
392-
return enableINotifyPropertyChangingSupport;
393-
}
394-
}
395-
396-
// This setting is enabled by default, for backwards compatibility.
397-
// Note that this path should never be reached, as the default
398-
// value is also set in a .targets file bundled in the package.
399-
return true;
400-
}
401-
402382
/// <summary>
403383
/// Validates the containing type for a given field being annotated.
404384
/// </summary>

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public override void Initialize(AnalysisContext context)
3636
return;
3737
}
3838

39+
// If CsWinRT is in AOT-optimization mode, disable this analyzer, as the WinRT one will produce a warning instead
40+
if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation))
41+
{
42+
return;
43+
}
44+
3945
// Get the symbol for [ObservableProperty]
4046
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
4147
{
@@ -73,7 +79,7 @@ public override void Initialize(AnalysisContext context)
7379
.Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name)
7480
.Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)),
7581
fieldSymbol.ContainingType,
76-
fieldSymbol));
82+
fieldSymbol.Name));
7783
}, SymbolKind.Field);
7884
});
7985
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#if ROSLYN_4_11_0_OR_GREATER
6+
7+
using System.Collections.Generic;
8+
using System.Collections.Immutable;
9+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
13+
14+
namespace CommunityToolkit.Mvvm.SourceGenerators;
15+
16+
/// <summary>
17+
/// A diagnostic analyzer that generates an error when <c>[GeneratedBindableCustomProperty]</c> is used on types with invalid generated base members.
18+
/// </summary>
19+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
20+
public sealed class WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer : DiagnosticAnalyzer
21+
{
22+
/// <inheritdoc/>
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
24+
WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField,
25+
WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand);
26+
27+
/// <inheritdoc/>
28+
public override void Initialize(AnalysisContext context)
29+
{
30+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
31+
context.EnableConcurrentExecution();
32+
33+
context.RegisterCompilationStartAction(static context =>
34+
{
35+
// This analyzer is only enabled when CsWinRT is also used
36+
if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsUsingWindowsRuntimePack())
37+
{
38+
return;
39+
}
40+
41+
// Get the symbol for [ObservableProperty], [RelayCommand] and [GeneratedBindableCustomProperty]
42+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
43+
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is not INamedTypeSymbol relayCommandSymbol ||
44+
context.Compilation.GetTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute") is not INamedTypeSymbol generatedBindableCustomPropertySymbol)
45+
{
46+
return;
47+
}
48+
49+
context.RegisterSymbolAction(context =>
50+
{
51+
// Ensure we do have a valid type
52+
if (context.Symbol is not INamedTypeSymbol typeSymbol)
53+
{
54+
return;
55+
}
56+
57+
// We only care about it if it's using [GeneratedBindableCustomProperty]
58+
if (!typeSymbol.TryGetAttributeWithType(generatedBindableCustomPropertySymbol, out AttributeData? generatedBindableCustomPropertyAttribute))
59+
{
60+
return;
61+
}
62+
63+
// Warn on all [ObservableProperty] fields
64+
foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, observablePropertySymbol))
65+
{
66+
context.ReportDiagnostic(Diagnostic.Create(
67+
WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField,
68+
generatedBindableCustomPropertyAttribute.GetLocation(),
69+
typeSymbol,
70+
fieldSymbol.ContainingType,
71+
fieldSymbol.Name));
72+
}
73+
74+
// Warn on all [RelayCommand] methods
75+
foreach (IMethodSymbol methodSymbol in FindRelayCommandMethods(typeSymbol, relayCommandSymbol))
76+
{
77+
context.ReportDiagnostic(Diagnostic.Create(
78+
WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand,
79+
generatedBindableCustomPropertyAttribute.GetLocation(),
80+
typeSymbol,
81+
methodSymbol));
82+
}
83+
}, SymbolKind.NamedType);
84+
});
85+
}
86+
87+
/// <summary>
88+
/// Finds all methods in the base types that have the <c>[RelayCommand]</c> attribute.
89+
/// </summary>
90+
/// <param name="typeSymbol">The <see cref="INamedTypeSymbol"/> instance to inspect.</param>
91+
/// <param name="relayCommandSymbol">The symbol for the <c>[RelayCommand]</c></param>
92+
/// <returns>All <see cref="IMethodSymbol"/> instances for matching members.</returns>
93+
private static IEnumerable<IMethodSymbol> FindRelayCommandMethods(INamedTypeSymbol typeSymbol, INamedTypeSymbol relayCommandSymbol)
94+
{
95+
// Check whether the base type (if any) is from the same assembly, and stop if it isn't. We do not
96+
// want to include methods from the same type, as those will already be caught by another analyzer.
97+
if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType?.ContainingAssembly))
98+
{
99+
yield break;
100+
}
101+
102+
foreach (ISymbol memberSymbol in typeSymbol.BaseType.GetAllMembersFromSameAssembly())
103+
{
104+
if (memberSymbol is IMethodSymbol methodSymbol &&
105+
methodSymbol.HasAttributeWithType(relayCommandSymbol))
106+
{
107+
yield return methodSymbol;
108+
}
109+
}
110+
}
111+
112+
/// <summary>
113+
/// Finds all fields in the base types that have the <c>[ObservableProperty]</c> attribute.
114+
/// </summary>
115+
/// <param name="typeSymbol">The <see cref="INamedTypeSymbol"/> instance to inspect.</param>
116+
/// <param name="observablePropertySymbol">The symbol for the <c>[ObservableProperty]</c></param>
117+
/// <returns>All <see cref="IFieldSymbol"/> instances for matching members.</returns>
118+
private static IEnumerable<IFieldSymbol> FindObservablePropertyFields(INamedTypeSymbol typeSymbol, INamedTypeSymbol observablePropertySymbol)
119+
{
120+
// Skip the base type if not from the same assembly, same as above
121+
if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType?.ContainingAssembly))
122+
{
123+
yield break;
124+
}
125+
126+
foreach (ISymbol memberSymbol in typeSymbol.BaseType.GetAllMembersFromSameAssembly())
127+
{
128+
if (memberSymbol is IFieldSymbol fieldSymbol &&
129+
fieldSymbol.HasAttributeWithType(observablePropertySymbol))
130+
{
131+
yield return fieldSymbol;
132+
}
133+
}
134+
}
135+
}
136+
137+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#if ROSLYN_4_11_0_OR_GREATER
6+
7+
using System.Collections.Immutable;
8+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
12+
13+
namespace CommunityToolkit.Mvvm.SourceGenerators;
14+
15+
/// <summary>
16+
/// A diagnostic analyzer that generates an error when <c>[ObservableProperty]</c> is used on a field in a scenario where it wouldn't be AOT compatible.
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public sealed class WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer : DiagnosticAnalyzer
20+
{
21+
/// <inheritdoc/>
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(WinRTObservablePropertyOnFieldsIsNotAotCompatible);
23+
24+
/// <inheritdoc/>
25+
public override void Initialize(AnalysisContext context)
26+
{
27+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
28+
context.EnableConcurrentExecution();
29+
30+
context.RegisterCompilationStartAction(static context =>
31+
{
32+
// This analyzer is only enabled in cases where CsWinRT is producing AOT-compatible code
33+
if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation))
34+
{
35+
return;
36+
}
37+
38+
// Get the symbol for [ObservableProperty]
39+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
40+
{
41+
return;
42+
}
43+
44+
context.RegisterSymbolAction(context =>
45+
{
46+
// Ensure we do have a valid field
47+
if (context.Symbol is not IFieldSymbol fieldSymbol)
48+
{
49+
return;
50+
}
51+
52+
// Emit a diagnostic if the field is using the [ObservableProperty] attribute
53+
if (fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute))
54+
{
55+
context.ReportDiagnostic(Diagnostic.Create(
56+
WinRTObservablePropertyOnFieldsIsNotAotCompatible,
57+
observablePropertyAttribute.GetLocation(),
58+
ImmutableDictionary.Create<string, string?>()
59+
.Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name)
60+
.Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)),
61+
fieldSymbol.ContainingType,
62+
fieldSymbol.Name));
63+
}
64+
}, SymbolKind.Field);
65+
});
66+
}
67+
}
68+
69+
#endif

0 commit comments

Comments
 (0)