Skip to content

Commit 51a0ebf

Browse files
committed
Add diagnostics when forwarded invalid attributes
1 parent c5e8445 commit 51a0ebf

File tree

6 files changed

+133
-25
lines changed

6 files changed

+133
-25
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,12 @@ MVVMTK0035 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
5757
Rule ID | Category | Severity | Notes
5858
--------|----------|----------|-------
5959
MVVMTK0036 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0036
60+
61+
## Release 8.2.1
62+
63+
### New Rules
64+
65+
Rule ID | Category | Severity | Notes
66+
--------|----------|----------|-------
67+
MVVMTK0037 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0037
68+
MVVMTK0038 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0038

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

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

55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Linq;
78
using System.Threading;
89
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
@@ -16,6 +17,9 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1617
/// <summary>
1718
/// A model representing an attribute declaration.
1819
/// </summary>
20+
/// <param name="TypeName">The type name of the attribute.</param>
21+
/// <param name="ConstructorArgumentInfo">The <see cref="TypedConstantInfo"/> values for all constructor arguments for the attribute.</param>
22+
/// <param name="NamedArgumentInfo">The <see cref="TypedConstantInfo"/> values for all named arguments for the attribute.</param>
1923
internal sealed record AttributeInfo(
2024
string TypeName,
2125
EquatableArray<TypedConstantInfo> ConstructorArgumentInfo,
@@ -26,7 +30,7 @@ internal sealed record AttributeInfo(
2630
/// </summary>
2731
/// <param name="attributeData">The input <see cref="AttributeData"/> value.</param>
2832
/// <returns>A <see cref="AttributeInfo"/> instance representing <paramref name="attributeData"/>.</returns>
29-
public static AttributeInfo From(AttributeData attributeData)
33+
public static AttributeInfo Create(AttributeData attributeData)
3034
{
3135
string typeName = attributeData.AttributeClass!.GetFullyQualifiedName();
3236

@@ -36,13 +40,13 @@ public static AttributeInfo From(AttributeData attributeData)
3640
// Get the constructor arguments
3741
foreach (TypedConstant typedConstant in attributeData.ConstructorArguments)
3842
{
39-
constructorArguments.Add(TypedConstantInfo.From(typedConstant));
43+
constructorArguments.Add(TypedConstantInfo.Create(typedConstant));
4044
}
4145

4246
// Get the named arguments
4347
foreach (KeyValuePair<string, TypedConstant> namedConstant in attributeData.NamedArguments)
4448
{
45-
namedArguments.Add((namedConstant.Key, TypedConstantInfo.From(namedConstant.Value)));
49+
namedArguments.Add((namedConstant.Key, TypedConstantInfo.Create(namedConstant.Value)));
4650
}
4751

4852
return new(
@@ -58,8 +62,14 @@ public static AttributeInfo From(AttributeData attributeData)
5862
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
5963
/// <param name="arguments">The sequence of <see cref="AttributeArgumentSyntax"/> instances to process.</param>
6064
/// <param name="token">The cancellation token for the current operation.</param>
61-
/// <returns>A <see cref="AttributeInfo"/> instance representing the input attribute data.</returns>
62-
public static AttributeInfo From(INamedTypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable<AttributeArgumentSyntax> arguments, CancellationToken token)
65+
/// <param name="info">The resulting <see cref="AttributeInfo"/> instance, if available</param>
66+
/// <returns>Whether a resulting <see cref="AttributeInfo"/> instance could be created.</returns>
67+
public static bool TryCreate(
68+
INamedTypeSymbol typeSymbol,
69+
SemanticModel semanticModel,
70+
IEnumerable<AttributeArgumentSyntax> arguments,
71+
CancellationToken token,
72+
[NotNullWhen(true)] out AttributeInfo? info)
6373
{
6474
string typeName = typeSymbol.GetFullyQualifiedName();
6575

@@ -74,7 +84,13 @@ public static AttributeInfo From(INamedTypeSymbol typeSymbol, SemanticModel sema
7484
continue;
7585
}
7686

77-
TypedConstantInfo argumentInfo = TypedConstantInfo.From(operation, semanticModel, argument.Expression, token);
87+
// Try to get the info for the current argument
88+
if (!TypedConstantInfo.TryCreate(operation, semanticModel, argument.Expression, token, out TypedConstantInfo? argumentInfo))
89+
{
90+
info = null;
91+
92+
return false;
93+
}
7894

7995
// Try to get the identifier name if the current expression is a named argument expression. If it
8096
// isn't, then the expression is a normal attribute constructor argument, so no extra work is needed.
@@ -88,10 +104,12 @@ public static AttributeInfo From(INamedTypeSymbol typeSymbol, SemanticModel sema
88104
}
89105
}
90106

91-
return new(
107+
info = new AttributeInfo(
92108
typeName,
93109
constructorArguments.ToImmutable(),
94110
namedArguments.ToImmutable());
111+
112+
return true;
95113
}
96114

97115
/// <summary>

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/TypedConstantInfo.Factory.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Immutable;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Linq;
89
using System.Threading;
910
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
@@ -22,7 +23,7 @@ partial record TypedConstantInfo
2223
/// <param name="arg">The input <see cref="TypedConstant"/> value.</param>
2324
/// <returns>A <see cref="TypedConstantInfo"/> instance representing <paramref name="arg"/>.</returns>
2425
/// <exception cref="ArgumentException">Thrown if the input argument is not valid.</exception>
25-
public static TypedConstantInfo From(TypedConstant arg)
26+
public static TypedConstantInfo Create(TypedConstant arg)
2627
{
2728
if (arg.IsNull)
2829
{
@@ -32,7 +33,7 @@ public static TypedConstantInfo From(TypedConstant arg)
3233
if (arg.Kind == TypedConstantKind.Array)
3334
{
3435
string elementTypeName = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
35-
ImmutableArray<TypedConstantInfo> items = arg.Values.Select(From).ToImmutableArray();
36+
ImmutableArray<TypedConstantInfo> items = arg.Values.Select(Create).ToImmutableArray();
3637

3738
return new Array(elementTypeName, items);
3839
}
@@ -69,24 +70,28 @@ public static TypedConstantInfo From(TypedConstant arg)
6970
/// <param name="semanticModel">The <see cref="SemanticModel"/> that was used to retrieve <paramref name="operation"/>.</param>
7071
/// <param name="expression">The <see cref="ExpressionSyntax"/> that <paramref name="operation"/> was retrieved from.</param>
7172
/// <param name="token">The cancellation token for the current operation.</param>
72-
/// <returns>A <see cref="TypedConstantInfo"/> instance representing <paramref name="operation"/>.</returns>
73+
/// <param name="info">The resulting <see cref="TypedConstantInfo"/> instance, if available</param>
74+
/// <returns>Whether a resulting <see cref="TypedConstantInfo"/> instance could be created.</returns>
7375
/// <exception cref="ArgumentException">Thrown if the input argument is not valid.</exception>
74-
public static TypedConstantInfo From(
76+
public static bool TryCreate(
7577
IOperation operation,
7678
SemanticModel semanticModel,
7779
ExpressionSyntax expression,
78-
CancellationToken token)
80+
CancellationToken token,
81+
[NotNullWhen(true)] out TypedConstantInfo? info)
7982
{
8083
if (operation.ConstantValue.HasValue)
8184
{
8285
// Enum values are constant but need to be checked explicitly in this case
8386
if (operation.Type?.TypeKind is TypeKind.Enum)
8487
{
85-
return new Enum(operation.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), operation.ConstantValue.Value!);
88+
info = new Enum(operation.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), operation.ConstantValue.Value!);
89+
90+
return true;
8691
}
8792

8893
// Handle all other constant literals normally
89-
return operation.ConstantValue.Value switch
94+
info = operation.ConstantValue.Value switch
9095
{
9196
null => new Null(),
9297
string text => new Primitive.String(text),
@@ -104,11 +109,15 @@ public static TypedConstantInfo From(
104109
ushort ush => new Primitive.Of<ushort>(ush),
105110
_ => throw new ArgumentException("Invalid primitive type")
106111
};
112+
113+
return true;
107114
}
108115

109116
if (operation is ITypeOfOperation typeOfOperation)
110117
{
111-
return new Type(typeOfOperation.TypeOperand.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
118+
info = new Type(typeOfOperation.TypeOperand.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
119+
120+
return true;
112121
}
113122

114123
if (operation is IArrayCreationOperation)
@@ -125,7 +134,9 @@ public static TypedConstantInfo From(
125134
// No initializer found, just return an empty array
126135
if (initializerExpression is null)
127136
{
128-
return new Array(elementTypeName, ImmutableArray<TypedConstantInfo>.Empty);
137+
info = new Array(elementTypeName, ImmutableArray<TypedConstantInfo>.Empty);
138+
139+
return true;
129140
}
130141

131142
using ImmutableArrayBuilder<TypedConstantInfo> items = ImmutableArrayBuilder<TypedConstantInfo>.Rent();
@@ -135,15 +146,25 @@ public static TypedConstantInfo From(
135146
{
136147
if (semanticModel.GetOperation(initializationExpression, token) is not IOperation initializationOperation)
137148
{
138-
throw new ArgumentException("Failed to retrieve an operation for the current array element");
149+
goto Failure;
139150
}
140151

141-
items.Add(From(initializationOperation, semanticModel, initializationExpression, token));
152+
if (!TryCreate(initializationOperation, semanticModel, initializationExpression, token, out TypedConstantInfo? elementInfo))
153+
{
154+
goto Failure;
155+
}
156+
157+
items.Add(elementInfo);
142158
}
143159

144-
return new Array(elementTypeName, items.ToImmutable());
160+
info = new Array(elementTypeName, items.ToImmutable());
161+
162+
return true;
145163
}
146164

147-
throw new ArgumentException("Invalid attribute argument value");
165+
Failure:
166+
info = null;
167+
168+
return false;
148169
}
149170
}

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

Lines changed: 18 additions & 3 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.Collections.Generic;
56
using System.Collections.Immutable;
67
using System.ComponentModel;
78
using System.Diagnostics.CodeAnalysis;
@@ -174,7 +175,7 @@ public static bool TryGetInfo(
174175
{
175176
hasAnyValidationAttributes = true;
176177

177-
forwardedAttributes.Add(AttributeInfo.From(attributeData));
178+
forwardedAttributes.Add(AttributeInfo.Create(attributeData));
178179
}
179180

180181
// Also track the current attribute for forwarding if it is of any of the following types:
@@ -189,7 +190,7 @@ public static bool TryGetInfo(
189190
attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true ||
190191
attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true)
191192
{
192-
forwardedAttributes.Add(AttributeInfo.From(attributeData));
193+
forwardedAttributes.Add(AttributeInfo.Create(attributeData));
193194
}
194195
}
195196

@@ -231,7 +232,21 @@ public static bool TryGetInfo(
231232
continue;
232233
}
233234

234-
forwardedAttributes.Add(AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token));
235+
IEnumerable<AttributeArgumentSyntax> attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>();
236+
237+
// Try to extract the forwarded attribute
238+
if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo))
239+
{
240+
builder.Add(
241+
InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField,
242+
attribute,
243+
fieldSymbol,
244+
attribute.Name);
245+
246+
continue;
247+
}
248+
249+
forwardedAttributes.Add(attributeInfo);
235250
}
236251
}
237252

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,11 +598,43 @@ internal static class DiagnosticDescriptors
598598
/// </summary>
599599
public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod = new DiagnosticDescriptor(
600600
id: "MVVMTK0036",
601-
title: "Invalid field targeted attribute type",
601+
title: "Invalid field or property targeted attribute type",
602602
messageFormat: "The method {0} annotated with [RelayCommand] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)",
603603
category: typeof(RelayCommandGenerator).FullName,
604604
defaultSeverity: DiagnosticSeverity.Error,
605605
isEnabledByDefault: true,
606606
description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must correctly be resolved to valid types.",
607607
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0036");
608+
609+
/// <summary>
610+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a field with <c>[ObservableProperty]</c> is using an invalid attribute expression targeting the property.
611+
/// <para>
612+
/// Format: <c>"The field {0} annotated with [ObservableProperty] is using attribute "{1}" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)"</c>.
613+
/// </para>
614+
/// </summary>
615+
public static readonly DiagnosticDescriptor InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField = new DiagnosticDescriptor(
616+
id: "MVVMTK0037",
617+
title: "Invalid property targeted attribute expression",
618+
messageFormat: "The field {0} annotated with [ObservableProperty] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)",
619+
category: typeof(ObservablePropertyGenerator).FullName,
620+
defaultSeverity: DiagnosticSeverity.Error,
621+
isEnabledByDefault: true,
622+
description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions.",
623+
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0037");
624+
625+
/// <summary>
626+
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a method with <c>[RelayCommand]</c> is using an invalid attribute targeting the field or property.
627+
/// <para>
628+
/// Format: <c>"The method {0} annotated with [RelayCommand] is using attribute "{1}" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)"</c>.
629+
/// </para>
630+
/// </summary>
631+
public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeExpressionOnRelayCommandMethod = new DiagnosticDescriptor(
632+
id: "MVVMTK0038",
633+
title: "Invalid field or property targeted attribute expression",
634+
messageFormat: "The method {0} annotated with [RelayCommand] is using attribute \"{1}\" with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)",
635+
category: typeof(RelayCommandGenerator).FullName,
636+
defaultSeverity: DiagnosticSeverity.Error,
637+
isEnabledByDefault: true,
638+
description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions.",
639+
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0038");
608640
}

src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Generic;
67
using System.Collections.Immutable;
78
using System.Diagnostics.CodeAnalysis;
89
using System.Globalization;
@@ -1004,7 +1005,19 @@ static void GatherForwardedAttributes(
10041005
continue;
10051006
}
10061007

1007-
AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token);
1008+
IEnumerable<AttributeArgumentSyntax> attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>();
1009+
1010+
// Try to extract the forwarded attribute
1011+
if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo))
1012+
{
1013+
diagnostics.Add(
1014+
InvalidFieldOrPropertyTargetedAttributeExpressionOnRelayCommandMethod,
1015+
attribute,
1016+
methodSymbol,
1017+
attribute.Name);
1018+
1019+
continue;
1020+
}
10081021

10091022
// Add the new attribute info to the right builder
10101023
if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword))

0 commit comments

Comments
 (0)