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 @@ -39,6 +39,8 @@
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldReferenceForObservablePropertyFieldAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,26 +555,6 @@ private static bool TryGetIsNotifyingRecipients(
return false;
}

/// <summary>
/// Checks whether a given type using <c>[NotifyPropertyChangedRecipients]</c> is valid and creates a <see cref="Diagnostic"/> if not.
/// </summary>
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
/// <returns>The <see cref="Diagnostic"/> for <paramref name="typeSymbol"/>, if not a valid type.</returns>
public static Diagnostic? GetIsNotifyingRecipientsDiagnosticForType(INamedTypeSymbol typeSymbol)
{
// If the containing type is valid, track it
if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") &&
!typeSymbol.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute"))
{
return Diagnostic.Create(
InvalidTypeForNotifyPropertyChangedRecipientsError,
typeSymbol.Locations.FirstOrDefault(),
typeSymbol);
}

return null;
}

/// <summary>
/// Checks whether a given generated property should also validate its value.
/// </summary>
Expand Down Expand Up @@ -657,25 +637,6 @@ private static bool TryGetNotifyDataErrorInfo(
return false;
}

/// <summary>
/// Checks whether a given type using <c>[NotifyDataErrorInfo]</c> is valid and creates a <see cref="Diagnostic"/> if not.
/// </summary>
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
/// <returns>The <see cref="Diagnostic"/> for <paramref name="typeSymbol"/>, if not a valid type.</returns>
public static Diagnostic? GetIsNotifyDataErrorInfoDiagnosticForType(INamedTypeSymbol typeSymbol)
{
// If the containing type is valid, track it
if (!typeSymbol.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))
{
return Diagnostic.Create(
InvalidTypeForNotifyDataErrorInfoError,
typeSymbol.Locations.FirstOrDefault(),
typeSymbol);
}

return null;
}

/// <summary>
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,32 +113,5 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
context.AddSource("__KnownINotifyPropertyChangedArgs.g.cs", compilationUnit.GetText(Encoding.UTF8));
}
});

// Get all class declarations with at least one attribute
IncrementalValuesProvider<INamedTypeSymbol> classSymbols =
context.SyntaxProvider
.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);

// Filter only the type symbols with [NotifyPropertyChangedRecipients] and create diagnostics for them
IncrementalValuesProvider<Diagnostic> notifyRecipientsErrors =
classSymbols
.Where(static item => item.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"))
.Select(static (item, _) => Execute.GetIsNotifyingRecipientsDiagnosticForType(item))
.Where(static item => item is not null)!;

// Output the diagnostics for [NotifyPropertyChangedRecipients]
context.ReportDiagnostics(notifyRecipientsErrors);

// Filter only the type symbols with [NotifyDataErrorInfo] and create diagnostics for them
IncrementalValuesProvider<Diagnostic> notifyDataErrorInfoErrors =
classSymbols
.Where(static item => item.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"))
.Select(static (item, _) => Execute.GetIsNotifyDataErrorInfoDiagnosticForType(item))
.Where(static item => item is not null)!;

// Output the diagnostics for [NotifyDataErrorInfo]
context.ReportDiagnostics(notifyDataErrorInfoErrors);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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 System.Linq;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.Mvvm.SourceGenerators;

/// <summary>
/// A diagnostic analyzer that generates an error when a class level <c>[NotifyDataErrorInfo]</c> use is detected.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidTypeForNotifyDataErrorInfoError);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(static context =>
{
// Get the symbols for [NotifyDataErrorInfo] and ObservableValidator
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") is not INamedTypeSymbol notifyDataErrorInfoAttributeSymbol ||
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator") is not INamedTypeSymbol observableValidatorSymbol)
{
return;
}

context.RegisterSymbolAction(context =>
{
// We're looking for all class declarations
if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } classSymbol)
{
return;
}

// Emit a diagnostic for types that use [NotifyDataErrorInfo] but don't inherit from ObservableValidator
if (classSymbol.HasAttributeWithType(notifyDataErrorInfoAttributeSymbol) &&
!classSymbol.InheritsFromType(observableValidatorSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidTypeForNotifyDataErrorInfoError,
classSymbol.Locations.FirstOrDefault(),
classSymbol));
}
}, SymbolKind.NamedType);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// 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 System.Linq;
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;

namespace CommunityToolkit.Mvvm.SourceGenerators;

/// <summary>
/// A diagnostic analyzer that generates an error when a class level <c>[NotifyPropertyChangedRecipients]</c> use is detected.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidTypeForNotifyPropertyChangedRecipientsError);

