Skip to content

Commit 2aad03c

Browse files
authored
Merge pull request #1015 from CommunityToolkit/dev/better-modifiers-support
Support more partial property modifiers, ban pointer types
2 parents 28c6a8c + 00fdbef commit 2aad03c

File tree

9 files changed

+458
-38
lines changed

9 files changed

+458
-38
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,4 @@ MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
9696
MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0052
9797
MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053
9898
MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054
99+
MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055

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\InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs" />
4445
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs" />
4546
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer.cs" />
4647
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs" />

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1515
/// <param name="TypeNameWithNullabilityAnnotations">The type name for the generated property, including nullability annotations.</param>
1616
/// <param name="FieldName">The field name.</param>
1717
/// <param name="PropertyName">The generated property name.</param>
18+
/// <param name="PropertyModifers">The list of additional modifiers for the property (they are <see cref="SyntaxKind"/> values).</param>
1819
/// <param name="PropertyAccessibility">The accessibility of the property.</param>
1920
/// <param name="GetterAccessibility">The accessibility of the <see langword="get"/> accessor.</param>
2021
/// <param name="SetterAccessibility">The accessibility of the <see langword="set"/> accessor.</param>
@@ -23,7 +24,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
2324
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
2425
/// <param name="NotifyPropertyChangedRecipients">Whether or not the generated property also broadcasts changes.</param>
2526
/// <param name="NotifyDataErrorInfo">Whether or not the generated property also validates its value.</param>
26-
/// <param name="IsRequired">Whether or not the generated property should be marked as required.</param>
2727
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
2828
/// <param name="IsReferenceTypeOrUnconstrainedTypeParameter">Indicates whether the property is of a reference type or an unconstrained type parameter.</param>
2929
/// <param name="IncludeMemberNotNullOnSetAccessor">Indicates whether to include nullability annotations on the setter.</param>
@@ -34,6 +34,7 @@ internal sealed record PropertyInfo(
3434
string TypeNameWithNullabilityAnnotations,
3535
string FieldName,
3636
string PropertyName,
37+
EquatableArray<ushort> PropertyModifers,
3738
Accessibility PropertyAccessibility,
3839
Accessibility GetterAccessibility,
3940
Accessibility SetterAccessibility,
@@ -42,7 +43,6 @@ internal sealed record PropertyInfo(
4243
EquatableArray<string> NotifiedCommandNames,
4344
bool NotifyPropertyChangedRecipients,
4445
bool NotifyDataErrorInfo,
45-
bool IsRequired,
4646
bool IsOldPropertyValueDirectlyReferenced,
4747
bool IsReferenceTypeOrUnconstrainedTypeParameter,
4848
bool IncludeMemberNotNullOnSetAccessor,

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

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
56
using System.Collections.Generic;
67
using System.Collections.Immutable;
78
using System.ComponentModel;
@@ -98,7 +99,7 @@ public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax node,
9899
public static bool IsCandidateSymbolValid(ISymbol memberSymbol)
99100
{
100101
#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+
// We only need these additional checks for properties (Roslyn already validates things for fields in our scenarios)
102103
if (memberSymbol is IPropertySymbol propertySymbol)
103104
{
104105
// Ensure that the property declaration is a partial definition with no implementation
@@ -115,6 +116,14 @@ public static bool IsCandidateSymbolValid(ISymbol memberSymbol)
115116
}
116117
#endif
117118

119+
// Pointer types are never allowed in either case
120+
if (memberSymbol is
121+
IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or
122+
IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer })
123+
{
124+
return false;
125+
}
126+
118127
// We assume all other cases are supported (other failure cases will be detected later)
119128
return true;
120129
}
@@ -362,6 +371,9 @@ public static bool TryGetInfo(
362371

363372
token.ThrowIfCancellationRequested();
364373

374+
// Get all additional modifiers for the member
375+
ImmutableArray<SyntaxKind> propertyModifiers = GetPropertyModifiers(memberSyntax);
376+
365377
// Retrieve the accessibility values for all components
366378
if (!TryGetAccessibilityModifiers(
367379
memberSyntax,
@@ -378,16 +390,12 @@ public static bool TryGetInfo(
378390

379391
token.ThrowIfCancellationRequested();
380392

381-
// Check whether the property should be required
382-
bool isRequired = GetIsRequiredProperty(memberSymbol);
383-
384-
token.ThrowIfCancellationRequested();
385-
386393
propertyInfo = new PropertyInfo(
387394
memberSyntax.Kind(),
388395
typeNameWithNullabilityAnnotations,
389396
fieldName,
390397
propertyName,
398+
propertyModifiers.AsUnderlyingType(),
391399
propertyAccessibility,
392400
getterAccessibility,
393401
setterAccessibility,
@@ -396,7 +404,6 @@ public static bool TryGetInfo(
396404
notifiedCommandNames.ToImmutable(),
397405
notifyRecipients,
398406
notifyDataErrorInfo,
399-
isRequired,
400407
isOldPropertyValueDirectlyReferenced,
401408
isReferenceTypeOrUnconstrainedTypeParameter,
402409
includeMemberNotNullOnSetAccessor,
@@ -970,6 +977,45 @@ private static void GatherLegacyForwardedAttributes(
970977
}
971978
}
972979

980+
/// <summary>
981+
/// Gathers all allowed property modifiers that should be forwarded to the generated property.
982+
/// </summary>
983+
/// <param name="memberSyntax">The <see cref="MemberDeclarationSyntax"/> instance to process.</param>
984+
/// <returns>The returned set of property modifiers, if any.</returns>
985+
private static ImmutableArray<SyntaxKind> GetPropertyModifiers(MemberDeclarationSyntax memberSyntax)
986+
{
987+
// Fields never need to carry additional modifiers along
988+
if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration))
989+
{
990+
return ImmutableArray<SyntaxKind>.Empty;
991+
}
992+
993+
// We only allow a subset of all possible modifiers (aside from the accessibility modifiers)
994+
ReadOnlySpan<SyntaxKind> candidateKinds =
995+
[
996+
SyntaxKind.NewKeyword,
997+
SyntaxKind.VirtualKeyword,
998+
SyntaxKind.SealedKeyword,
999+
SyntaxKind.OverrideKeyword,
1000+
#if ROSLYN_4_3_1_OR_GREATER
1001+
SyntaxKind.RequiredKeyword
1002+
#endif
1003+
];
1004+
1005+
using ImmutableArrayBuilder<SyntaxKind> builder = ImmutableArrayBuilder<SyntaxKind>.Rent();
1006+
1007+
// Track all modifiers from the allowed set on the input property declaration
1008+
foreach (SyntaxKind kind in candidateKinds)
1009+
{
1010+
if (memberSyntax.Modifiers.Any(kind))
1011+
{
1012+
builder.Add(kind);
1013+
}
1014+
}
1015+
1016+
return builder.ToImmutable();
1017+
}
1018+
9731019
/// <summary>
9741020
/// Tries to get the accessibility of the property and accessors, if possible.
9751021
/// If the target member is not a property, it will use the defaults.
@@ -1043,20 +1089,6 @@ private static bool TryGetAccessibilityModifiers(
10431089
return true;
10441090
}
10451091

1046-
/// <summary>
1047-
/// Checks whether an input member is a required property.
1048-
/// </summary>
1049-
/// <param name="memberSymbol">The input <see cref="ISymbol"/> instance to process.</param>
1050-
/// <returns>Whether <paramref name="memberSymbol"/> is a required property.</returns>
1051-
private static bool GetIsRequiredProperty(ISymbol memberSymbol)
1052-
{
1053-
#if ROSLYN_4_3_1_OR_GREATER
1054-
return memberSymbol is IPropertySymbol { IsRequired: true };
1055-
#else
1056-
return false;
1057-
#endif
1058-
}
1059-
10601092
/// <summary>
10611093
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
10621094
/// </summary>
@@ -1395,13 +1427,11 @@ private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo)
13951427
{
13961428
SyntaxTokenList propertyModifiers = propertyInfo.PropertyAccessibility.ToSyntaxTokenList();
13971429

1398-
#if ROSLYN_4_3_1_OR_GREATER
1399-
// Add the 'required' modifier if the original member also had it
1400-
if (propertyInfo.IsRequired)
1430+
// Add all gathered modifiers
1431+
foreach (SyntaxKind modifier in propertyInfo.PropertyModifers.AsImmutableArray().FromUnderlyingType())
14011432
{
1402-
propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword));
1433+
propertyModifiers = propertyModifiers.Add(Token(modifier));
14031434
}
1404-
#endif
14051435

14061436
// Add the 'partial' modifier if the original member is a partial property
14071437
if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
using System.Collections.Immutable;
6+
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
10+
11+
namespace CommunityToolkit.Mvvm.SourceGenerators;
12+
13+
/// <summary>
14+
/// A diagnostic analyzer that generates an error whenever <c>[ObservableProperty]</c> is used with pointer types.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class InvalidPointerTypeObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
18+
{
19+
/// <inheritdoc/>
20+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyDeclarationReturnsPointerLikeType);
21+
22+
/// <inheritdoc/>
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
26+
context.EnableConcurrentExecution();
27+
28+
context.RegisterCompilationStartAction(static context =>
29+
{
30+
// Get the [ObservableProperty] symbol
31+
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
32+
{
33+
return;
34+
}
35+
36+
context.RegisterSymbolAction(context =>
37+
{
38+
// Ensure that we have a valid target symbol to analyze
39+
if (context.Symbol is not (IFieldSymbol or IPropertySymbol))
40+
{
41+
return;
42+
}
43+
44+
// If the property is not using [ObservableProperty], there's nothing to do
45+
if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute))
46+
{
47+
return;
48+
}
49+
50+
// Emit a diagnostic if the type is a pointer type
51+
if (context.Symbol is
52+
IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or
53+
IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer })
54+
{
55+
context.ReportDiagnostic(Diagnostic.Create(
56+
InvalidObservablePropertyDeclarationReturnsPointerLikeType,
57+
observablePropertyAttribute.GetLocation(),
58+
context.Symbol.ContainingType,
59+
context.Symbol.Name));
60+
}
61+
}, SymbolKind.Field, SymbolKind.Property);
62+
});
63+
}
64+
}

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,4 +907,20 @@ internal static class DiagnosticDescriptors
907907
isEnabledByDefault: true,
908908
description: "A property using [ObservableProperty] returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type).",
909909
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0054");
910+
911+
/// <summary>
912+
/// Gets a <see cref="DiagnosticDescriptor"/> for when <c>[ObservableProperty]</c> is used on a property that returns a pointer type.
913+
/// <para>
914+
/// Format: <c>"The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)"</c>.
915+
/// </para>
916+
/// </summary>
917+
public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsPointerLikeType = new DiagnosticDescriptor(
918+
id: "MVVMTK0055",
919+
title: "Using [ObservableProperty] on a property that returns pointer-like",
920+
messageFormat: """The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)""",
921+
category: typeof(ObservablePropertyGenerator).FullName,
922+
defaultSeverity: DiagnosticSeverity.Error,
923+
isEnabledByDefault: true,
924+
description: "A property using [ObservableProperty] returns a pointer-like value ([ObservableProperty] must be used on properties of a non pointer-like type).",
925+
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0055");
910926
}

