Skip to content

Commit 28c6a8c

Browse files
authored
Merge pull request #1010 from CommunityToolkit/dev/more-analyzers
Add more analyzers, enable unit tests for partial properties
2 parents fce5bf8 + 889cc08 commit 28c6a8c

16 files changed

+2274
-24
lines changed

dotnet Community Toolkit.sln

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeF
9191
EndProject
9292
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4120", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj", "{98572004-D29A-486E-8053-6D409557CE44}"
9393
EndProject
94+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Roslyn4120.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj", "{87BF1537-935A-414D-8318-458F61A6E562}"
95+
EndProject
9496
Global
9597
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9698
Debug|Any CPU = Debug|Any CPU
@@ -525,6 +527,26 @@ Global
525527
{98572004-D29A-486E-8053-6D409557CE44}.Release|x64.Build.0 = Release|Any CPU
526528
{98572004-D29A-486E-8053-6D409557CE44}.Release|x86.ActiveCfg = Release|Any CPU
527529
{98572004-D29A-486E-8053-6D409557CE44}.Release|x86.Build.0 = Release|Any CPU
530+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
531+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.Build.0 = Debug|Any CPU
532+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.ActiveCfg = Debug|Any CPU
533+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.Build.0 = Debug|Any CPU
534+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.ActiveCfg = Debug|Any CPU
535+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.Build.0 = Debug|Any CPU
536+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.ActiveCfg = Debug|Any CPU
537+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.Build.0 = Debug|Any CPU
538+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.ActiveCfg = Debug|Any CPU
539+
{87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.Build.0 = Debug|Any CPU
540+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.ActiveCfg = Release|Any CPU
541+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.Build.0 = Release|Any CPU
542+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.ActiveCfg = Release|Any CPU
543+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.Build.0 = Release|Any CPU
544+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.ActiveCfg = Release|Any CPU
545+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.Build.0 = Release|Any CPU
546+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.ActiveCfg = Release|Any CPU
547+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.Build.0 = Release|Any CPU
548+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.ActiveCfg = Release|Any CPU
549+
{87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.Build.0 = Release|Any CPU
528550
EndGlobalSection
529551
GlobalSection(SolutionProperties) = preSolution
530552
HideSolutionNode = FALSE
@@ -548,6 +570,7 @@ Global
548570
{ECFE93AA-4B98-4292-B3FA-9430D513B4F9} = {B30036C4-D514-4E5B-A323-587A061772CE}
549571
{4FCD501C-1BB5-465C-AD19-356DAB6600C6} = {B30036C4-D514-4E5B-A323-587A061772CE}
550572
{C342302D-A263-42D6-B8EE-01DEF8192690} = {B30036C4-D514-4E5B-A323-587A061772CE}
573+
{87BF1537-935A-414D-8318-458F61A6E562} = {B30036C4-D514-4E5B-A323-587A061772CE}
551574
EndGlobalSection
552575
GlobalSection(ExtensibilityGlobals) = postSolution
553576
SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345}
@@ -556,6 +579,7 @@ Global
556579
tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{4fcd501c-1bb5-465c-ad19-356dab6600c6}*SharedItemsImports = 5
557580
tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{5b44f7f1-dca2-4776-924e-a266f7bbf753}*SharedItemsImports = 5
558581
src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13
582+
tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{87bf1537-935a-414d-8318-458f61a6e562}*SharedItemsImports = 5
559583
src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{98572004-d29a-486e-8053-6d409557ce44}*SharedItemsImports = 5
560584
src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{a2ebda90-b720-430d-83f5-c6bcc355232c}*SharedItemsImports = 13
561585
tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.403",
3+
"version": "9.0.100",
44
"rollForward": "latestFeature",
55
"allowPrerelease": false
66
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,7 @@ MVVMTK0047 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
9292
MVVMTK0048 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0048
9393
MVVMTK0049 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0049
9494
MVVMTK0050 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050
95-
MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050
95+
MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0051
96+
MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0052
97+
MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053
98+
MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
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\InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer.cs" />
4546
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs" />
4647
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs" />

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain
7171
/// <param name="node">The <see cref="MemberDeclarationSyntax"/> instance to process.</param>
7272
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
7373
/// <returns>Whether <paramref name="node"/> is valid.</returns>
74-
public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel semanticModel)
74+
public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax node, SemanticModel semanticModel)
7575
{
7676
// At least C# 8 is always required
7777
if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8))
@@ -90,6 +90,35 @@ public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel
9090
return true;
9191
}
9292

