Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@ MVVMTK0032 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenera
MVVMTK0033 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0033
MVVMTK0034 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0034
MVVMTK0035 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0035

## Release 8.2

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
MVVMTK0036 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0036
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldReferenceForObservablePropertyFieldAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UnsupportedCSharpLanguageVersionAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\DiagnosticDescriptors.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\SuppressionDescriptors.cs" />
Expand All @@ -54,10 +55,12 @@
<Compile Include="$(MSBuildThisFileDirectory)Extensions\INamedTypeSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalGeneratorInitializationContextExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\IncrementalValuesProviderExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SymbolInfoExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ISymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SourceProductionContextExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\ITypeSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\MemberDeclarationSyntaxExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SyntaxTokenExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\SyntaxNodeExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\TypeDeclarationSyntaxExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Helpers\EquatableArray{T}.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public static bool TryGetInfo(
// Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a
// CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor
// that recognizes uses of this target specifically to support [ObservableProperty].
if (attributeList.Target?.Identifier.Kind() is not SyntaxKind.PropertyKeyword)
if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword))
{
continue;
}
Expand All @@ -214,12 +214,7 @@ public static bool TryGetInfo(
// There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the
// generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the
// lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway.
SymbolInfo attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute, token);

// Check if the attribute type can be resolved, and emit a diagnostic if that is not the case. This happens if eg. the
// attribute type is spelled incorrectly, or if a user is missing the using directive for the attribute type being used.
if ((attributeSymbolInfo.Symbol ?? attributeSymbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol ||
(attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol attributeTypeSymbol)
if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol))
{
builder.Add(
InvalidPropertyTargetedAttributeOnObservablePropertyField,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,4 +589,20 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must correctly be resolved to valid types.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0035");

/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a method with <c>[RelayCommand]</c> is using an invalid attribute targeting the field or property.
/// <para>
/// Format: <c>"The method {0} annotated with [RelayCommand] is using attribute "{1}" which was not recognized as a valid type (are you missing a using directive?)"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod = new DiagnosticDescriptor(
id: "MVVMTK0036",
title: "Invalid field targeted attribute type",
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?)",
category: typeof(RelayCommandGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must correctly be resolved to valid types.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0036");
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
internal static class SuppressionDescriptors
{
/// <summary>
/// Gets a <see cref="SuppressionDescriptor"/> for a field using [ObservableProperty] with on attribute list targeting a property.
/// Gets a <see cref="SuppressionDescriptor"/> for a field using [ObservableProperty] with an attribute list targeting a property.
/// </summary>
public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new(
id: "MVVMTKSPR0001",
suppressedDiagnosticId: "CS0657",
justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties");

/// <summary>
/// Gets a <see cref="SuppressionDescriptor"/> for a method using [RelayCommand] with an attribute list targeting a field or property.
/// </summary>
public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new(
id: "MVVMTKSPR0002",
suppressedDiagnosticId: "CS0657",
justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Linq;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
/// <para>
/// That is, this diagnostic suppressor will suppress the following diagnostic:
/// <code>
/// public class MyViewModel : ObservableObject
/// public partial class MyViewModel : ObservableObject
/// {
/// [ObservableProperty]
/// [property: JsonPropertyName("Name")]
Expand Down Expand Up @@ -53,7 +53,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
// Check if the field is using [ObservableProperty], in which case we should suppress the warning
if (declaredSymbol is IFieldSymbol fieldSymbol &&
semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol &&
fieldSymbol.GetAttributes().Select(attribute => attribute.AttributeClass).Contains(observablePropertySymbol, SymbolEqualityComparer.Default))
fieldSymbol.HasAttributeWithType(observablePropertySymbol))
{
context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.SuppressionDescriptors;

namespace CommunityToolkit.Mvvm.SourceGenerators;

/// <summary>
/// <para>
/// A diagnostic suppressor to suppress CS0657 warnings for methods with [RelayCommand] using a [field:] or [property:] attribute list.
/// </para>
/// <para>
/// That is, this diagnostic suppressor will suppress the following diagnostic:
/// <code>
/// public partial class MyViewModel : ObservableObject
/// {
/// [RelayCommand]
/// [field: JsonIgnore]
/// [property: SomeOtherAttribute]
/// private void DoSomething()
/// {
/// }
/// }
/// </code>
/// </para>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor
{
/// <inheritdoc/>
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForRelayCommandMethod);

/// <inheritdoc/>
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
{
SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);

// Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) })
{
SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);

// Get the method symbol from the first variable declaration
ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken);

// Check if the method is using [RelayCommand], in which case we should suppress the warning
if (declaredSymbol is IMethodSymbol methodSymbol &&
semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is INamedTypeSymbol relayCommandSymbol &&
methodSymbol.HasAttributeWithType(relayCommandSymbol))
{
context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForRelayCommandMethod, diagnostic));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;

/// <summary>
/// Extension methods for the <see cref="SymbolInfo"/> type.
/// </summary>
internal static class SymbolInfoExtensions
{
/// <summary>
/// Tries to get the resolved attribute type symbol from a given <see cref="SymbolInfo"/> value.
/// </summary>
/// <param name="symbolInfo">The <see cref="SymbolInfo"/> value to check.</param>
/// <param name="typeSymbol">The resulting attribute type symbol, if correctly resolved.</param>
/// <returns>Whether <paramref name="symbolInfo"/> is resolved to a symbol.</returns>
/// <remarks>
/// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just
/// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore
/// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own.
/// </remarks>
public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol)
{
ISymbol? attributeSymbol = symbolInfo.Symbol;

// If no symbol is selected and there is a single candidate symbol, use that
if (attributeSymbol is null && symbolInfo.CandidateSymbols is [ISymbol candidateSymbol])
{
attributeSymbol = candidateSymbol;
}

// Extract the symbol from either the current one or the containing type
if ((attributeSymbol as INamedTypeSymbol ?? attributeSymbol?.ContainingType) is not INamedTypeSymbol resultingSymbol)
{
typeSymbol = null;

return false;
}

typeSymbol = resultingSymbol;

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;

/// <summary>
/// Extension methods for the <see cref="SyntaxToken"/> type.
/// </summary>
internal static class SyntaxTokenExtensions
{
/// <summary>
/// Deconstructs a <see cref="SyntaxToken"/> into its <see cref="SyntaxKind"/> value.
/// </summary>
/// <param name="syntaxToken">The input <see cref="SyntaxToken"/> value.</param>
/// <param name="syntaxKind">The resulting <see cref="SyntaxKind"/> value for <paramref name="syntaxToken"/>.</param>
public static void Deconstruct(this SyntaxToken syntaxToken, out SyntaxKind syntaxKind)
{
syntaxKind = syntaxToken.Kind();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;

namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
Expand All @@ -22,6 +23,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been enabled.</param>
/// <param name="FlowExceptionsToTaskScheduler">Whether or not exceptions should flow to the task scheduler.</param>
/// <param name="IncludeCancelCommand">Whether or not to also generate a cancel command.</param>
/// <param name="ForwardedFieldAttributes">The sequence of forwarded attributes for the generated field.</param>
/// <param name="ForwardedPropertyAttributes">The sequence of forwarded attributes for the generated property.</param>
internal sealed record CommandInfo(
string MethodName,
string FieldName,
Expand All @@ -35,4 +38,6 @@ internal sealed record CommandInfo(
CanExecuteExpressionType? CanExecuteExpressionType,
bool AllowConcurrentExecutions,
bool FlowExceptionsToTaskScheduler,
bool IncludeCancelCommand);
bool IncludeCancelCommand,
EquatableArray<AttributeInfo> ForwardedFieldAttributes,
EquatableArray<AttributeInfo> ForwardedPropertyAttributes);
Loading