/// <inheritdoc/>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(static context =>
{
// Get the symbols for [NotifyPropertyChangedRecipients], ObservableRecipient and [ObservableRecipient]
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") is not INamedTypeSymbol notifyPropertyChangedRecipientsAttributeSymbol ||
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") is not INamedTypeSymbol observableRecipientSymbol ||
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute") is not INamedTypeSymbol observableRecipientAttributeSymbol)
{
return;
}

context.RegisterSymbolAction(context =>
{
// We're looking for all class declarations
if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsImplicitlyDeclared: false } classSymbol)
{
return;
}

// Emit a diagnstic for types that use [NotifyPropertyChangedRecipients] but are neither inheriting from ObservableRecipient nor using [ObservableRecipient]
if (classSymbol.HasAttributeWithType(notifyPropertyChangedRecipientsAttributeSymbol) &&
!classSymbol.InheritsFromType(observableRecipientSymbol) &&
!classSymbol.HasOrInheritsAttributeWithType(observableRecipientAttributeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidTypeForNotifyPropertyChangedRecipientsError,
classSymbol.Locations.FirstOrDefault(),
classSymbol));
}
}, SymbolKind.NamedType);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// 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;
#if !ROSLYN_4_3_1_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif
Expand Down Expand Up @@ -54,9 +53,7 @@ public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified name.</returns>
public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name)
{
ImmutableArray<AttributeData> attributes = symbol.GetAttributes();

foreach (AttributeData attribute in attributes)
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
{
Expand All @@ -67,6 +64,25 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
return false;
}

/// <summary>
/// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name.
/// </summary>
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
/// <param name="typeSymbol">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified type.</returns>
public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol)
{
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol))
{
return true;
}
}

return false;
}

#if !ROSLYN_4_3_1_OR_GREATER
/// <summary>
/// Tries to get an attribute with the specified fully qualified metadata name.
Expand All @@ -77,9 +93,7 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo
/// <returns>Whether or not <paramref name="symbol"/> has an attribute with the specified name.</returns>
public static bool TryGetAttributeWithFullyQualifiedMetadataName(this ISymbol symbol, string name, [NotNullWhen(true)] out AttributeData? attributeData)
{
ImmutableArray<AttributeData> attributes = symbol.GetAttributes();

foreach (AttributeData attribute in attributes)
foreach (AttributeData attribute in symbol.GetAttributes())
{
if (attribute.AttributeClass?.HasFullyQualifiedMetadataName(name) == true)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
{
INamedTypeSymbol? baseType = typeSymbol.BaseType;

while (baseType != null)
while (baseType is not null)
{
if (baseType.HasFullyQualifiedMetadataName(name))
{
Expand All @@ -56,6 +56,29 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
return false;
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
/// </summary>
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instane to check for inheritance from.</param>
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="baseTypeSymbol"/>.</returns>
public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
{
INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType;

while (currentBaseTypeSymbol is not null)
{
if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol))
{
return true;
}

currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType;
}

return false;
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> implements an interface with a specified name.
/// </summary>
Expand Down Expand Up @@ -113,6 +136,25 @@ public static bool HasOrInheritsAttributeWithFullyQualifiedMetadataName(this ITy
return false;
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits a specified attribute.
/// </summary>
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instane to check for inheritance from.</param>
/// <returns>Whether or not <paramref name="typeSymbol"/> has or inherits an attribute of type <paramref name="baseTypeSymbol"/>.</returns>
public static bool HasOrInheritsAttributeWithType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
{
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
{
if (currentType.HasAttributeWithType(baseTypeSymbol))
{
return true;
}
}

return false;
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits a specified attribute.
/// If the type has no base type, this method will automatically handle that and return <see langword="false"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1337,27 +1337,27 @@ public partial class SampleViewModel : ObservableValidator
}

[TestMethod]
public void InvalidTypeForNotifyPropertyChangedRecipientsError()
public async Task InvalidTypeForNotifyPropertyChangedRecipientsError()
{
string source = """
using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp
{
[NotifyPropertyChangedRecipients]
public partial class MyViewModel : ObservableObject
public partial class {|MVVMTK0027:MyViewModel|} : ObservableObject
{
[ObservableProperty]
public int number;
}
}
""";

VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0027");
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer>(source, LanguageVersion.CSharp8);
}

[TestMethod]
public void InvalidTypeForNotifyDataErrorInfoError()
public async Task InvalidTypeForNotifyDataErrorInfoError()
{
string source = """
using System.ComponentModel.DataAnnotations;
Expand All @@ -1366,16 +1366,13 @@ public void InvalidTypeForNotifyDataErrorInfoError()
namespace MyApp
{
[NotifyDataErrorInfo]
public partial class SampleViewModel : ObservableObject
public partial class {|MVVMTK0028:SampleViewModel|} : ObservableObject
{
[ObservableProperty]
[Required]
private string name;
}
}
""";

VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0006", "MVVMTK0028");
await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration<InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer>(source, LanguageVersion.CSharp8);
}

[TestMethod]
Expand Down