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 @@ -40,6 +40,9 @@
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AsyncVoidReturningRelayCommandMethodAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidTargetObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,6 @@ public static bool TryGetInfo(
// Validate the target type
if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging))
{
builder.Add(
InvalidContainingTypeForObservablePropertyMemberError,
memberSymbol,
memberSyntax.Kind().ToFieldOrPropertyKeyword(),
memberSymbol.ContainingType,
memberSymbol.Name);

propertyInfo = null;
diagnostics = builder.ToImmutable();

Expand All @@ -173,12 +166,6 @@ public static bool TryGetInfo(
// Check for name collisions (only for fields)
if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration))
{
builder.Add(
ObservablePropertyNameCollisionError,
memberSymbol,
memberSymbol.ContainingType,
memberSymbol.Name);

propertyInfo = null;
diagnostics = builder.ToImmutable();

Expand All @@ -193,12 +180,6 @@ public static bool TryGetInfo(
// Check for special cases that are explicitly not allowed
if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol)))
{
builder.Add(
InvalidObservablePropertyError,
memberSymbol,
memberSymbol.ContainingType,
memberSymbol.Name);

propertyInfo = null;
diagnostics = builder.ToImmutable();

Expand Down Expand Up @@ -440,7 +421,7 @@ private static bool IsTargetTypeValid(ISymbol memberSymbol, out bool shouldInvok
/// <param name="propertyName">The property name.</param>
/// <param name="propertyType">The property type.</param>
/// <returns>Whether the generated property is invalid.</returns>
private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType)
public static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType)
{
// If the generated property name is called "Property" and the type is either object or it is PropertyChangedEventArgs or
// PropertyChangingEventArgs (or a type derived from either of those two types), consider it invalid. This is needed because
Expand Down Expand Up @@ -1506,7 +1487,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
/// </summary>
/// <param name="memberSymbol">The input <see cref="ISymbol"/> instance to process.</param>
/// <returns>The type of <paramref name="memberSymbol"/>.</returns>
private static ITypeSymbol GetPropertyType(ISymbol memberSymbol)
public static ITypeSymbol GetPropertyType(ISymbol memberSymbol)
{
// Check if the member is a property first
if (memberSymbol is IPropertySymbol propertySymbol)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 field or property with <c>[ObservableProperty]</c> is not valid (special cases)
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyError);

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

context.RegisterCompilationStartAction(static context =>
{
// Get the symbol for [ObservableProperty] and the event args we need
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs") is not INamedTypeSymbol propertyChangedEventArgsSymbol ||
context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs") is not INamedTypeSymbol propertyChangingEventArgsSymbol)
{
return;
}

context.RegisterSymbolAction(context =>
{
// Validate that we do have a field or a property
if (context.Symbol is not (IFieldSymbol or IPropertySymbol))
{
return;
}

// Ensure we do have the [ObservableProperty] attribute
if (!context.Symbol.HasAttributeWithType(observablePropertySymbol))
{
return;
}

ITypeSymbol propertyType = ObservablePropertyGenerator.Execute.GetPropertyType(context.Symbol);
string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(context.Symbol);

// Same logic as 'IsGeneratedPropertyInvalid' in the generator
if (propertyName == "Property")
{
// Check for collisions with the generated helpers and the property, only happens with these 3 types
if (propertyType.SpecialType == SpecialType.System_Object ||
propertyType.HasOrInheritsFromType(propertyChangedEventArgsSymbol) ||
propertyType.HasOrInheritsFromType(propertyChangingEventArgsSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidObservablePropertyError,
context.Symbol.Locations.FirstOrDefault(),
context.Symbol.Kind.ToFieldOrPropertyKeyword(),
context.Symbol.ContainingType,
context.Symbol.Name));
}
}
}, SymbolKind.Field, SymbolKind.Property);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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 field or property with <c>[ObservableProperty]</c> is not a valid target.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class InvalidTargetObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidContainingTypeForObservablePropertyMemberError);

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

context.RegisterCompilationStartAction(static context =>
{
// Get the required symbols for the analyzer
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol ||
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") is not INamedTypeSymbol observableObjectAttributeSymbol ||
context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute") is not INamedTypeSymbol notifyPropertyChangedAttributeSymbol)
{
return;
}

context.RegisterSymbolAction(context =>
{
// Validate that we do have a field or a property
if (context.Symbol is not (IFieldSymbol or IPropertySymbol))
{
return;
}

// Ensure we do have the [ObservableProperty] attribute
if (!context.Symbol.HasAttributeWithType(observablePropertySymbol))
{
return;
}

// Same logic as in 'IsTargetTypeValid' in the generator
bool isObservableObject = context.Symbol.ContainingType.InheritsFromType(observableObjectSymbol);
bool hasObservableObjectAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(observableObjectAttributeSymbol);
bool hasINotifyPropertyChangedAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(notifyPropertyChangedAttributeSymbol);

// Emit the diagnostic if the target is not valid
if (!isObservableObject && !hasObservableObjectAttribute && !hasINotifyPropertyChangedAttribute)
{
context.ReportDiagnostic(Diagnostic.Create(
InvalidContainingTypeForObservablePropertyMemberError,
context.Symbol.Locations.FirstOrDefault(),
context.Symbol.Kind.ToFieldOrPropertyKeyword(),
context.Symbol.ContainingType,
context.Symbol.Name));
}
}, SymbolKind.Field, SymbolKind.Property);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 generated property from <c>[ObservableProperty]</c> would collide with the field name.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PropertyNameCollisionObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(ObservablePropertyNameCollisionError);

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

context.RegisterCompilationStartAction(static context =>
{
// Get the symbol for [ObservableProperty]
if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
{
return;
}

context.RegisterSymbolAction(context =>
{
// Ensure we do have a valid field
if (context.Symbol is not IFieldSymbol fieldSymbol)
{
return;
}

// We only care if the field has [ObservableProperty]
if (!fieldSymbol.HasAttributeWithType(observablePropertySymbol))
{
return;
}

// Emit the diagnostic if there is a name collision
if (fieldSymbol.Name == ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
ObservablePropertyNameCollisionError,
fieldSymbol.Locations.FirstOrDefault(),
fieldSymbol.ContainingType,
fieldSymbol.Name));
}
}, SymbolKind.Field);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -407,17 +407,17 @@ internal static class DiagnosticDescriptors
/// <summary>
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a generated property created with <c>[ObservableProperty]</c> would cause conflicts with other generated members.
/// <para>
/// Format: <c>"The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members"</c>.
/// Format: <c>"The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members"</c>.
/// </para>
/// </summary>
public static readonly DiagnosticDescriptor InvalidObservablePropertyError = new DiagnosticDescriptor(
id: "MVVMTK0024",
title: "Invalid generated property declaration",
messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members",
messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members",
category: typeof(ObservablePropertyGenerator).FullName,
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The fields annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.",
description: "The fields and properties annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0024");

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol
return false;
}

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

return false;
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
/// </summary>
Expand Down Expand Up @@ -60,7 +79,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS
/// 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>
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instance 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)
{
Expand Down
Loading