93+
/// <summary>
94+
/// Performs additional checks before running the core generation logic.
95+
/// </summary>
96+
/// <param name="memberSymbol">The input <see cref="ISymbol"/> instance to process.</param>
97+
/// <returns>Whether <paramref name="memberSymbol"/> is valid.</returns>
98+
public static bool IsCandidateSymbolValid(ISymbol memberSymbol)
99+
{
100+
#if ROSLYN_4_12_0_OR_GREATER
101+
// We only need additional checks for properties (Roslyn already validates things for fields in our scenarios)
102+
if (memberSymbol is IPropertySymbol propertySymbol)
103+
{
104+
// Ensure that the property declaration is a partial definition with no implementation
105+
if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null })
106+
{
107+
return false;
108+
}
109+
110+
// Also ignore all properties that have an invalid declaration
111+
if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly || propertySymbol.Type.IsRefLikeType)
112+
{
113+
return false;
114+
}
115+
}
116+
#endif
117+
118+
// We assume all other cases are supported (other failure cases will be detected later)
119+
return true;
120+
}
121+
93122
/// <summary>
94123
/// Gets the candidate <see cref="MemberDeclarationSyntax"/> after the initial filtering.
95124
/// </summary>
@@ -140,13 +169,11 @@ public static bool TryGetInfo(
140169
return false;
141170
}
142171