src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Immutable;
7+
using System.Runtime.CompilerServices;
68
using Microsoft.CodeAnalysis.CSharp;
79

810
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
@@ -12,6 +14,30 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1214
/// </summary>
1315
internal static class SyntaxKindExtensions
1416
{
17+
/// <summary>
18+
/// Converts an <see cref="ImmutableArray{T}"/> of <see cref="SyntaxKind"/> values to one of their underlying type.
19+
/// </summary>
20+
/// <param name="array">The input <see cref="ImmutableArray{T}"/> value.</param>
21+
/// <returns>The resulting <see cref="ImmutableArray{T}"/> of <see cref="ushort"/> values.</returns>
22+
public static ImmutableArray<ushort> AsUnderlyingType(this ImmutableArray<SyntaxKind> array)
23+
{
24+
ushort[]? underlyingArray = (ushort[]?)(object?)Unsafe.As<ImmutableArray<SyntaxKind>, SyntaxKind[]?>(ref array);
25+
26+
return Unsafe.As<ushort[]?, ImmutableArray<ushort>>(ref underlyingArray);
27+
}
28+
29+
/// <summary>
30+
/// Converts an <see cref="ImmutableArray{T}"/> of <see cref="ushort"/> values to one of their real type.
31+
/// </summary>
32+
/// <param name="array">The input <see cref="ImmutableArray{T}"/> value.</param>
33+
/// <returns>The resulting <see cref="ImmutableArray{T}"/> of <see cref="SyntaxKind"/> values.</returns>
34+
public static ImmutableArray<SyntaxKind> FromUnderlyingType(this ImmutableArray<ushort> array)
35+
{
36+
SyntaxKind[]? typedArray = (SyntaxKind[]?)(object?)Unsafe.As<ImmutableArray<ushort>, ushort[]?>(ref array);
37+
38+
return Unsafe.As<SyntaxKind[]?, ImmutableArray<SyntaxKind>>(ref typedArray);
39+
}
40+
1541
/// <summary>
1642
/// Converts a <see cref="SyntaxKind"/> value to either "field" or "property" based on the kind.
1743
/// </summary>

0 commit comments

Comments
 (0)