143-
using ImmutableArrayBuilder<DiagnosticInfo> builder = ImmutableArrayBuilder<DiagnosticInfo>.Rent();
144-
145172
// Validate the target type
146173
if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging))
147174
{
148175
propertyInfo = null;
149-
diagnostics = builder.ToImmutable();
176+
diagnostics = ImmutableArray<DiagnosticInfo>.Empty;
150177

151178
return false;
152179
}
@@ -168,7 +195,7 @@ public static bool TryGetInfo(
168195
if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration))
169196
{
170197
propertyInfo = null;
171-
diagnostics = builder.ToImmutable();
198+
diagnostics = ImmutableArray<DiagnosticInfo>.Empty;
172199

173200
// If the generated property would collide, skip generating it entirely. This makes sure that
174201
// users only get the helpful diagnostic about the collision, and not the normal compiler error
@@ -182,7 +209,7 @@ public static bool TryGetInfo(
182209
if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol)))
183210
{
184211
propertyInfo = null;
185-
diagnostics = builder.ToImmutable();
212+
diagnostics = ImmutableArray<DiagnosticInfo>.Empty;
186213

187214
return false;
188215
}
@@ -232,6 +259,8 @@ public static bool TryGetInfo(
232259

233260
token.ThrowIfCancellationRequested();
234261

262+
using ImmutableArrayBuilder<DiagnosticInfo> builder = ImmutableArrayBuilder<DiagnosticInfo>.Rent();
263+
235264
// Gather attributes info
236265
foreach (AttributeData attributeData in memberSymbol.GetAttributes())
237266
{

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
1010
using CommunityToolkit.Mvvm.SourceGenerators.Models;
1111
using Microsoft.CodeAnalysis;
12-
using Microsoft.CodeAnalysis.CSharp;
1312
using Microsoft.CodeAnalysis.CSharp.Syntax;
1413

1514
namespace CommunityToolkit.Mvvm.SourceGenerators;
@@ -38,6 +37,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3837
return default;
3938
}
4039

40+
// Validate the symbol as well before doing any work
41+
if (!Execute.IsCandidateSymbolValid(context.TargetSymbol))
42+
{
43+
return default;
44+
}
45+
46+
token.ThrowIfCancellationRequested();
47+
4148
// Get the hierarchy info for the target symbol, and try to gather the property info
4249
HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType);
4350

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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_12_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 whenever <c>[ObservableProperty]</c> is used on an invalid partial property declaration.
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public sealed class InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
20+
{
21+
/// <inheritdoc/>
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
23+
InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition,
24+
InvalidObservablePropertyDeclarationReturnsByRef,
25+
InvalidObservablePropertyDeclarationReturnsRefLikeType);
26+
27+
/// <inheritdoc/>
28+
public override void Initialize(AnalysisContext context)
29+
{
30+
// This generator is intentionally also analyzing generated code, because Roslyn will interpret properties
31+
// that have '[GeneratedCode]' on them as being generated (and the same will apply to all partial parts).
32+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
33+
context.EnableConcurrentExecution();
34+
35+
context.RegisterCompilationStartAction(static context =>
36+
{
37+
// Get the [ObservableProperty] and [GeneratedCode] symbols
38+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
39+
context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol)
40+
{
41+
return;
42+
}
43+
44+
context.RegisterSymbolAction(context =>
45+
{
46+
// Ensure that we have some target property to analyze (also skip implementation parts)
47+
if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol)
48+
{
49+
return;
50+
}
51+
52+
// If the property is not using [ObservableProperty], there's nothing to do
53+
if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute))
54+
{
55+
return;
56+
}
57+
58+
// Emit an error if the property is not a partial definition with no implementation...
59+
if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null })
60+
{
61+
// ...But only if it wasn't actually generated by the [ObservableProperty] generator.
62+
bool isImplementationAllowed =
63+
propertySymbol is { IsPartialDefinition: true, PartialImplementationPart: IPropertySymbol implementationPartSymbol } &&
64+
implementationPartSymbol.TryGetAttributeWithType(generatedCodeAttributeSymbol, out AttributeData? generatedCodeAttributeData) &&
65+
generatedCodeAttributeData.TryGetConstructorArgument(0, out string? toolName) &&
66+
toolName == typeof(ObservablePropertyGenerator).FullName;
67+
68+
// Emit the diagnostic only for cases that were not valid generator outputs
69+
if (!isImplementationAllowed)
70+
{
71+
context.ReportDiagnostic(Diagnostic.Create(
72+
InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition,
73+
observablePropertyAttribute.GetLocation(),
74+
propertySymbol.ContainingType,
75+
propertySymbol.Name));
76+
}
77+
}
78+
79+
// Emit an error if the property returns a value by ref
80+
if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly)
81+
{
82+
context.ReportDiagnostic(Diagnostic.Create(
83+
InvalidObservablePropertyDeclarationReturnsByRef,
84+
observablePropertyAttribute.GetLocation(),
85+
propertySymbol.ContainingType,
86+
propertySymbol.Name));
87+
}
88+
89+
// Emit an error if the property type is a ref struct
90+
if (propertySymbol.Type.IsRefLikeType)
91+
{
92+
context.ReportDiagnostic(Diagnostic.Create(
93+
InvalidObservablePropertyDeclarationReturnsRefLikeType,
94+
observablePropertyAttribute.GetLocation(),
95+
propertySymbol.ContainingType,
96+
propertySymbol.Name));
97+
}
98+
}, SymbolKind.Property);
99+
});
100+
}
101+
}
102+
103+
#endif

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ public override void Initialize(AnalysisContext context)
6060
InvalidPropertyDeclarationForObservableProperty,
6161
observablePropertyAttribute.GetLocation(),
6262
propertySymbol.ContainingType,
63-
propertySymbol));
63+
propertySymbol.Name));
64+
65+
return;
6466
}
6567
}
6668
}, SymbolKind.Property);
@@ -91,6 +93,14 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati
9193
return false;
9294
}
9395

96+
// Static properties are not supported
97+
if (property.Modifiers.Any(SyntaxKind.StaticKeyword))
98+
{
99+
containingTypeNode = null;
100+
101+
return false;
102+
}
103+
94104
// The accessors must be a get and a set (with any accessibility)
95105
if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) ||
96106
accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public override void Initialize(AnalysisContext context)
5151
UnsupportedRoslynVersionForObservablePartialPropertySupport,
5252
propertySymbol.Locations.FirstOrDefault(),
5353
propertySymbol.ContainingType,
54-
propertySymbol));
54+
propertySymbol.Name));
5555
}
5656
}, SymbolKind.Property);
5757
});

0 commit comments

Comments
 (0)