mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2025-04-10 11:15:45 +02:00
Merge pull request #63 from CommunityToolkit/dev/incremental-generators
Switch source generators to incremental generators
This commit is contained in:
commit
5309963791
CommunityToolkit.Mvvm.SourceGenerators
AnalyzerReleases.Unshipped.mdCommunityToolkit.Mvvm.SourceGenerators.csproj
ComponentModel
INotifyPropertyChangedGenerator.cs
Models
AttributeInfo.csINotifyPropertyChangedInfo.csObservableRecipientInfo.csPropertyInfo.csTypedConstantInfo.Comparer.csTypedConstantInfo.Factory.csTypedConstantInfo.csValidationInfo.cs
ObservableObjectGenerator.csObservablePropertyGenerator.Execute.csObservablePropertyGenerator.SyntaxReceiver.csObservablePropertyGenerator.csObservableRecipientGenerator.csObservableValidatorValidateAllPropertiesGenerator.Execute.csObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.csObservableValidatorValidateAllPropertiesGenerator.csTransitiveMembersGenerator.Execute.csTransitiveMembersGenerator.SyntaxReceiver.csTransitiveMembersGenerator.csDiagnostics
EmbeddedResources
Extensions
AttributeDataExtensions.csHashCodeExtensions.csIEqualityComparerExtensions.csINamedTypeSymbolExtensions.csISymbolExtensions.csIncrementalGeneratorInitializationContextExtensions.csIncrementalValuesProviderExtensions.cs
Helpers
Input
Messaging
IMessengerRegisterAllGenerator.Execute.csIMessengerRegisterAllGenerator.SyntaxReceiver.csIMessengerRegisterAllGenerator.cs
Models
Models
CommunityToolkit.Mvvm
ThirdPartyNotices.txttests
CommunityToolkit.Mvvm.SourceGenerators.UnitTests
CommunityToolkit.Mvvm.UnitTests
@ -7,18 +7,13 @@ ### New Rules
|
||||
--------|----------|----------|-------
|
||||
MVVMTK0001 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0002 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0003 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0004 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0005 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0006 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0007 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0008 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0009 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0010 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0011 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0003 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0004 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0005 | CommunityToolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0006 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0007 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0008 | Microsoft.CodeAnalysis.CSharp.CSharpParseOptions | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0009 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0010 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0011 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0012 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0013 | Microsoft.CodeAnalysis.CSharp.CSharpParseOptions | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0014 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0015 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0016 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
MVVMTK0017 | CommunityToolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error
|
||||
|
@ -32,7 +32,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.11.0" PrivateAssets="all" Pack="false" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" Pack="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -3,11 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
@ -15,62 +18,56 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
/// <summary>
|
||||
/// A source generator for the <c>INotifyPropertyChangedAttribute</c> type.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class INotifyPropertyChangedGenerator : TransitiveMembersGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed class INotifyPropertyChangedGenerator : TransitiveMembersGenerator<INotifyPropertyChangedInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="INotifyPropertyChangedGenerator"/> class.
|
||||
/// </summary>
|
||||
public INotifyPropertyChangedGenerator()
|
||||
: base("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute")
|
||||
: base("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute")
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override DiagnosticDescriptor TargetTypeErrorDescriptor => INotifyPropertyChangedGeneratorError;
|
||||
protected override INotifyPropertyChangedInfo GetInfo(INamedTypeSymbol typeSymbol, AttributeData attributeData)
|
||||
{
|
||||
bool includeAdditionalHelperMethods = attributeData.GetNamedArgument<bool>("IncludeAdditionalHelperMethods", true);
|
||||
|
||||
return new(includeAdditionalHelperMethods);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ValidateTargetType(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
[NotNullWhen(false)] out DiagnosticDescriptor? descriptor)
|
||||
protected override bool ValidateTargetType(INamedTypeSymbol typeSymbol, INotifyPropertyChangedInfo info, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
INamedTypeSymbol iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!;
|
||||
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
||||
|
||||
// Check if the type already implements INotifyPropertyChanged
|
||||
if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangedSymbol)))
|
||||
if (typeSymbol.AllInterfaces.Any(i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanged")))
|
||||
{
|
||||
descriptor = DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError;
|
||||
builder.Add(DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError, typeSymbol, typeSymbol);
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<MemberDeclarationSyntax> FilterDeclaredMembers(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
ClassDeclarationSyntax sourceDeclaration)
|
||||
protected override ImmutableArray<MemberDeclarationSyntax> FilterDeclaredMembers(INotifyPropertyChangedInfo info, ImmutableArray<MemberDeclarationSyntax> memberDeclarations)
|
||||
{
|
||||
// If requested, only include the event and the basic methods to raise it, but not the additional helpers
|
||||
if (attributeData.HasNamedArgument("IncludeAdditionalHelperMethods", false))
|
||||
if (!info.IncludeAdditionalHelperMethods)
|
||||
{
|
||||
return sourceDeclaration.Members.Where(static member =>
|
||||
{
|
||||
return member
|
||||
is EventFieldDeclarationSyntax
|
||||
or MethodDeclarationSyntax { Identifier: { ValueText: "OnPropertyChanged" } };
|
||||
});
|
||||
return memberDeclarations.Where(static member => member
|
||||
is EventFieldDeclarationSyntax
|
||||
or MethodDeclarationSyntax { Identifier.ValueText: "OnPropertyChanged" }).ToImmutableArray();
|
||||
}
|
||||
|
||||
return sourceDeclaration.Members;
|
||||
return memberDeclarations;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,118 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model representing an attribute declaration.
|
||||
/// </summary>
|
||||
internal sealed record AttributeInfo(
|
||||
string TypeName,
|
||||
ImmutableArray<TypedConstantInfo> ConstructorArgumentInfo,
|
||||
ImmutableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="AttributeInfo"/> instance from a given <see cref="AttributeData"/> value.
|
||||
/// </summary>
|
||||
/// <param name="attributeData">The input <see cref="AttributeData"/> value.</param>
|
||||
/// <returns>A <see cref="AttributeInfo"/> instance representing <paramref name="attributeData"/>.</returns>
|
||||
public static AttributeInfo From(AttributeData attributeData)
|
||||
{
|
||||
string typeName = attributeData.AttributeClass!.GetFullyQualifiedName();
|
||||
|
||||
// Get the constructor arguments
|
||||
ImmutableArray<TypedConstantInfo> constructorArguments =
|
||||
attributeData.ConstructorArguments
|
||||
.Select(TypedConstantInfo.From)
|
||||
.ToImmutableArray();
|
||||
|
||||
// Get the named arguments
|
||||
ImmutableArray<(string, TypedConstantInfo)>.Builder namedArguments = ImmutableArray.CreateBuilder<(string, TypedConstantInfo)>();
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> arg in attributeData.NamedArguments)
|
||||
{
|
||||
namedArguments.Add((arg.Key, TypedConstantInfo.From(arg.Value)));
|
||||
}
|
||||
|
||||
return new(
|
||||
typeName,
|
||||
constructorArguments,
|
||||
namedArguments.ToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an <see cref="AttributeSyntax"/> instance representing the current value.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ExpressionSyntax"/> instance representing the current value.</returns>
|
||||
public AttributeSyntax GetSyntax()
|
||||
{
|
||||
// Gather the constructor arguments
|
||||
IEnumerable<AttributeArgumentSyntax> arguments =
|
||||
ConstructorArgumentInfo
|
||||
.Select(static arg => AttributeArgument(arg.GetSyntax()));
|
||||
|
||||
// Gather the named arguments
|
||||
IEnumerable<AttributeArgumentSyntax> namedArguments =
|
||||
NamedArgumentInfo.Select(static arg =>
|
||||
AttributeArgument(arg.Value.GetSyntax())
|
||||
.WithNameEquals(NameEquals(IdentifierName(arg.Name))));
|
||||
|
||||
return Attribute(IdentifierName(TypeName), AttributeArgumentList(SeparatedList(arguments.Concat(namedArguments))));
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="AttributeInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<AttributeInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, AttributeInfo obj)
|
||||
{
|
||||
hashCode.Add(obj.TypeName);
|
||||
hashCode.AddRange(obj.ConstructorArgumentInfo, TypedConstantInfo.Comparer.Default);
|
||||
|
||||
foreach ((string key, TypedConstantInfo value) in obj.NamedArgumentInfo)
|
||||
{
|
||||
hashCode.Add(key);
|
||||
hashCode.Add(value, TypedConstantInfo.Comparer.Default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(AttributeInfo x, AttributeInfo y)
|
||||
{
|
||||
if (x.TypeName != y.TypeName ||
|
||||
!x.ConstructorArgumentInfo.SequenceEqual(y.ConstructorArgumentInfo, TypedConstantInfo.Comparer.Default) ||
|
||||
x.NamedArgumentInfo.Length != y.NamedArgumentInfo.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < x.NamedArgumentInfo.Length; i++)
|
||||
{
|
||||
(string Name, TypedConstantInfo Value) left = x.NamedArgumentInfo[i];
|
||||
(string Name, TypedConstantInfo Value) right = y.NamedArgumentInfo[i];
|
||||
|
||||
if (left.Name != right.Name ||
|
||||
!TypedConstantInfo.Comparer.Default.Equals(left.Value, right.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// 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.
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model with gathered info on a given <c>INotifyPropertyChangedAttribute</c> instance.
|
||||
/// </summary>
|
||||
/// <param name="IncludeAdditionalHelperMethods">Whether to also generate helper methods in the target type.</param>
|
||||
public sealed record INotifyPropertyChangedInfo(bool IncludeAdditionalHelperMethods);
|
@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model with gathered info on a given <c>ObservableRecipientAttribute</c> instance.
|
||||
/// </summary>
|
||||
/// <param name="TypeName">The type name of the type being annotated.</param>
|
||||
/// <param name="HasExplicitConstructors">Whether or not the target type has explicit constructors.</param>
|
||||
/// <param name="IsAbstract">Whether or not the target type is abstract.</param>
|
||||
/// <param name="IsObservableValidator">Whether or not the target type inherits from <c>ObservableValidator</c>.</param>
|
||||
public sealed record ObservableRecipientInfo(
|
||||
string TypeName,
|
||||
bool HasExplicitConstructors,
|
||||
bool IsAbstract,
|
||||
bool IsObservableValidator);
|
@ -0,0 +1,67 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model representing an generated property
|
||||
/// </summary>
|
||||
/// <param name="TypeName">The type name for the generated property.</param>
|
||||
/// <param name="IsNullableReferenceType">Whether or not the property is of a nullable reference type.</param>
|
||||
/// <param name="FieldName">The field name.</param>
|
||||
/// <param name="PropertyName">The generated property name.</param>
|
||||
/// <param name="PropertyChangingNames">The sequence of property changing properties to notify.</param>
|
||||
/// <param name="PropertyChangedNames">The sequence of property changed properties to notify.</param>
|
||||
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
|
||||
/// <param name="ValidationAttributes">The sequence of validation attributes for the generated property.</param>
|
||||
internal sealed record PropertyInfo(
|
||||
string TypeName,
|
||||
bool IsNullableReferenceType,
|
||||
string FieldName,
|
||||
string PropertyName,
|
||||
ImmutableArray<string> PropertyChangingNames,
|
||||
ImmutableArray<string> PropertyChangedNames,
|
||||
ImmutableArray<string> NotifiedCommandNames,
|
||||
ImmutableArray<AttributeInfo> ValidationAttributes)
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="PropertyInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<PropertyInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, PropertyInfo obj)
|
||||
{
|
||||
hashCode.Add(obj.TypeName);
|
||||
hashCode.Add(obj.IsNullableReferenceType);
|
||||
hashCode.Add(obj.FieldName);
|
||||
hashCode.Add(obj.PropertyName);
|
||||
hashCode.AddRange(obj.PropertyChangingNames);
|
||||
hashCode.AddRange(obj.PropertyChangedNames);
|
||||
hashCode.AddRange(obj.NotifiedCommandNames);
|
||||
hashCode.AddRange(obj.ValidationAttributes, AttributeInfo.Comparer.Default);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(PropertyInfo x, PropertyInfo y)
|
||||
{
|
||||
return
|
||||
x.TypeName == y.TypeName &&
|
||||
x.IsNullableReferenceType == y.IsNullableReferenceType &&
|
||||
x.FieldName == y.FieldName &&
|
||||
x.PropertyName == y.PropertyName &&
|
||||
x.PropertyChangingNames.SequenceEqual(y.PropertyChangingNames) &&
|
||||
x.PropertyChangedNames.SequenceEqual(y.PropertyChangedNames) &&
|
||||
x.NotifiedCommandNames.SequenceEqual(y.NotifiedCommandNames) &&
|
||||
x.ValidationAttributes.SequenceEqual(y.ValidationAttributes, AttributeInfo.Comparer.Default);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial record TypedConstantInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="TypedConstantInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<TypedConstantInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, TypedConstantInfo obj)
|
||||
{
|
||||
obj.AddToHashCode(ref hashCode);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(TypedConstantInfo x, TypedConstantInfo y)
|
||||
{
|
||||
return x.IsEqualTo(y);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial record TypedConstantInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="TypedConstantInfo"/> instance from a given <see cref="TypedConstant"/> value.
|
||||
/// </summary>
|
||||
/// <param name="arg">The input <see cref="TypedConstant"/> value.</param>
|
||||
/// <returns>A <see cref="TypedConstantInfo"/> instance representing <paramref name="arg"/>.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the input argument is not valid.</exception>
|
||||
public static TypedConstantInfo From(TypedConstant arg)
|
||||
{
|
||||
if (arg.IsNull)
|
||||
{
|
||||
return new Null();
|
||||
}
|
||||
|
||||
if (arg.Kind == TypedConstantKind.Array)
|
||||
{
|
||||
string elementTypeName = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
ImmutableArray<TypedConstantInfo> items = arg.Values.Select(From).ToImmutableArray();
|
||||
|
||||
return new Array(elementTypeName, items);
|
||||
}
|
||||
|
||||
return (arg.Kind, arg.Value) switch
|
||||
{
|
||||
(TypedConstantKind.Primitive, string text) => new Primitive.String(text),
|
||||
(TypedConstantKind.Primitive, bool flag) => new Primitive.Boolean(flag),
|
||||
(TypedConstantKind.Primitive, object value) => value switch
|
||||
{
|
||||
byte b => new Primitive.Of<byte>(b),
|
||||
char c => new Primitive.Of<char>(c),
|
||||
double d => new Primitive.Of<double>(d),
|
||||
float f => new Primitive.Of<float>(f),
|
||||
int i => new Primitive.Of<int>(i),
|
||||
long l => new Primitive.Of<long>(l),
|
||||
sbyte sb => new Primitive.Of<sbyte>(sb),
|
||||
short sh => new Primitive.Of<short>(sh),
|
||||
uint ui => new Primitive.Of<uint>(ui),
|
||||
ulong ul => new Primitive.Of<ulong>(ul),
|
||||
ushort ush => new Primitive.Of<ushort>(ush),
|
||||
_ => throw new ArgumentException("Invalid primitive type")
|
||||
},
|
||||
(TypedConstantKind.Type, ITypeSymbol type) => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
(TypedConstantKind.Enum, object value) => new Enum(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value),
|
||||
_ => throw new ArgumentException("Invalid typed constant type"),
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
// 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;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model representing a typed constant item.
|
||||
/// </summary>
|
||||
/// <remarks>This model is fully serializeable and comparable.</remarks>
|
||||
internal abstract partial record TypedConstantInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an <see cref="ExpressionSyntax"/> instance representing the current constant.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="ExpressionSyntax"/> instance representing the current constant.</returns>
|
||||
public abstract ExpressionSyntax GetSyntax();
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the current instance is the same as an input one.
|
||||
/// </summary>
|
||||
/// <param name="other">The <see cref="TypedConstantInfo"/> instance to compare to.</param>
|
||||
/// <returns>Whether or not the two instances are the same.</returns>
|
||||
/// <remarks>This method differs from <see cref="Equals(TypedConstantInfo?)"/> in that it checks for deep equality.</remarks>
|
||||
protected abstract bool IsEqualTo(TypedConstantInfo other);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the current instance to an incremental <see cref="HashCode"/> value.
|
||||
/// </summary>
|
||||
/// <param name="hashCode">The target <see cref="HashCode"/> value.</param>
|
||||
protected abstract void AddToHashCode(ref HashCode hashCode);
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing an array.
|
||||
/// </summary>
|
||||
/// <param name="ElementTypeName">The type name for array elements.</param>
|
||||
/// <param name="Items">The sequence of contained elements.</param>
|
||||
public sealed record Array(string ElementTypeName, ImmutableArray<TypedConstantInfo> Items) : TypedConstantInfo
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return
|
||||
ArrayCreationExpression(
|
||||
ArrayType(IdentifierName(ElementTypeName))
|
||||
.AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList<ExpressionSyntax>(OmittedArraySizeExpression()))))
|
||||
.WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression)
|
||||
.AddExpressions(Items.Select(static c => c.GetSyntax()).ToArray()));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
if (other is Array array &&
|
||||
ElementTypeName == array.ElementTypeName &&
|
||||
Items.Length == array.Items.Length)
|
||||
{
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
if (!Items[i].IsEqualTo(array.Items[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add(ElementTypeName);
|
||||
|
||||
foreach (TypedConstantInfo item in Items)
|
||||
{
|
||||
item.AddToHashCode(ref hashCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing a primitive value.
|
||||
/// </summary>
|
||||
public abstract record Primitive : TypedConstantInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing a <see cref="string"/> value.
|
||||
/// </summary>
|
||||
/// <param name="Value">The input <see cref="string"/> value.</param>
|
||||
public sealed record String(string Value) : TypedConstantInfo
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(Value));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
return
|
||||
other is String @string &&
|
||||
Value == @string.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing a <see cref="bool"/> value.
|
||||
/// </summary>
|
||||
/// <param name="Value">The input <see cref="bool"/> value.</param>
|
||||
public sealed record Boolean(bool Value) : TypedConstantInfo
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return LiteralExpression(Value ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
return
|
||||
other is Boolean @bool &&
|
||||
Value == @bool.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing a generic primitive value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The primitive type.</typeparam>
|
||||
/// <param name="Value">The input primitive value.</param>
|
||||
public sealed record Of<T>(T Value) : TypedConstantInfo
|
||||
where T : unmanaged, IEquatable<T>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return LiteralExpression(SyntaxKind.NumericLiteralExpression, Value switch
|
||||
{
|
||||
byte b => Literal(b),
|
||||
char c => Literal(c),
|
||||
double d => Literal(d),
|
||||
float f => Literal(f),
|
||||
int i => Literal(i),
|
||||
long l => Literal(l),
|
||||
sbyte sb => Literal(sb),
|
||||
short sh => Literal(sh),
|
||||
uint ui => Literal(ui),
|
||||
ulong ul => Literal(ul),
|
||||
ushort ush => Literal(ush),
|
||||
_ => throw new ArgumentException("Invalid primitive type")
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
return
|
||||
other is Of<T> box &&
|
||||
Value.Equals(box.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add(Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing a type.
|
||||
/// </summary>
|
||||
/// <param name="TypeName">The input type name.</param>
|
||||
public sealed record Type(string TypeName) : TypedConstantInfo
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return TypeOfExpression(IdentifierName(TypeName));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
return
|
||||
other is Type type &&
|
||||
TypeName == type.TypeName;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add(TypeName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing an enum value.
|
||||
/// </summary>
|
||||
/// <param name="TypeName">The enum type name.</param>
|
||||
/// <param name="Value">The boxed enum value.</param>
|
||||
public sealed record Enum(string TypeName, object Value) : TypedConstantInfo
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return
|
||||
CastExpression(
|
||||
IdentifierName(TypeName),
|
||||
LiteralExpression(SyntaxKind.NumericLiteralExpression, ParseToken(Value.ToString())));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
return
|
||||
other is Enum @enum &&
|
||||
TypeName == @enum.TypeName &&
|
||||
Value.Equals(@enum.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add(TypeName);
|
||||
hashCode.Add(Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="TypedConstantInfo"/> type representing a <see langword="null"/> value.
|
||||
/// </summary>
|
||||
public sealed record Null : TypedConstantInfo
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override ExpressionSyntax GetSyntax()
|
||||
{
|
||||
return LiteralExpression(SyntaxKind.NullLiteralExpression);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool IsEqualTo(TypedConstantInfo other)
|
||||
{
|
||||
return other is Null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode)
|
||||
{
|
||||
hashCode.Add((object?)null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model with gathered info on all validatable properties in a given type.
|
||||
/// </summary>
|
||||
/// <param name="FilenameHint">The filename hint for the current type.</param>
|
||||
/// <param name="TypeName">The fully qualified type name of the target type.</param>
|
||||
/// <param name="PropertyNames">The name of validatable properties.</param>
|
||||
internal sealed record ValidationInfo(
|
||||
string FilenameHint,
|
||||
string TypeName,
|
||||
ImmutableArray<string> PropertyNames)
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="ValidationInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<ValidationInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, ValidationInfo obj)
|
||||
{
|
||||
hashCode.Add(obj.FilenameHint);
|
||||
hashCode.Add(obj.TypeName);
|
||||
hashCode.AddRange(obj.PropertyNames);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(ValidationInfo x, ValidationInfo y)
|
||||
{
|
||||
return
|
||||
x.FilenameHint == y.FilenameHint &&
|
||||
x.TypeName == y.TypeName &&
|
||||
x.PropertyNames.SequenceEqual(y.PropertyNames);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,12 @@
|
||||
// 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 System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
|
||||
@ -13,49 +16,56 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
/// <summary>
|
||||
/// A source generator for the <c>ObservableObjectAttribute</c> type.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class ObservableObjectGenerator : TransitiveMembersGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed class ObservableObjectGenerator : TransitiveMembersGenerator<object?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObservableObjectGenerator"/> class.
|
||||
/// </summary>
|
||||
public ObservableObjectGenerator()
|
||||
: base("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute")
|
||||
: base("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute")
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableObjectGeneratorError;
|
||||
protected override object? GetInfo(INamedTypeSymbol typeSymbol, AttributeData attributeData)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ValidateTargetType(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
[NotNullWhen(false)] out DiagnosticDescriptor? descriptor)
|
||||
protected override bool ValidateTargetType(INamedTypeSymbol typeSymbol, object? info, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
INamedTypeSymbol iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!;
|
||||
INamedTypeSymbol iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanging")!;
|
||||
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
||||
|
||||
// Check if the type already implements INotifyPropertyChanged...
|
||||
if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangedSymbol)))
|
||||
if (typeSymbol.AllInterfaces.Any(i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanged")))
|
||||
{
|
||||
descriptor = DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError;
|
||||
builder.Add(DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError, typeSymbol, typeSymbol);
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ...or INotifyPropertyChanging
|
||||
if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangingSymbol)))
|
||||
if (typeSymbol.AllInterfaces.Any(i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanging")))
|
||||
{
|
||||
descriptor = DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError;
|
||||
builder.Add(DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError, typeSymbol, typeSymbol);
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override ImmutableArray<MemberDeclarationSyntax> FilterDeclaredMembers(object? info, ImmutableArray<MemberDeclarationSyntax> memberDeclarations)
|
||||
{
|
||||
return memberDeclarations;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,414 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial class ObservablePropertyGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for all the logic for <see cref="ObservablePropertyGenerator"/>.
|
||||
/// </summary>
|
||||
internal static class Execute
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes a given field.
|
||||
/// </summary>
|
||||
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
||||
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
|
||||
/// <returns>The resulting <see cref="PropertyInfo"/> instance for <paramref name="fieldSymbol"/>.</returns>
|
||||
public static PropertyInfo GetInfo(IFieldSymbol fieldSymbol, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
||||
|
||||
// Check whether the containing type implements INotifyPropertyChanging and whether it inherits from ObservableValidator
|
||||
bool isObservableObject = fieldSymbol.ContainingType.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject");
|
||||
bool isObservableValidator = fieldSymbol.ContainingType.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
|
||||
bool isNotifyPropertyChanging = fieldSymbol.ContainingType.AllInterfaces.Any(static i => i.HasFullyQualifiedName("global::System.ComponentModel.INotifyPropertyChanging"));
|
||||
bool hasObservableObjectAttribute = fieldSymbol.ContainingType.GetAttributes().Any(static a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") == true);
|
||||
|
||||
// Get the property type and name
|
||||
string typeName = fieldSymbol.Type.GetFullyQualifiedName();
|
||||
bool isNullableReferenceType = fieldSymbol.Type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated };
|
||||
string fieldName = fieldSymbol.Name;
|
||||
string propertyName = GetGeneratedPropertyName(fieldSymbol);
|
||||
|
||||
ImmutableArray<string>.Builder propertyChangedNames = ImmutableArray.CreateBuilder<string>();
|
||||
ImmutableArray<string>.Builder propertyChangingNames = ImmutableArray.CreateBuilder<string>();
|
||||
ImmutableArray<string>.Builder notifiedCommandNames = ImmutableArray.CreateBuilder<string>();
|
||||
ImmutableArray<AttributeInfo>.Builder validationAttributes = ImmutableArray.CreateBuilder<AttributeInfo>();
|
||||
|
||||
// Track the property changing event for the property, if the type supports it
|
||||
if (isObservableObject || isNotifyPropertyChanging || hasObservableObjectAttribute)
|
||||
{
|
||||
propertyChangingNames.Add(propertyName);
|
||||
}
|
||||
|
||||
// The current property is always notified
|
||||
propertyChangedNames.Add(propertyName);
|
||||
|
||||
// Gather attributes info
|
||||
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
|
||||
{
|
||||
// Add dependent property notifications, if needed
|
||||
if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute") == true)
|
||||
{
|
||||
foreach (string dependentPropertyName in attributeData.GetConstructorArguments<string>())
|
||||
{
|
||||
propertyChangedNames.Add(dependentPropertyName);
|
||||
}
|
||||
}
|
||||
else if (attributeData.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute") == true)
|
||||
{
|
||||
// Add dependent relay command notifications, if needed
|
||||
foreach (string commandName in attributeData.GetConstructorArguments<string>())
|
||||
{
|
||||
notifiedCommandNames.Add(commandName);
|
||||
}
|
||||
}
|
||||
else if (attributeData.AttributeClass?.InheritsFrom("global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true)
|
||||
{
|
||||
// Track the current validation attribute
|
||||
validationAttributes.Add(AttributeInfo.From(attributeData));
|
||||
}
|
||||
}
|
||||
|
||||
// Log the diagnostics if needed
|
||||
if (validationAttributes.Count > 0 &&
|
||||
!isObservableValidator)
|
||||
{
|
||||
builder.Add(
|
||||
MissingObservableValidatorInheritanceError,
|
||||
fieldSymbol,
|
||||
fieldSymbol.ContainingType,
|
||||
fieldSymbol.Name,
|
||||
validationAttributes.Count);
|
||||
}
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return new(
|
||||
typeName,
|
||||
isNullableReferenceType,
|
||||
fieldName,
|
||||
propertyName,
|
||||
propertyChangingNames.ToImmutable(),
|
||||
propertyChangedNames.ToImmutable(),
|
||||
notifiedCommandNames.ToImmutable(),
|
||||
validationAttributes.ToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
|
||||
/// </summary>
|
||||
/// <param name="names">The sequence of property names to cache args for.</param>
|
||||
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
||||
public static CompilationUnitSyntax? GetKnownPropertyChangingArgsSyntax(ImmutableArray<string> names)
|
||||
{
|
||||
return GetKnownPropertyChangingOrChangedArgsSyntax(
|
||||
"__KnownINotifyPropertyChangingArgs",
|
||||
"global::System.ComponentModel.PropertyChangingEventArgs",
|
||||
names);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changed notifications.
|
||||
/// </summary>
|
||||
/// <param name="names">The sequence of property names to cache args for.</param>
|
||||
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
||||
public static CompilationUnitSyntax? GetKnownPropertyChangedArgsSyntax(ImmutableArray<string> names)
|
||||
{
|
||||
return GetKnownPropertyChangingOrChangedArgsSyntax(
|
||||
"__KnownINotifyPropertyChangedArgs",
|
||||
"global::System.ComponentModel.PropertyChangedEventArgs",
|
||||
names);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="MemberDeclarationSyntax"/> instance for the input field.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The input <see cref="PropertyInfo"/> instance to process.</param>
|
||||
/// <returns>The generated <see cref="MemberDeclarationSyntax"/> instance for <paramref name="propertyInfo"/>.</returns>
|
||||
public static MemberDeclarationSyntax GetSyntax(PropertyInfo propertyInfo)
|
||||
{
|
||||
ImmutableArray<StatementSyntax>.Builder setterStatements = ImmutableArray.CreateBuilder<StatementSyntax>();
|
||||
|
||||
// Gather the statements to notify dependent properties
|
||||
foreach (string propertyName in propertyInfo.PropertyChangingNames)
|
||||
{
|
||||
// This code generates a statement as follows:
|
||||
//
|
||||
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.<PROPERTY_NAME>);
|
||||
setterStatements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanging"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs"),
|
||||
IdentifierName(propertyName))))));
|
||||
}
|
||||
|
||||
// In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments
|
||||
// with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter.
|
||||
ExpressionSyntax fieldExpression = propertyInfo.FieldName switch
|
||||
{
|
||||
"value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")),
|
||||
string name => IdentifierName(name)
|
||||
};
|
||||
|
||||
// Add the assignment statement:
|
||||
//
|
||||
// <FIELD_EXPRESSION> = value;
|
||||
setterStatements.Add(
|
||||
ExpressionStatement(
|
||||
AssignmentExpression(
|
||||
SyntaxKind.SimpleAssignmentExpression,
|
||||
fieldExpression,
|
||||
IdentifierName("value"))));
|
||||
|
||||
// If there are validation attributes, add a call to ValidateProperty:
|
||||
//
|
||||
// ValidateProperty(value, <PROPERTY_NAME>);
|
||||
if (propertyInfo.ValidationAttributes.Length > 0)
|
||||
{
|
||||
setterStatements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("ValidateProperty"))
|
||||
.AddArgumentListArguments(
|
||||
Argument(IdentifierName("value")),
|
||||
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName))))));
|
||||
}
|
||||
|
||||
// Gather the statements to notify dependent properties
|
||||
foreach (string propertyName in propertyInfo.PropertyChangedNames)
|
||||
{
|
||||
// This code generates a statement as follows:
|
||||
//
|
||||
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.<PROPERTY_NAME>);
|
||||
setterStatements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanged"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs"),
|
||||
IdentifierName(propertyName))))));
|
||||
}
|
||||
|
||||
// Gather the statements to notify commands
|
||||
foreach (string commandName in propertyInfo.NotifiedCommandNames)
|
||||
{
|
||||
// This code generates a statement as follows:
|
||||
//
|
||||
// <COMMAND_NAME>.NotifyCanExecuteChanged();
|
||||
setterStatements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName(commandName),
|
||||
IdentifierName("NotifyCanExecuteChanged")))));
|
||||
}
|
||||
|
||||
// Get the property type syntax (adding the nullability annotation, if needed)
|
||||
TypeSyntax propertyType = propertyInfo.IsNullableReferenceType
|
||||
? NullableType(IdentifierName(propertyInfo.TypeName))
|
||||
: IdentifierName(propertyInfo.TypeName);
|
||||
|
||||
// Generate the inner setter block as follows:
|
||||
//
|
||||
// if (!global::System.Collections.Generic.EqualityComparer<<PROPERTY_TYPE>>.Default.Equals(<FIELD_EXPRESSION>, value))
|
||||
// {
|
||||
// <STATEMENTS>
|
||||
// }
|
||||
IfStatementSyntax setterIfStatement =
|
||||
IfStatement(
|
||||
PrefixUnaryExpression(
|
||||
SyntaxKind.LogicalNotExpression,
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
|
||||
.AddTypeArgumentListArguments(propertyType),
|
||||
IdentifierName("Default")),
|
||||
IdentifierName("Equals")))
|
||||
.AddArgumentListArguments(
|
||||
Argument(fieldExpression),
|
||||
Argument(IdentifierName("value")))),
|
||||
Block(setterStatements));
|
||||
|
||||
// Prepare the validation attributes, if any
|
||||
ImmutableArray<AttributeListSyntax> validationAttributes =
|
||||
propertyInfo.ValidationAttributes
|
||||
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
|
||||
.ToImmutableArray();
|
||||
|
||||
// Construct the generated property as follows:
|
||||
//
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// <VALIDATION_ATTRIBUTES>
|
||||
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
|
||||
// {
|
||||
// get => <FIELD_NAME>;
|
||||
// set
|
||||
// {
|
||||
// <BODY>
|
||||
// }
|
||||
// }
|
||||
return
|
||||
PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
|
||||
.AddAttributeLists(validationAttributes.ToArray())
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddAccessorListAccessors(
|
||||
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
|
||||
.WithExpressionBody(ArrowExpressionClause(IdentifierName(propertyInfo.FieldName)))
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
|
||||
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
|
||||
.WithBody(Block(setterIfStatement)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args of a specified type.
|
||||
/// </summary>
|
||||
/// <param name="ContainingTypeName">The name of the generated type.</param>
|
||||
/// <param name="ArgsTypeName">The argument type name.</param>
|
||||
/// <param name="names">The sequence of property names to cache args for.</param>
|
||||
/// <returns>A <see cref="CompilationUnitSyntax"/> instance with the sequence of cached args, if any.</returns>
|
||||
private static CompilationUnitSyntax? GetKnownPropertyChangingOrChangedArgsSyntax(
|
||||
string ContainingTypeName,
|
||||
string ArgsTypeName,
|
||||
ImmutableArray<string> names)
|
||||
{
|
||||
if (names.IsEmpty)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// This code takes a class symbol and produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
// #pragma warning disable
|
||||
// #nullable enable
|
||||
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
||||
// {
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
||||
// internal static class <CONTAINING_TYPE_NAME>
|
||||
// {
|
||||
// <FIELDS>
|
||||
// }
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)),
|
||||
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration(ContainingTypeName).AddModifiers(
|
||||
Token(SyntaxKind.InternalKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This type is not intended to be used directly by user code")))))))
|
||||
.AddMembers(names.Select(name => CreateFieldDeclaration(ArgsTypeName, name)).ToArray())))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a field declaration for a cached property changing/changed name.
|
||||
/// </summary>
|
||||
/// <param name="typeName">The field type name (either <see cref="PropertyChangedEventArgs"/> or <see cref="PropertyChangingEventArgs"/>).</param>
|
||||
/// <param name="propertyName">The name of the cached property name.</param>
|
||||
/// <returns>A <see cref="FieldDeclarationSyntax"/> instance for the input cached property name.</returns>
|
||||
private static FieldDeclarationSyntax CreateFieldDeclaration(string typeName, string propertyName)
|
||||
{
|
||||
// Create a static field with a cached property changed/changing argument for a specified property.
|
||||
// This code produces a field declaration as follows:
|
||||
//
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This field is not intended to be referenced directly by user code")]
|
||||
// public static readonly <ARG_TYPE> <PROPERTY_NAME> = new("<PROPERTY_NAME>");
|
||||
return
|
||||
FieldDeclaration(
|
||||
VariableDeclaration(IdentifierName(typeName))
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier(propertyName))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
ImplicitObjectCreationExpression()
|
||||
.AddArgumentListArguments(Argument(
|
||||
LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName))))))))
|
||||
.AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword),
|
||||
Token(SyntaxKind.ReadOnlyKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This field is not intended to be referenced directly by user code")))))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generated property name for an input field.
|
||||
/// </summary>
|
||||
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
||||
/// <returns>The generated property name for <paramref name="fieldSymbol"/>.</returns>
|
||||
public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol)
|
||||
{
|
||||
string propertyName = fieldSymbol.Name;
|
||||
|
||||
if (propertyName.StartsWith("m_"))
|
||||
{
|
||||
propertyName = propertyName.Substring(2);
|
||||
}
|
||||
else if (propertyName.StartsWith("_"))
|
||||
{
|
||||
propertyName = propertyName.TrimStart('_');
|
||||
}
|
||||
|
||||
return $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc cref="ObservablePropertyGenerator"/>
|
||||
public sealed partial class ObservablePropertyGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISyntaxContextReceiver"/> that selects candidate nodes to process.
|
||||
/// </summary>
|
||||
private sealed class SyntaxReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of info gathered during exploration.
|
||||
/// </summary>
|
||||
private readonly List<Item> gatheredInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of gathered info to process.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Item> GatheredInfo => this.gatheredInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclaration &&
|
||||
context.SemanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol)
|
||||
{
|
||||
SyntaxTriviaList leadingTrivia = fieldDeclaration.GetLeadingTrivia();
|
||||
|
||||
foreach (VariableDeclaratorSyntax variableDeclarator in fieldDeclaration.Declaration.Variables)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(variableDeclarator) is IFieldSymbol fieldSymbol &&
|
||||
fieldSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)))
|
||||
{
|
||||
this.gatheredInfo.Add(new Item(leadingTrivia, fieldSymbol));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A model for a group of item representing a discovered type to process.
|
||||
/// </summary>
|
||||
/// <param name="LeadingTrivia">The leading trivia for the field declaration.</param>
|
||||
/// <param name="FieldSymbol">The <see cref="IFieldSymbol"/> instance for the target field.</param>
|
||||
public sealed record Item(SyntaxTriviaList LeadingTrivia, IFieldSymbol FieldSymbol);
|
||||
}
|
||||
}
|
@ -3,17 +3,17 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
@ -21,545 +21,111 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
/// <summary>
|
||||
/// A source generator for the <c>ObservablePropertyAttribute</c> type.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed partial class ObservablePropertyGenerator : ISourceGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed partial class ObservablePropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
|
||||
}
|
||||
// Get all field declarations with at least one attribute
|
||||
IncrementalValuesProvider<IFieldSymbol> fieldSymbols =
|
||||
context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
static (node, _) => node is FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 },
|
||||
static (context, _) => ((FieldDeclarationSyntax)context.Node).Declaration.Variables.Select(v => (IFieldSymbol)context.SemanticModel.GetDeclaredSymbol(v)!))
|
||||
.SelectMany(static (item, _) => item);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
// Get the syntax receiver with the candidate nodes
|
||||
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
|
||||
syntaxReceiver.GatheredInfo.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Filter the fields using [ObservableProperty]
|
||||
IncrementalValuesProvider<IFieldSymbol> fieldSymbolsWithAttribute =
|
||||
fieldSymbols
|
||||
.Where(static item => item.GetAttributes().Any(a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true));
|
||||
|
||||
// Validate the language version. Note that we're emitting this diagnostic in each generator (excluding the one
|
||||
// only emitting the nullability annotation attributes if missing) so that the diagnostic is emitted only when
|
||||
// users are using one of these generators, and not by default as soon as they add a reference to the MVVM Toolkit.
|
||||
// This ensures that users not using any of the source generators won't be broken when upgrading to this new version.
|
||||
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
|
||||
}
|
||||
// Filter by language version
|
||||
context.FilterWithLanguageVersion(ref fieldSymbolsWithAttribute, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
|
||||
|
||||
// Sets of discovered property names
|
||||
HashSet<string> propertyChangedNames = new();
|
||||
HashSet<string> propertyChangingNames = new();
|
||||
|
||||
// Process the annotated fields
|
||||
foreach (IGrouping<INamedTypeSymbol, SyntaxReceiver.Item>? items in syntaxReceiver.GatheredInfo.GroupBy<SyntaxReceiver.Item, INamedTypeSymbol>(static item => item.FieldSymbol.ContainingType, SymbolEqualityComparer.Default))
|
||||
{
|
||||
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&
|
||||
items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration)
|
||||
// Gather info for all annotated fields
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<PropertyInfo> Info)> propertyInfoWithErrors =
|
||||
fieldSymbolsWithAttribute
|
||||
.Select(static (item, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
OnExecuteForProperties(context, classDeclaration, items.Key, items, propertyChangedNames, propertyChangingNames);
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.ReportDiagnostic(ObservablePropertyGeneratorError, items.Key, items.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
HierarchyInfo hierarchy = HierarchyInfo.From(item.ContainingType);
|
||||
PropertyInfo propertyInfo = Execute.GetInfo(item, out ImmutableArray<Diagnostic> diagnostics);
|
||||
|
||||
// Process the fields for the cached args
|
||||
OnExecuteForPropertyArgs(context, propertyChangedNames, propertyChangingNames);
|
||||
}
|
||||
return (hierarchy, new Result<PropertyInfo>(propertyInfo, diagnostics));
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Processes a given target type for declared observable properties.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
|
||||
/// <param name="items">The sequence of fields to process.</param>
|
||||
/// <param name="propertyChangedNames">The collection of discovered property changed names.</param>
|
||||
/// <param name="propertyChangingNames">The collection of discovered property changing names.</param>
|
||||
private static void OnExecuteForProperties(
|
||||
GeneratorExecutionContext context,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
IEnumerable<SyntaxReceiver.Item> items,
|
||||
ICollection<string> propertyChangedNames,
|
||||
ICollection<string> propertyChangingNames)
|
||||
{
|
||||
INamedTypeSymbol iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanging")!;
|
||||
INamedTypeSymbol observableObjectSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject")!;
|
||||
INamedTypeSymbol observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute")!;
|
||||
INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")!;
|
||||
// Output the diagnostics
|
||||
context.ReportDiagnostics(propertyInfoWithErrors.Select(static (item, _) => item.Info.Errors));
|
||||
|
||||
// Check whether the current type implements INotifyPropertyChanging and whether it inherits from ObservableValidator
|
||||
bool isObservableObject = classDeclarationSymbol.InheritsFrom(observableObjectSymbol);
|
||||
bool isObservableValidator = classDeclarationSymbol.InheritsFrom(observableValidatorSymbol);
|
||||
bool isNotifyPropertyChanging =
|
||||
isObservableObject ||
|
||||
classDeclarationSymbol.AllInterfaces.Contains(iNotifyPropertyChangingSymbol, SymbolEqualityComparer.Default) ||
|
||||
classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol));
|
||||
// Get the filtered sequence to enable caching
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, PropertyInfo Info)> propertyInfo =
|
||||
propertyInfoWithErrors
|
||||
.Select(static (item, _) => (item.Hierarchy, item.Info.Value))
|
||||
.WithComparers(HierarchyInfo.Comparer.Default, PropertyInfo.Comparer.Default);
|
||||
|
||||
// Create the class declaration for the user type. This will produce a tree as follows:
|
||||
//
|
||||
// <MODIFIERS> <CLASS_NAME>
|
||||
// {
|
||||
// <MEMBERS>
|
||||
// }
|
||||
ClassDeclarationSyntax? classDeclarationSyntax =
|
||||
ClassDeclaration(classDeclarationSymbol.Name)
|
||||
.WithModifiers(classDeclaration.Modifiers)
|
||||
.AddMembers(items.Select(item =>
|
||||
CreatePropertyDeclaration(
|
||||
context,
|
||||
item.LeadingTrivia,
|
||||
item.FieldSymbol,
|
||||
isNotifyPropertyChanging,
|
||||
isObservableValidator,
|
||||
propertyChangedNames,
|
||||
propertyChangingNames)).ToArray());
|
||||
// Split and group by containing type
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, ImmutableArray<PropertyInfo> Properties)> groupedPropertyInfo =
|
||||
propertyInfo
|
||||
.GroupBy(HierarchyInfo.Comparer.Default)
|
||||
.WithComparers(HierarchyInfo.Comparer.Default, PropertyInfo.Comparer.Default.ForImmutableArray());
|
||||
|
||||
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
|
||||
|
||||
// Add all parent types in ascending order, if any
|
||||
foreach (TypeDeclarationSyntax? parentType in classDeclaration.Ancestors().OfType<TypeDeclarationSyntax>())
|
||||
// Generate the requested properties
|
||||
context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) =>
|
||||
{
|
||||
typeDeclarationSyntax = parentType
|
||||
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax))
|
||||
.WithConstraintClauses(List<TypeParameterConstraintClauseSyntax>())
|
||||
.WithBaseList(null)
|
||||
.WithAttributeLists(List<AttributeListSyntax>())
|
||||
.WithoutTrivia();
|
||||
}
|
||||
// Generate all properties for the current type
|
||||
ImmutableArray<MemberDeclarationSyntax> propertyDeclarations =
|
||||
item.Properties
|
||||
.Select(Execute.GetSyntax)
|
||||
.ToImmutableArray();
|
||||
|
||||
// Create the compilation unit with the namespace and target member.
|
||||
// From this, we can finally generate the source code to output.
|
||||
string? namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
|
||||
// Insert all properties into the same partial type declaration
|
||||
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(propertyDeclarations);
|
||||
|
||||
// Create the final compilation unit to generate (with leading trivia)
|
||||
string? source =
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
|
||||
.AddMembers(typeDeclarationSyntax))
|
||||
.NormalizeWhitespace()
|
||||
.ToFullString();
|
||||
context.AddSource(
|
||||
hintName: $"{item.Hierarchy.FilenameHint}.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
|
||||
// Add the partial type
|
||||
context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
// Gather all property changing names
|
||||
IncrementalValueProvider<ImmutableArray<string>> propertyChangingNames =
|
||||
propertyInfo
|
||||
.SelectMany(static (item, _) => item.Info.PropertyChangingNames)
|
||||
.Collect()
|
||||
.Select(static (item, _) => item.Distinct().ToImmutableArray())
|
||||
.WithComparer(EqualityComparer<string>.Default.ForImmutableArray());
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PropertyDeclarationSyntax"/> instance for a specified field.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="leadingTrivia">The leading trivia for the field to process.</param>
|
||||
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
||||
/// <param name="isNotifyPropertyChanging">Indicates whether or not <see cref="INotifyPropertyChanging"/> is also implemented.</param>
|
||||
/// <param name="isObservableValidator">Indicates whether or not the containing type inherits from <c>ObservableValidator</c>.</param>
|
||||
/// <param name="propertyChangedNames">The collection of discovered property changed names.</param>
|
||||
/// <param name="propertyChangingNames">The collection of discovered property changing names.</param>
|
||||
/// <returns>A generated <see cref="PropertyDeclarationSyntax"/> instance for the input field.</returns>
|
||||
private static PropertyDeclarationSyntax CreatePropertyDeclaration(
|
||||
GeneratorExecutionContext context,
|
||||
SyntaxTriviaList leadingTrivia,
|
||||
IFieldSymbol fieldSymbol,
|
||||
bool isNotifyPropertyChanging,
|
||||
bool isObservableValidator,
|
||||
ICollection<string> propertyChangedNames,
|
||||
ICollection<string> propertyChangingNames)
|
||||
{
|
||||
// Get the field type and the target property name
|
||||
string typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
string propertyName = GetGeneratedPropertyName(fieldSymbol);
|
||||
|
||||
INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute")!;
|
||||
INamedTypeSymbol alsoNotifyCanExecuteForAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.AlsoNotifyCanExecuteForAttribute")!;
|
||||
INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute");
|
||||
|
||||
List<StatementSyntax> dependentNotificationStatements = new();
|
||||
List<AttributeSyntax> validationAttributes = new();
|
||||
|
||||
foreach (AttributeData attributeData in fieldSymbol.GetAttributes())
|
||||
// Generate the cached property changing names
|
||||
context.RegisterSourceOutput(propertyChangingNames, static (context, item) =>
|
||||
{
|
||||
// Add dependent property notifications, if needed
|
||||
if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyChangeForAttributeSymbol))
|
||||
CompilationUnitSyntax? compilationUnit = Execute.GetKnownPropertyChangingArgsSyntax(item);
|
||||
|
||||
if (compilationUnit is not null)
|
||||
{
|
||||
foreach (string dependentPropertyName in attributeData.GetConstructorArguments<string>())
|
||||
{
|
||||
propertyChangedNames.Add(dependentPropertyName);
|
||||
|
||||
// OnPropertyChanged("<PROPERTY_NAME>");
|
||||
dependentNotificationStatements.Add(ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanged"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
|
||||
IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}"))))));
|
||||
}
|
||||
context.AddSource(
|
||||
hintName: "__KnownINotifyPropertyChangingArgs.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
}
|
||||
else if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyCanExecuteForAttributeSymbol))
|
||||
});
|
||||
|
||||
// Gather all property changed names
|
||||
IncrementalValueProvider<ImmutableArray<string>> propertyChangedNames =
|
||||
propertyInfo
|
||||
.SelectMany(static (item, _) => item.Info.PropertyChangedNames)
|
||||
.Collect()
|
||||
.Select(static (item, _) => item.Distinct().ToImmutableArray())
|
||||
.WithComparer(EqualityComparer<string>.Default.ForImmutableArray());
|
||||
|
||||
// Generate the cached property changed names
|
||||
context.RegisterSourceOutput(propertyChangedNames, static (context, item) =>
|
||||
{
|
||||
CompilationUnitSyntax? compilationUnit = Execute.GetKnownPropertyChangedArgsSyntax(item);
|
||||
|
||||
if (compilationUnit is not null)
|
||||
{
|
||||
// Add dependent relay command notifications, if needed
|
||||
foreach (string commandName in attributeData.GetConstructorArguments<string>())
|
||||
{
|
||||
// <PROPERTY_NAME>.NotifyCanExecuteChanged();
|
||||
dependentNotificationStatements.Add(ExpressionStatement(
|
||||
InvocationExpression(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName(commandName),
|
||||
IdentifierName("NotifyCanExecuteChanged")))));
|
||||
}
|
||||
context.AddSource(
|
||||
hintName: "__KnownINotifyPropertyChangedArgs.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
}
|
||||
else if (validationAttributeSymbol is not null &&
|
||||
attributeData.AttributeClass?.InheritsFrom(validationAttributeSymbol) == true)
|
||||
{
|
||||
// Track the current validation attribute
|
||||
validationAttributes.Add(attributeData.AsAttributeSyntax());
|
||||
}
|
||||
}
|
||||
|
||||
// In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments
|
||||
// with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter.
|
||||
ExpressionSyntax fieldExpression = fieldSymbol.Name switch
|
||||
{
|
||||
"value" => MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("value")),
|
||||
string name => IdentifierName(name)
|
||||
};
|
||||
|
||||
BlockSyntax setterBlock;
|
||||
|
||||
if (validationAttributes.Count > 0)
|
||||
{
|
||||
// Emit a diagnostic if the current type doesn't inherit from ObservableValidator
|
||||
if (!isObservableValidator)
|
||||
{
|
||||
context.ReportDiagnostic(
|
||||
MissingObservableValidatorInheritanceError,
|
||||
fieldSymbol,
|
||||
fieldSymbol.ContainingType,
|
||||
fieldSymbol.Name,
|
||||
validationAttributes.Count);
|
||||
|
||||
setterBlock = Block();
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyChangedNames.Add(propertyName);
|
||||
propertyChangingNames.Add(propertyName);
|
||||
|
||||
// Generate the inner setter block as follows:
|
||||
//
|
||||
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(this.<FIELD_NAME>, value))
|
||||
// {
|
||||
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangingEventArgs); // Optional
|
||||
// this.<FIELD_NAME> = value;
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangedEventArgs);
|
||||
// ValidateProperty(value, <PROPERTY_NAME>);
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_1>PropertyChangedEventArgs); // Optional
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_2>PropertyChangedEventArgs);
|
||||
// ...
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_N>PropertyChangedEventArgs);
|
||||
// <COMMAND_1>.NotifyCanExecuteChanged(); // Optional
|
||||
// <COMMAND_2>.NotifyCanExecuteChanged();
|
||||
// ...
|
||||
// <COMMAND_N>.NotifyCanExecuteChanged();
|
||||
// }
|
||||
//
|
||||
// The reason why the code is explicitly generated instead of just calling ObservableValidator.SetProperty() is so that we can
|
||||
// take advantage of the cached property changed arguments for the current property as well, not just for the dependent ones.
|
||||
setterBlock = Block(
|
||||
IfStatement(
|
||||
PrefixUnaryExpression(
|
||||
SyntaxKind.LogicalNotExpression,
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
|
||||
.AddTypeArgumentListArguments(IdentifierName(typeName)),
|
||||
IdentifierName("Default")),
|
||||
IdentifierName("Equals")))
|
||||
.AddArgumentListArguments(
|
||||
Argument(fieldExpression),
|
||||
Argument(IdentifierName("value")))),
|
||||
Block(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanging"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
|
||||
IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}"))))),
|
||||
ExpressionStatement(
|
||||
AssignmentExpression(
|
||||
SyntaxKind.SimpleAssignmentExpression,
|
||||
fieldExpression,
|
||||
IdentifierName("value"))),
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanged"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
|
||||
IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}"))))),
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("ValidateProperty"))
|
||||
.AddArgumentListArguments(
|
||||
Argument(IdentifierName("value")),
|
||||
Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName))))))
|
||||
.AddStatements(dependentNotificationStatements.ToArray())));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BlockSyntax updateAndNotificationBlock = Block();
|
||||
|
||||
// Add OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs) if necessary
|
||||
if (isNotifyPropertyChanging)
|
||||
{
|
||||
propertyChangingNames.Add(propertyName);
|
||||
|
||||
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanging"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
|
||||
IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}"))))));
|
||||
}
|
||||
|
||||
propertyChangedNames.Add(propertyName);
|
||||
|
||||
// Add the following statements:
|
||||
//
|
||||
// <FIELD_NAME> = value;
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs);
|
||||
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(
|
||||
ExpressionStatement(
|
||||
AssignmentExpression(
|
||||
SyntaxKind.SimpleAssignmentExpression,
|
||||
fieldExpression,
|
||||
IdentifierName("value"))),
|
||||
ExpressionStatement(
|
||||
InvocationExpression(IdentifierName("OnPropertyChanged"))
|
||||
.AddArgumentListArguments(Argument(MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"),
|
||||
IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}"))))));
|
||||
|
||||
// Add the dependent property notifications at the end
|
||||
updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentNotificationStatements.ToArray());
|
||||
|
||||
// Generate the inner setter block as follows:
|
||||
//
|
||||
// if (!global::System.Collections.Generic.EqualityComparer<<FIELD_TYPE>>.Default.Equals(<FIELD_NAME>, value))
|
||||
// {
|
||||
// OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangingEventArgs); // Optional
|
||||
// <FIELD_NAME> = value;
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_NAME>PropertyChangedEventArgs);
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_1>PropertyChangedEventArgs); // Optional
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_2>PropertyChangedEventArgs);
|
||||
// ...
|
||||
// OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.<PROPERTY_N>PropertyChangedEventArgs);
|
||||
// <COMMAND_1>.NotifyCanExecuteChanged(); // Optional
|
||||
// <COMMAND_2>.NotifyCanExecuteChanged();
|
||||
// ...
|
||||
// <COMMAND_N>.NotifyCanExecuteChanged();
|
||||
// }
|
||||
setterBlock = Block(
|
||||
IfStatement(
|
||||
PrefixUnaryExpression(
|
||||
SyntaxKind.LogicalNotExpression,
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
GenericName(Identifier("global::System.Collections.Generic.EqualityComparer"))
|
||||
.AddTypeArgumentListArguments(IdentifierName(typeName)),
|
||||
IdentifierName("Default")),
|
||||
IdentifierName("Equals")))
|
||||
.AddArgumentListArguments(
|
||||
Argument(fieldExpression),
|
||||
Argument(IdentifierName("value")))),
|
||||
updateAndNotificationBlock));
|
||||
}
|
||||
|
||||
// Get the right type for the declared property (including nullability annotations)
|
||||
TypeSyntax propertyType = IdentifierName(typeName);
|
||||
|
||||
if (fieldSymbol.Type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated })
|
||||
{
|
||||
propertyType = NullableType(propertyType);
|
||||
}
|
||||
|
||||
// Construct the generated property as follows:
|
||||
//
|
||||
// <FIELD_TRIVIA>
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// <VALIDATION_ATTRIBUTE1> // Optional
|
||||
// <VALIDATION_ATTRIBUTE2>
|
||||
// ...
|
||||
// <VALIDATION_ATTRIBUTEN>
|
||||
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
|
||||
// {
|
||||
// get => <FIELD_NAME>;
|
||||
// set
|
||||
// {
|
||||
// <BODY>
|
||||
// }
|
||||
// }
|
||||
return
|
||||
PropertyDeclaration(propertyType, Identifier(propertyName))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
|
||||
.AddAttributeLists(validationAttributes.Select(static a => AttributeList(SingletonSeparatedList(a))).ToArray())
|
||||
.WithLeadingTrivia(leadingTrivia.Where(static trivia => !trivia.IsKind(SyntaxKind.RegionDirectiveTrivia) && !trivia.IsKind(SyntaxKind.EndRegionDirectiveTrivia)))
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddAccessorListAccessors(
|
||||
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
|
||||
.WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldSymbol.Name)))
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
|
||||
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
|
||||
.WithBody(setterBlock));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generated property name for an input field.
|
||||
/// </summary>
|
||||
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
|
||||
/// <returns>The generated property name for <paramref name="fieldSymbol"/>.</returns>
|
||||
public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol)
|
||||
{
|
||||
string propertyName = fieldSymbol.Name;
|
||||
|
||||
if (propertyName.StartsWith("m_"))
|
||||
{
|
||||
propertyName = propertyName.Substring(2);
|
||||
}
|
||||
else if (propertyName.StartsWith("_"))
|
||||
{
|
||||
propertyName = propertyName.TrimStart('_');
|
||||
}
|
||||
|
||||
return $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the cached property changed/changing args.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="propertyChangedNames">The collection of discovered property changed names.</param>
|
||||
/// <param name="propertyChangingNames">The collection of discovered property changing names.</param>
|
||||
public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnlyCollection<string> propertyChangedNames, IReadOnlyCollection<string> propertyChangingNames)
|
||||
{
|
||||
if (propertyChangedNames.Count == 0 &&
|
||||
propertyChangingNames.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
INamedTypeSymbol propertyChangedEventArgsSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs")!;
|
||||
INamedTypeSymbol propertyChangingEventArgsSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs")!;
|
||||
|
||||
// Create a static method to validate all properties in a given class.
|
||||
// This code takes a class symbol and produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
//
|
||||
// #pragma warning disable
|
||||
//
|
||||
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
||||
// {
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
||||
// internal static class __KnownINotifyPropertyChangedOrChangingArgs
|
||||
// {
|
||||
// <FIELDS>
|
||||
// }
|
||||
// }
|
||||
string? source =
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__KnownINotifyPropertyChangedOrChangingArgs").AddModifiers(
|
||||
Token(SyntaxKind.InternalKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This type is not intended to be used directly by user code")))))))
|
||||
.AddMembers(propertyChangedNames.Select(name => CreateFieldDeclaration(propertyChangedEventArgsSymbol, name)).ToArray())
|
||||
.AddMembers(propertyChangingNames.Select(name => CreateFieldDeclaration(propertyChangingEventArgsSymbol, name)).ToArray())))
|
||||
.NormalizeWhitespace()
|
||||
.ToFullString();
|
||||
|
||||
// Add the partial type
|
||||
context.AddSource("__KnownINotifyPropertyChangedOrChangingArgs.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a field declaration for a cached property change name.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of cached property change argument (either <see cref="PropertyChangedEventArgs"/> or <see cref="PropertyChangingEventArgs"/>).</param>
|
||||
/// <param name="propertyName">The name of the cached property name.</param>
|
||||
/// <returns>A <see cref="FieldDeclarationSyntax"/> instance for the input cached property name.</returns>
|
||||
private static FieldDeclarationSyntax CreateFieldDeclaration(INamedTypeSymbol type, string propertyName)
|
||||
{
|
||||
// Create a static field with a cached property changed/changing argument for a specified property.
|
||||
// This code produces a field declaration as follows:
|
||||
//
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This field is not intended to be referenced directly by user code")]
|
||||
// public static readonly <ARG_TYPE> <PROPERTY_NAME><ARG_TYPE> = new("<PROPERTY_NAME>");
|
||||
return
|
||||
FieldDeclaration(
|
||||
VariableDeclaration(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier($"{propertyName}{type.Name}"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
ImplicitObjectCreationExpression()
|
||||
.AddArgumentListArguments(Argument(
|
||||
LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName))))))))
|
||||
.AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword),
|
||||
Token(SyntaxKind.ReadOnlyKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This field is not intended to be referenced directly by user code")))))));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,127 +2,127 @@
|
||||
// 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.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// A source generator for the <c>ObservableRecipientAttribute</c> type.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class ObservableRecipientGenerator : TransitiveMembersGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed class ObservableRecipientGenerator : TransitiveMembersGenerator<ObservableRecipientInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObservableRecipientGenerator"/> class.
|
||||
/// </summary>
|
||||
public ObservableRecipientGenerator()
|
||||
: base("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")
|
||||
: base("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableRecipientGeneratorError;
|
||||
protected override ObservableRecipientInfo GetInfo(INamedTypeSymbol typeSymbol, AttributeData attributeData)
|
||||
{
|
||||
string typeName = typeSymbol.Name;
|
||||
bool hasExplicitConstructors = !(typeSymbol.InstanceConstructors.Length == 1 && typeSymbol.InstanceConstructors[0] is { Parameters.IsEmpty: true, IsImplicitlyDeclared: true });
|
||||
bool isAbstract = typeSymbol.IsAbstract;
|
||||
bool isObservableValidator = typeSymbol.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
|
||||
|
||||
return new(
|
||||
typeName,
|
||||
hasExplicitConstructors,
|
||||
isAbstract,
|
||||
isObservableValidator);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool ValidateTargetType(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
[NotNullWhen(false)] out DiagnosticDescriptor? descriptor)
|
||||
protected override bool ValidateTargetType(INamedTypeSymbol typeSymbol, ObservableRecipientInfo info, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
INamedTypeSymbol observableRecipientSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient")!;
|
||||
INamedTypeSymbol observableObjectSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject")!;
|
||||
INamedTypeSymbol observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute")!;
|
||||
INamedTypeSymbol iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!;
|
||||
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
||||
|
||||
// Check if the type already inherits from ObservableRecipient
|
||||
if (classDeclarationSymbol.InheritsFrom(observableRecipientSymbol))
|
||||
if (typeSymbol.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient"))
|
||||
{
|
||||
descriptor = DuplicateObservableRecipientError;
|
||||
builder.Add(DuplicateObservableRecipientError, typeSymbol, typeSymbol);
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// In order to use [ObservableRecipient], the target type needs to inherit from ObservableObject,
|
||||
// or be annotated with [ObservableObject] or [INotifyPropertyChanged] (with additional helpers).
|
||||
if (!classDeclarationSymbol.InheritsFrom(observableObjectSymbol) &&
|
||||
!classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)) &&
|
||||
!classDeclarationSymbol.GetAttributes().Any(a =>
|
||||
SymbolEqualityComparer.Default.Equals(a.AttributeClass, iNotifyPropertyChangedSymbol) &&
|
||||
if (!typeSymbol.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObject") &&
|
||||
!typeSymbol.GetAttributes().Any(static a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") == true) &&
|
||||
!typeSymbol.GetAttributes().Any(static a =>
|
||||
a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute") == true &&
|
||||
!a.HasNamedArgument("IncludeAdditionalHelperMethods", false)))
|
||||
{
|
||||
descriptor = MissingBaseObservableObjectFunctionalityError;
|
||||
builder.Add(MissingBaseObservableObjectFunctionalityError, typeSymbol, typeSymbol);
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<MemberDeclarationSyntax> FilterDeclaredMembers(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
ClassDeclarationSyntax sourceDeclaration)
|
||||
protected override ImmutableArray<MemberDeclarationSyntax> FilterDeclaredMembers(ObservableRecipientInfo info, ImmutableArray<MemberDeclarationSyntax> memberDeclarations)
|
||||
{
|
||||
ImmutableArray<MemberDeclarationSyntax>.Builder builder = ImmutableArray.CreateBuilder<MemberDeclarationSyntax>();
|
||||
|
||||
// If the target type has no constructors, generate constructors as well
|
||||
if (classDeclarationSymbol.InstanceConstructors.Length == 1 &&
|
||||
classDeclarationSymbol.InstanceConstructors[0] is
|
||||
{
|
||||
Parameters: { IsEmpty: true },
|
||||
DeclaringSyntaxReferences: { IsEmpty: true },
|
||||
IsImplicitlyDeclared: true
|
||||
})
|
||||
if (!info.HasExplicitConstructors)
|
||||
{
|
||||
foreach (ConstructorDeclarationSyntax ctor in sourceDeclaration.Members.OfType<ConstructorDeclarationSyntax>())
|
||||
foreach (ConstructorDeclarationSyntax ctor in memberDeclarations.OfType<ConstructorDeclarationSyntax>())
|
||||
{
|
||||
string text = ctor.NormalizeWhitespace().ToFullString();
|
||||
string replaced = text.Replace("ObservableRecipient", classDeclarationSymbol.Name);
|
||||
string replaced = text.Replace("ObservableRecipient", info.TypeName);
|
||||
|
||||
// Adjust the visibility of the constructors based on whether the target type is abstract.
|
||||
// If that is not the case, the constructors have to be declared as public and not protected.
|
||||
if (!classDeclarationSymbol.IsAbstract)
|
||||
if (!info.IsAbstract)
|
||||
{
|
||||
replaced = replaced.Replace("protected", "public");
|
||||
}
|
||||
|
||||
yield return (ConstructorDeclarationSyntax)ParseMemberDeclaration(replaced)!;
|
||||
builder.Add((ConstructorDeclarationSyntax)ParseMemberDeclaration(replaced)!);
|
||||
}
|
||||
}
|
||||
|
||||
INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")!;
|
||||
|
||||
// Skip the SetProperty overloads if the target type inherits from ObservableValidator, to avoid conflicts
|
||||
if (classDeclarationSymbol.InheritsFrom(observableValidatorSymbol))
|
||||
if (info.IsObservableValidator)
|
||||
{
|
||||
foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax))
|
||||
foreach (MemberDeclarationSyntax member in memberDeclarations.Where(static member => member is not ConstructorDeclarationSyntax))
|
||||
{
|
||||
if (member is not MethodDeclarationSyntax { Identifier: { ValueText: "SetProperty" } })
|
||||
if (member is not MethodDeclarationSyntax { Identifier.ValueText: "SetProperty" })
|
||||
{
|
||||
yield return member;
|
||||
builder.Add(member);
|
||||
}
|
||||
}
|
||||
|
||||
yield break;
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
// If the target type has at least one custom constructor, only generate methods
|
||||
foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax))
|
||||
foreach (MemberDeclarationSyntax member in memberDeclarations.Where(static member => member is not ConstructorDeclarationSyntax))
|
||||
{
|
||||
yield return member;
|
||||
builder.Add(member);
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,278 @@
|
||||
// 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;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial class ObservableValidatorValidateAllPropertiesGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for all the logic for <see cref="ObservableValidatorValidateAllPropertiesGenerator"/>.
|
||||
/// </summary>
|
||||
private static class Execute
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether a given type inherits from <c>ObservableValidator</c>.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to check.</param>
|
||||
/// <returns>Whether <paramref name="typeSymbol"/> inherits from <c>ObservableValidator</c>.</returns>
|
||||
public static bool IsObservableValidator(INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
return typeSymbol.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ValidationInfo"/> instance from an input symbol.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to inspect.</param>
|
||||
/// <returns>The resulting <see cref="ValidationInfo"/> instance for <paramref name="typeSymbol"/>.</returns>
|
||||
public static ValidationInfo GetInfo(INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
ImmutableArray<string>.Builder propertyNames = ImmutableArray.CreateBuilder<string>();
|
||||
|
||||
foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
|
||||
{
|
||||
if (memberSymbol is { IsStatic: true } or not (IPropertySymbol { IsIndexer: false } or IFieldSymbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImmutableArray<AttributeData> attributes = memberSymbol.GetAttributes();
|
||||
|
||||
// Also include fields that are annotated with [ObservableProperty]. This is necessary because
|
||||
// all generators run in an undefined order and looking at the same original compilation, so the
|
||||
// current one wouldn't be able to see generated properties from other generators directly.
|
||||
if (memberSymbol is IFieldSymbol &&
|
||||
!attributes.Any(static a => a.AttributeClass?.HasFullyQualifiedName(
|
||||
"global::CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") == true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the current member if there are no validation attributes applied to it
|
||||
if (!attributes.Any(a => a.AttributeClass?.InheritsFrom(
|
||||
"global::System.ComponentModel.DataAnnotations.ValidationAttribute") == true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the target property name either directly or matching the generated one
|
||||
string propertyName = memberSymbol switch
|
||||
{
|
||||
IPropertySymbol propertySymbol => propertySymbol.Name,
|
||||
IFieldSymbol fieldSymbol => ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol),
|
||||
_ => throw new InvalidOperationException("Invalid symbol type")
|
||||
};
|
||||
|
||||
propertyNames.Add(propertyName);
|
||||
}
|
||||
|
||||
return new(
|
||||
typeSymbol.GetFullMetadataNameForFileName(),
|
||||
typeSymbol.GetFullyQualifiedName(),
|
||||
propertyNames.ToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="RecipientInfo"/> instance from the given info.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The type symbol for the target type being inspected.</param>
|
||||
/// <param name="interfaceSymbols">The input array of interface type symbols being handled.</param>
|
||||
/// <returns>A <see cref="RecipientInfo"/> instance for the current type being inspected.</returns>
|
||||
public static RecipientInfo GetInfo(INamedTypeSymbol typeSymbol, ImmutableArray<INamedTypeSymbol> interfaceSymbols)
|
||||
{
|
||||
ImmutableArray<string>.Builder names = ImmutableArray.CreateBuilder<string>(interfaceSymbols.Length);
|
||||
|
||||
foreach (INamedTypeSymbol interfaceSymbol in interfaceSymbols)
|
||||
{
|
||||
names.Add(interfaceSymbol.TypeArguments[0].GetFullyQualifiedName());
|
||||
}
|
||||
|
||||
return new(
|
||||
typeSymbol.GetFullMetadataNameForFileName(),
|
||||
typeSymbol.GetFullyQualifiedName(),
|
||||
names.MoveToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the head <see cref="CompilationUnitSyntax"/> instance.
|
||||
/// </summary>
|
||||
/// <returns>The head <see cref="CompilationUnitSyntax"/> instance with the type attributes.</returns>
|
||||
public static CompilationUnitSyntax GetSyntax()
|
||||
{
|
||||
// This code produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
// #pragma warning disable
|
||||
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
||||
// {
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
||||
// internal static partial class __ObservableValidatorExtensions
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(
|
||||
Token(SyntaxKind.InternalKeyword),
|
||||
Token(SyntaxKind.StaticKeyword),
|
||||
Token(SyntaxKind.PartialKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidateAllPropertiesGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableValidatorValidateAllPropertiesGenerator).Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This type is not intended to be used directly by user code")))))))))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CompilationUnitSyntax"/> instance for the input recipient.
|
||||
/// </summary>
|
||||
/// <param name="validationInfo">The input <see cref="ValidationInfo"/> instance to process.</param>
|
||||
/// <returns>The generated <see cref="CompilationUnitSyntax"/> instance for <paramref name="validationInfo"/>.</returns>
|
||||
public static CompilationUnitSyntax GetSyntax(ValidationInfo validationInfo)
|
||||
{
|
||||
// Create a static factory method creating a delegate that can be used to validate all properties in a given class.
|
||||
// This pattern is used so that the library doesn't have to use MakeGenericType(...) at runtime, nor use unsafe casts
|
||||
// over the created delegate to be able to cache it as an Action<object> instance. This pattern enables the same
|
||||
// functionality and with almost identical performance (not noticeable in this context anyway), but while preserving
|
||||
// full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed.
|
||||
// Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler
|
||||
// create a static class to cache a reusable delegate, because each generated method will only be called at most once,
|
||||
// as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the the produced code is minimal,
|
||||
// and that there will be no unnecessary static fields and objects being created and possibly never collected.
|
||||
// This code will produce a syntax tree as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
// #pragma warning disable
|
||||
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
||||
// {
|
||||
// partial class __ObservableValidatorExtensions
|
||||
// {
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This method is not intended to be called directly by user code")]
|
||||
// public static global::System.Action<object> CreateAllPropertiesValidator(<INSTANCE_TYPE> _)
|
||||
// {
|
||||
// static void ValidateAllProperties(object obj)
|
||||
// {
|
||||
// var instance = (<INSTANCE_TYPE>)obj;
|
||||
// <BODY>
|
||||
// }
|
||||
//
|
||||
// return ValidateAllProperties;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(Token(SyntaxKind.PartialKeyword)).AddMembers(
|
||||
MethodDeclaration(
|
||||
GenericName("global::System.Action").AddTypeArgumentListArguments(PredefinedType(Token(SyntaxKind.ObjectKeyword))),
|
||||
Identifier("CreateAllPropertiesValidator")).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
|
||||
Parameter(Identifier("_")).WithType(IdentifierName(validationInfo.TypeName)))
|
||||
.WithBody(Block(
|
||||
LocalFunctionStatement(
|
||||
PredefinedType(Token(SyntaxKind.VoidKeyword)),
|
||||
Identifier("ValidateAllProperties"))
|
||||
.AddModifiers(Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(
|
||||
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))))
|
||||
.WithBody(Block(
|
||||
LocalDeclarationStatement(
|
||||
VariableDeclaration(IdentifierName("var")) // Cannot use Token(SyntaxKind.VarKeyword) here (throws an ArgumentException)
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier("instance"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
CastExpression(
|
||||
IdentifierName(validationInfo.TypeName),
|
||||
IdentifierName("obj")))))))
|
||||
.AddStatements(EnumerateValidationStatements(validationInfo).ToArray())),
|
||||
ReturnStatement(IdentifierName("ValidateAllProperties")))))))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sequence of statements to validate declared properties.
|
||||
/// </summary>
|
||||
/// <param name="validationInfo">The input <see cref="ValidationInfo"/> instance to process.</param>
|
||||
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to validate declared properties.</returns>
|
||||
private static ImmutableArray<StatementSyntax> EnumerateValidationStatements(ValidationInfo validationInfo)
|
||||
{
|
||||
ImmutableArray<StatementSyntax>.Builder statements = ImmutableArray.CreateBuilder<StatementSyntax>(validationInfo.PropertyNames.Length);
|
||||
|
||||
// This loop produces a sequence of statements as follows:
|
||||
//
|
||||
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
|
||||
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
|
||||
// ...
|
||||
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_1>, nameof(instance.<PROPERTY_1>));
|
||||
foreach (string propertyName in validationInfo.PropertyNames)
|
||||
{
|
||||
statements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("__ObservableValidatorHelper"),
|
||||
IdentifierName("ValidateProperty")))
|
||||
.AddArgumentListArguments(
|
||||
Argument(IdentifierName("instance")),
|
||||
Argument(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("instance"),
|
||||
IdentifierName(propertyName))),
|
||||
Argument(
|
||||
InvocationExpression(IdentifierName("nameof"))
|
||||
.AddArgumentListArguments(Argument(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("instance"),
|
||||
IdentifierName(propertyName))))))));
|
||||
}
|
||||
|
||||
return statements.MoveToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// 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.Generic;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc cref="ObservableValidatorValidateAllPropertiesGenerator"/>
|
||||
public sealed partial class ObservableValidatorValidateAllPropertiesGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISyntaxContextReceiver"/> that selects candidate nodes to process.
|
||||
/// </summary>
|
||||
private sealed class SyntaxReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of info gathered during exploration.
|
||||
/// </summary>
|
||||
private readonly List<INamedTypeSymbol> gatheredInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of gathered info to process.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<INamedTypeSymbol> GatheredInfo => this.gatheredInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is ClassDeclarationSyntax classDeclaration &&
|
||||
context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol { IsGenericType: false } classSymbol &&
|
||||
context.SemanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator") is INamedTypeSymbol validatorSymbol &&
|
||||
classSymbol.InheritsFrom(validatorSymbol))
|
||||
{
|
||||
this.gatheredInfo.Add(classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,233 +2,63 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// A source generator for properties validation without relying on compiled LINQ expressions.
|
||||
/// A source generator for message registration without relying on compiled LINQ expressions.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed partial class ObservableValidatorValidateAllPropertiesGenerator : ISourceGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed partial class ObservableValidatorValidateAllPropertiesGenerator : IIncrementalGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
|
||||
}
|
||||
// Get all class declarations
|
||||
IncrementalValuesProvider<INamedTypeSymbol> typeSymbols =
|
||||
context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
static (node, _) => node is ClassDeclarationSyntax,
|
||||
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
// Get the syntax receiver with the candidate nodes
|
||||
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
|
||||
syntaxReceiver.GatheredInfo.Count == 0)
|
||||
// Get the types that inherit from ObservableValidator and gather their info
|
||||
IncrementalValuesProvider<ValidationInfo> validationInfo =
|
||||
typeSymbols
|
||||
.Where(Execute.IsObservableValidator)
|
||||
.Select(static (item, _) => Execute.GetInfo(item))
|
||||
.WithComparer(ValidationInfo.Comparer.Default);
|
||||
|
||||
// Check whether the header file is needed
|
||||
IncrementalValueProvider<bool> isHeaderFileNeeded =
|
||||
validationInfo
|
||||
.Collect()
|
||||
.Select(static (item, _) => item.Length > 0);
|
||||
|
||||
// Generate the header file with the attributes
|
||||
context.RegisterImplementationSourceOutput(isHeaderFileNeeded, static (context, item) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
CompilationUnitSyntax compilationUnit = Execute.GetSyntax();
|
||||
|
||||
// Validate the language version (this needs at least C# 8.0 due to static local functions being used).
|
||||
// If a lower C# version is set, just skip the execution silently. The fallback path will be used just fine.
|
||||
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp8 })
|
||||
context.AddSource(
|
||||
hintName: "__ObservableValidatorExtensions.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
|
||||
// Generate the class with all validation methods
|
||||
context.RegisterImplementationSourceOutput(validationInfo, static (context, item) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
CompilationUnitSyntax compilationUnit = Execute.GetSyntax(item);
|
||||
|
||||
// Get the symbol for the required attributes
|
||||
INamedTypeSymbol validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!;
|
||||
INamedTypeSymbol observablePropertySymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute")!;
|
||||
|
||||
// Prepare the attributes to add to the first class declaration
|
||||
AttributeListSyntax[] classAttributes = new[]
|
||||
{
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This type is not intended to be used directly by user code"))))))
|
||||
};
|
||||
|
||||
foreach (INamedTypeSymbol classSymbol in syntaxReceiver.GatheredInfo)
|
||||
{
|
||||
// Create a static factory method creating a delegate that can be used to validate all properties in a given class.
|
||||
// This pattern is used so that the library doesn't have to use MakeGenericType(...) at runtime, nor use unsafe casts
|
||||
// over the created delegate to be able to cache it as an Action<object> instance. This pattern enables the same
|
||||
// functionality and with almost identical performance (not noticeable in this context anyway), but while preserving
|
||||
// full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed.
|
||||
// Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler
|
||||
// create a static class to cache a reusable delegate, because each generated method will only be called at most once,
|
||||
// as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the the produced code is minimal,
|
||||
// and that there will be no unnecessary static fields and objects being created and possibly never collected.
|
||||
// This code takes a class symbol and produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
//
|
||||
// #pragma warning disable
|
||||
//
|
||||
// namespace CommunityToolkit.Mvvm.ComponentModel.__Internals
|
||||
// {
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
||||
// internal static partial class __ObservableValidatorExtensions
|
||||
// {
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This method is not intended to be called directly by user code")]
|
||||
// public static global::System.Action<object> CreateAllPropertiesValidator(<INSTANCE_TYPE> _)
|
||||
// {
|
||||
// static void ValidateAllProperties(object obj)
|
||||
// {
|
||||
// var instance = (<INSTANCE_TYPE>)obj;
|
||||
// <BODY>
|
||||
// }
|
||||
//
|
||||
// return ValidateAllProperties;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
string? source =
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__ObservableValidatorExtensions").AddModifiers(
|
||||
Token(SyntaxKind.InternalKeyword),
|
||||
Token(SyntaxKind.StaticKeyword),
|
||||
Token(SyntaxKind.PartialKeyword)).AddAttributeLists(classAttributes).AddMembers(
|
||||
MethodDeclaration(
|
||||
GenericName("global::System.Action").AddTypeArgumentListArguments(PredefinedType(Token(SyntaxKind.ObjectKeyword))),
|
||||
Identifier("CreateAllPropertiesValidator")).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
|
||||
Parameter(Identifier("_")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))))
|
||||
.WithBody(Block(
|
||||
LocalFunctionStatement(
|
||||
PredefinedType(Token(SyntaxKind.VoidKeyword)),
|
||||
Identifier("ValidateAllProperties"))
|
||||
.AddModifiers(Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(
|
||||
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))))
|
||||
.WithBody(Block(
|
||||
LocalDeclarationStatement(
|
||||
VariableDeclaration(IdentifierName("var")) // Cannot use Token(SyntaxKind.VarKeyword) here (throws an ArgumentException)
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier("instance"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
CastExpression(
|
||||
IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
IdentifierName("obj")))))))
|
||||
.AddStatements(EnumerateValidationStatements(classSymbol, validationSymbol, observablePropertySymbol).ToArray())),
|
||||
ReturnStatement(IdentifierName("ValidateAllProperties")))))))
|
||||
.NormalizeWhitespace()
|
||||
.ToFullString();
|
||||
|
||||
// Reset the attributes list (so the same class doesn't get duplicate attributes)
|
||||
classAttributes = Array.Empty<AttributeListSyntax>();
|
||||
|
||||
// Add the partial type
|
||||
context.AddSource($"{classSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sequence of statements to validate declared properties (including generated ones).
|
||||
/// </summary>
|
||||
/// <param name="classSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
|
||||
/// <param name="validationSymbol">The type symbol for the <c>ValidationAttribute</c> type.</param>
|
||||
/// <param name="observablePropertySymbol">The type symbol for the <c>ObservablePropertyAttribute</c> type.</param>
|
||||
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to validate declared properties.</returns>
|
||||
private static IEnumerable<StatementSyntax> EnumerateValidationStatements(INamedTypeSymbol classSymbol, INamedTypeSymbol validationSymbol, INamedTypeSymbol observablePropertySymbol)
|
||||
{
|
||||
foreach (ISymbol? memberSymbol in classSymbol.GetMembers())
|
||||
{
|
||||
if (memberSymbol is not (IPropertySymbol { IsIndexer: false } or IFieldSymbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImmutableArray<AttributeData> attributes = memberSymbol.GetAttributes();
|
||||
|
||||
// Also include fields that are annotated with [ObservableProperty]. This is necessary because
|
||||
// all generators run in an undefined order and looking at the same original compilation, so the
|
||||
// current one wouldn't be able to see generated properties from other generators directly.
|
||||
if (memberSymbol is IFieldSymbol &&
|
||||
!attributes.Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observablePropertySymbol)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the current member if there are no validation attributes applied to it
|
||||
if (!attributes.Any(a => a.AttributeClass?.InheritsFrom(validationSymbol) == true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the target property name either directly or matching the generated one
|
||||
string propertyName = memberSymbol switch
|
||||
{
|
||||
IPropertySymbol propertySymbol => propertySymbol.Name,
|
||||
IFieldSymbol fieldSymbol => ObservablePropertyGenerator.GetGeneratedPropertyName(fieldSymbol),
|
||||
_ => throw new InvalidOperationException("Invalid symbol type.")
|
||||
};
|
||||
|
||||
// This enumerator produces a sequence of statements as follows:
|
||||
//
|
||||
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
|
||||
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_0>, nameof(instance.<PROPERTY_0>));
|
||||
// ...
|
||||
// __ObservableValidatorHelper.ValidateProperty(instance, instance.<PROPERTY_1>, nameof(instance.<PROPERTY_1>));
|
||||
yield return
|
||||
ExpressionStatement(
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("__ObservableValidatorHelper"),
|
||||
IdentifierName("ValidateProperty")))
|
||||
.AddArgumentListArguments(
|
||||
Argument(IdentifierName("instance")),
|
||||
Argument(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("instance"),
|
||||
IdentifierName(propertyName))),
|
||||
Argument(
|
||||
InvocationExpression(IdentifierName("nameof"))
|
||||
.AddArgumentListArguments(Argument(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("instance"),
|
||||
IdentifierName(propertyName)))))));
|
||||
}
|
||||
context.AddSource(
|
||||
hintName: $"{item.FilenameHint}.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,98 @@
|
||||
// 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;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial class TransitiveMembersGenerator<TInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for all the logic for <see cref="TransitiveMembersGenerator{TInfo}"/>.
|
||||
/// </summary>
|
||||
internal static class Execute
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the source <see cref="ClassDeclarationSyntax"/> instance to get member declarations from.
|
||||
/// </summary>
|
||||
/// <param name="attributeType">The fully qualified name of the attribute type to look for.</param>
|
||||
/// <returns>The source <see cref="ClassDeclarationSyntax"/> instance to get member declarations from.</returns>
|
||||
public static ClassDeclarationSyntax LoadClassDeclaration(string attributeType)
|
||||
{
|
||||
string attributeTypeName = attributeType.Split('.').Last();
|
||||
string filename = $"CommunityToolkit.Mvvm.SourceGenerators.EmbeddedResources.{attributeTypeName.Replace("Attribute", string.Empty)}.cs";
|
||||
|
||||
using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename);
|
||||
using StreamReader reader = new(stream);
|
||||
|
||||
string observableObjectSource = reader.ReadToEnd();
|
||||
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(observableObjectSource);
|
||||
|
||||
return syntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the sequence of member declarations to generate.
|
||||
/// </summary>
|
||||
/// <param name="generatorType">The type of generator being used.</param>
|
||||
/// <param name="memberDeclarations">The input sequence of member declarations to generate.</param>
|
||||
/// <param name="sealedMemberDeclarations">The resulting sequence of member declarations for sealed types.</param>
|
||||
/// <param name="nonSealedMemberDeclarations">The resulting sequence of member declarations for non sealed types.</param>
|
||||
public static void ProcessMemberDeclarations(
|
||||
Type generatorType,
|
||||
ImmutableArray<MemberDeclarationSyntax> memberDeclarations,
|
||||
out ImmutableArray<MemberDeclarationSyntax> sealedMemberDeclarations,
|
||||
out ImmutableArray<MemberDeclarationSyntax> nonSealedMemberDeclarations)
|
||||
{
|
||||
ImmutableArray<MemberDeclarationSyntax> annotatedMemberDeclarations = memberDeclarations.Select(member =>
|
||||
{
|
||||
// [GeneratedCode] is always present
|
||||
member =
|
||||
member
|
||||
.WithoutLeadingTrivia()
|
||||
.AddAttributeLists(AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(generatorType.FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(generatorType.Assembly.GetName().Version.ToString())))))))
|
||||
.WithLeadingTrivia(member.GetLeadingTrivia());
|
||||
|
||||
// [DebuggerNonUserCode] is not supported over interfaces, events or fields
|
||||
if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.EventFieldDeclaration and not SyntaxKind.FieldDeclaration)
|
||||
{
|
||||
member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))));
|
||||
}
|
||||
|
||||
// [ExcludeFromCodeCoverage] is not supported on interfaces and fields
|
||||
if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.FieldDeclaration)
|
||||
{
|
||||
member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))));
|
||||
}
|
||||
|
||||
return member;
|
||||
}).ToImmutableArray();
|
||||
|
||||
// If the target class is sealed, make protected members private and remove the virtual modifier
|
||||
sealedMemberDeclarations = annotatedMemberDeclarations.Select(static member =>
|
||||
{
|
||||
return
|
||||
member
|
||||
.ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword)
|
||||
.RemoveModifier(SyntaxKind.VirtualKeyword);
|
||||
}).ToImmutableArray();
|
||||
|
||||
nonSealedMemberDeclarations = annotatedMemberDeclarations;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc cref="TransitiveMembersGenerator"/>
|
||||
public abstract partial class TransitiveMembersGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISyntaxContextReceiver"/> that selects candidate nodes to process.
|
||||
/// </summary>
|
||||
private sealed class SyntaxReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// The fully qualified name of the attribute type to look for.
|
||||
/// </summary>
|
||||
private readonly string attributeTypeFullName;
|
||||
|
||||
/// <summary>
|
||||
/// The list of info gathered during exploration.
|
||||
/// </summary>
|
||||
private readonly List<Item> gatheredInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyntaxReceiver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="attributeTypeFullName">The fully qualified name of the attribute type to look for.</param>
|
||||
public SyntaxReceiver(string attributeTypeFullName)
|
||||
{
|
||||
this.attributeTypeFullName = attributeTypeFullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of gathered info to process.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Item> GatheredInfo => this.gatheredInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is ClassDeclarationSyntax { AttributeLists: { Count: > 0 } } classDeclaration &&
|
||||
context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol &&
|
||||
context.SemanticModel.Compilation.GetTypeByMetadataName(this.attributeTypeFullName) is INamedTypeSymbol attributeSymbol &&
|
||||
classSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData &&
|
||||
attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference &&
|
||||
syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax)
|
||||
{
|
||||
this.gatheredInfo.Add(new Item(classDeclaration, classSymbol, attributeSyntax, attributeData));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A model for a group of item representing a discovered type to process.
|
||||
/// </summary>
|
||||
/// <param name="ClassDeclaration">The <see cref="ClassDeclarationSyntax"/> instance for the target class declaration.</param>
|
||||
/// <param name="ClassSymbol">The <see cref="INamedTypeSymbol"/> instance for <paramref name="ClassDeclaration"/>.</param>
|
||||
/// <param name="AttributeSyntax">The <see cref="AttributeSyntax"/> instance for the target attribute over <paramref name="ClassDeclaration"/>.</param>
|
||||
/// <param name="AttributeData">The <see cref="AttributeData"/> instance for <paramref name="AttributeSyntax"/>.</param>
|
||||
public sealed record Item(
|
||||
ClassDeclarationSyntax ClassDeclaration,
|
||||
INamedTypeSymbol ClassSymbol,
|
||||
AttributeSyntax AttributeSyntax,
|
||||
AttributeData AttributeData);
|
||||
}
|
||||
}
|
@ -2,21 +2,17 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
@ -24,242 +20,136 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
/// <summary>
|
||||
/// A source generator for a given attribute type.
|
||||
/// </summary>
|
||||
public abstract partial class TransitiveMembersGenerator : ISourceGenerator
|
||||
/// <typeparam name="TInfo">The type of info gathered for each target type to process.</typeparam>
|
||||
public abstract partial class TransitiveMembersGenerator<TInfo> : IIncrementalGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// The fully qualified name of the attribute type to look for.
|
||||
/// </summary>
|
||||
private readonly string attributeTypeFullName;
|
||||
private readonly string attributeType;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the attribute type to look for.
|
||||
/// An <see cref="IEqualityComparer{T}"/> instance to compare intermediate models.
|
||||
/// </summary>
|
||||
private readonly string attributeTypeName;
|
||||
/// <remarks>
|
||||
/// This is needed to cache extracted info on attributes used to annotate target types.
|
||||
/// </remarks>
|
||||
private readonly IEqualityComparer<TInfo> comparer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransitiveMembersGenerator"/> class.
|
||||
/// The preloaded <see cref="ClassDeclarationSyntax"/> instance with members to generate.
|
||||
/// </summary>
|
||||
/// <param name="attributeTypeFullName">The fully qualified name of the attribute type to look for.</param>
|
||||
protected TransitiveMembersGenerator(string attributeTypeFullName)
|
||||
private readonly ClassDeclarationSyntax classDeclaration;
|
||||
|
||||
/// <summary>
|
||||
/// The sequence of member declarations for sealed types.
|
||||
/// </summary>
|
||||
private ImmutableArray<MemberDeclarationSyntax> sealedMemberDeclarations;
|
||||
|
||||
/// <summary>
|
||||
/// The resulting sequence of member declarations for non sealed types.
|
||||
/// </summary>
|
||||
private ImmutableArray<MemberDeclarationSyntax> nonSealedMemberDeclarations;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransitiveMembersGenerator{TInfo}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="attributeType">The fully qualified name of the attribute type to look for.</param>
|
||||
/// <param name="comparer">An <see cref="IEqualityComparer{T}"/> instance to compare intermediate models.</param>
|
||||
private protected TransitiveMembersGenerator(string attributeType, IEqualityComparer<TInfo>? comparer = null)
|
||||
{
|
||||
this.attributeTypeFullName = attributeTypeFullName;
|
||||
this.attributeTypeName = attributeTypeFullName.Split('.').Last();
|
||||
}
|
||||
this.attributeType = attributeType;
|
||||
this.comparer = comparer ?? EqualityComparer<TInfo>.Default;
|
||||
this.classDeclaration = Execute.LoadClassDeclaration(attributeType);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when the generation failed for a given type.
|
||||
/// </summary>
|
||||
protected abstract DiagnosticDescriptor TargetTypeErrorDescriptor { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(this.attributeTypeFullName));
|
||||
Execute.ProcessMemberDeclarations(
|
||||
GetType(),
|
||||
this.classDeclaration.Members.ToImmutableArray(),
|
||||
out this.sealedMemberDeclarations,
|
||||
out this.nonSealedMemberDeclarations);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// Get the syntax receiver with the candidate nodes
|
||||
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
|
||||
syntaxReceiver.GatheredInfo.Count == 0)
|
||||
// Get all class declarations
|
||||
IncrementalValuesProvider<INamedTypeSymbol> typeSymbols =
|
||||
context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
|
||||
|
||||
// Filter the types with the target attribute
|
||||
IncrementalValuesProvider<(INamedTypeSymbol Symbol, TInfo Info)> typeSymbolsWithInfo =
|
||||
typeSymbols
|
||||
.Select((item, _) => (
|
||||
Symbol: item,
|
||||
Attribute: item.GetAttributes().FirstOrDefault(a => a.AttributeClass?.HasFullyQualifiedName(this.attributeType) == true)))
|
||||
.Where(static item => item.Attribute is not null)!
|
||||
.Select((item, _) => (item.Symbol, GetInfo(item.Symbol, item.Attribute!)));
|
||||
|
||||
// Filter by language version
|
||||
context.FilterWithLanguageVersion(ref typeSymbolsWithInfo, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
|
||||
|
||||
// Gather all generation info, and any diagnostics
|
||||
IncrementalValuesProvider<Result<(HierarchyInfo Hierarchy, bool IsSealed, TInfo Info)>> generationInfoWithErrors =
|
||||
typeSymbolsWithInfo.Select((item, _) =>
|
||||
{
|
||||
if (ValidateTargetType(item.Symbol, item.Info, out ImmutableArray<Diagnostic> diagnostics))
|
||||
{
|
||||
return new Result<(HierarchyInfo, bool, TInfo)>(
|
||||
(HierarchyInfo.From(item.Symbol), item.Symbol.IsSealed, item.Info),
|
||||
ImmutableArray<Diagnostic>.Empty);
|
||||
}
|
||||
|
||||
return new Result<(HierarchyInfo, bool, TInfo)>(default, diagnostics);
|
||||
});
|
||||
|
||||
// Emit the diagnostic, if needed
|
||||
context.ReportDiagnostics(generationInfoWithErrors.Select(static (item, _) => item.Errors));
|
||||
|
||||
// Get the filtered sequence to enable caching
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, bool IsSealed, TInfo Info)> generationInfo =
|
||||
generationInfoWithErrors
|
||||
.Where(static item => item.Errors.IsEmpty)
|
||||
.Select(static (item, _) => item.Value)
|
||||
.WithComparers(HierarchyInfo.Comparer.Default, EqualityComparer<bool>.Default, this.comparer);
|
||||
|
||||
// Generate the required members
|
||||
context.RegisterSourceOutput(generationInfo, (context, item) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
ImmutableArray<MemberDeclarationSyntax> sourceMemberDeclarations = item.IsSealed ? this.sealedMemberDeclarations : this.nonSealedMemberDeclarations;
|
||||
ImmutableArray<MemberDeclarationSyntax> filteredMemberDeclarations = FilterDeclaredMembers(item.Info, sourceMemberDeclarations);
|
||||
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(filteredMemberDeclarations, this.classDeclaration.BaseList);
|
||||
|
||||
// Validate the language version
|
||||
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
|
||||
}
|
||||
|
||||
// Load the syntax tree with the members to generate
|
||||
SyntaxTree sourceSyntaxTree = LoadSourceSyntaxTree();
|
||||
|
||||
foreach (SyntaxReceiver.Item item in syntaxReceiver.GatheredInfo)
|
||||
{
|
||||
if (!ValidateTargetType(context, item.AttributeData, item.ClassDeclaration, item.ClassSymbol, out DiagnosticDescriptor? descriptor))
|
||||
{
|
||||
context.ReportDiagnostic(descriptor, item.AttributeSyntax, item.ClassSymbol);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
OnExecute(context, item.AttributeData, item.ClassDeclaration, item.ClassSymbol, sourceSyntaxTree);
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.ReportDiagnostic(TargetTypeErrorDescriptor, item.AttributeSyntax, item.ClassSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the source syntax tree for the current generator.
|
||||
/// </summary>
|
||||
/// <returns>The syntax tree with the elements to emit in the generated code.</returns>
|
||||
private SyntaxTree LoadSourceSyntaxTree()
|
||||
{
|
||||
string filename = $"CommunityToolkit.Mvvm.SourceGenerators.EmbeddedResources.{this.attributeTypeName.Replace("Attribute", string.Empty)}.cs";
|
||||
|
||||
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename);
|
||||
StreamReader reader = new(stream);
|
||||
|
||||
string observableObjectSource = reader.ReadToEnd();
|
||||
|
||||
return CSharpSyntaxTree.ParseText(observableObjectSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a given target type.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> for the current attribute being processed.</param>
|
||||
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
|
||||
/// <param name="sourceSyntaxTree">The <see cref="Microsoft.CodeAnalysis.SyntaxTree"/> for the target parsed source.</param>
|
||||
private void OnExecute(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
SyntaxTree sourceSyntaxTree)
|
||||
{
|
||||
ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>().First();
|
||||
|
||||
// Create the class declaration for the user type. This will produce a tree as follows:
|
||||
//
|
||||
// <MODIFIERS> <CLASS_NAME> : <BASE_TYPES>
|
||||
// {
|
||||
// <MEMBERS>
|
||||
// }
|
||||
ClassDeclarationSyntax? classDeclarationSyntax =
|
||||
ClassDeclaration(classDeclaration.Identifier.Text)
|
||||
.WithModifiers(classDeclaration.Modifiers)
|
||||
.WithBaseList(sourceDeclaration.BaseList)
|
||||
.AddMembers(OnLoadDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray());
|
||||
|
||||
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
|
||||
|
||||
// Add all parent types in ascending order, if any
|
||||
foreach (TypeDeclarationSyntax? parentType in classDeclaration.Ancestors().OfType<TypeDeclarationSyntax>())
|
||||
{
|
||||
typeDeclarationSyntax = parentType
|
||||
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax))
|
||||
.WithConstraintClauses(List<TypeParameterConstraintClauseSyntax>())
|
||||
.WithBaseList(null)
|
||||
.WithAttributeLists(List<AttributeListSyntax>())
|
||||
.WithoutTrivia();
|
||||
}
|
||||
|
||||
// Create the compilation unit with the namespace and target member.
|
||||
// From this, we can finally generate the source code to output.
|
||||
string? namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
|
||||
|
||||
// Create the final compilation unit to generate (with the full type declaration)
|
||||
string? source =
|
||||
CompilationUnit()
|
||||
.AddMembers(NamespaceDeclaration(IdentifierName(namespaceName))
|
||||
.WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
|
||||
.AddMembers(typeDeclarationSyntax))
|
||||
.NormalizeWhitespace()
|
||||
.ToFullString();
|
||||
|
||||
// Add the partial type
|
||||
context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the <see cref="MemberDeclarationSyntax"/> nodes to generate from the input parsed tree.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> for the current attribute being processed.</param>
|
||||
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
|
||||
/// <param name="sourceDeclaration">The parsed <see cref="ClassDeclarationSyntax"/> instance with the source nodes.</param>
|
||||
/// <returns>A sequence of <see cref="MemberDeclarationSyntax"/> nodes to emit in the generated file.</returns>
|
||||
private IEnumerable<MemberDeclarationSyntax> OnLoadDeclaredMembers(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
ClassDeclarationSyntax sourceDeclaration)
|
||||
{
|
||||
IEnumerable<MemberDeclarationSyntax> generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration);
|
||||
|
||||
// Add the attributes on each member
|
||||
return generatedMembers.Select(member =>
|
||||
{
|
||||
// [GeneratedCode] is always present
|
||||
member = member
|
||||
.WithoutLeadingTrivia()
|
||||
.AddAttributeLists(AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))))
|
||||
.WithLeadingTrivia(member.GetLeadingTrivia());
|
||||
|
||||
// [DebuggerNonUserCode] is not supported over interfaces, events or fields
|
||||
if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.EventFieldDeclaration and not SyntaxKind.FieldDeclaration)
|
||||
{
|
||||
member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))));
|
||||
}
|
||||
|
||||
// [ExcludeFromCodeCoverage] is not supported on interfaces and fields
|
||||
if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.FieldDeclaration)
|
||||
{
|
||||
member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))));
|
||||
}
|
||||
|
||||
// If the target class is sealed, make protected members private and remove the virtual modifier
|
||||
if (classDeclarationSymbol.IsSealed)
|
||||
{
|
||||
return member
|
||||
.ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword)
|
||||
.RemoveModifier(SyntaxKind.VirtualKeyword);
|
||||
}
|
||||
|
||||
return member;
|
||||
context.AddSource(
|
||||
hintName: $"{item.Hierarchy.FilenameHint}.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an info model from a retrieved <see cref="AttributeData"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The <see cref="INamedTypeSymbol"/> instance for the target type.</param>
|
||||
/// <param name="attributeData">The input <see cref="AttributeData"/> to get info from.</param>
|
||||
/// <returns>A <typeparamref name="TInfo"/> instance with data extracted from <paramref name="attributeData"/>.</returns>
|
||||
protected abstract TInfo GetInfo(INamedTypeSymbol typeSymbol, AttributeData attributeData);
|
||||
|
||||
/// <summary>
|
||||
/// Validates a target type being processed.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> for the current attribute being processed.</param>
|
||||
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
|
||||
/// <param name="descriptor">The resulting <see cref="DiagnosticDescriptor"/> to emit in case the target type isn't valid.</param>
|
||||
/// <param name="typeSymbol">The <see cref="INamedTypeSymbol"/> instance for the target type.</param>
|
||||
/// <param name="info">The <typeparamref name="TInfo"/> instance with the current processing info.</param>
|
||||
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
|
||||
/// <returns>Whether or not the target type is valid and can be processed normally.</returns>
|
||||
protected abstract bool ValidateTargetType(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
[NotNullWhen(false)] out DiagnosticDescriptor? descriptor);
|
||||
protected abstract bool ValidateTargetType(INamedTypeSymbol typeSymbol, TInfo info, out ImmutableArray<Diagnostic> diagnostics);
|
||||
|
||||
/// <summary>
|
||||
/// Filters the <see cref="MemberDeclarationSyntax"/> nodes to generate from the input parsed tree.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> for the current attribute being processed.</param>
|
||||
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
|
||||
/// <param name="sourceDeclaration">The parsed <see cref="ClassDeclarationSyntax"/> instance with the source nodes.</param>
|
||||
/// <param name="info">The <typeparamref name="TInfo"/> instance with the current processing info.</param>
|
||||
/// <param name="memberDeclarations">The input sequence of <see cref="MemberDeclarationSyntax"/> instances to generate.</param>
|
||||
/// <returns>A sequence of <see cref="MemberDeclarationSyntax"/> nodes to emit in the generated file.</returns>
|
||||
protected virtual IEnumerable<MemberDeclarationSyntax> FilterDeclaredMembers(
|
||||
GeneratorExecutionContext context,
|
||||
AttributeData attributeData,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
ClassDeclarationSyntax sourceDeclaration)
|
||||
{
|
||||
return sourceDeclaration.Members;
|
||||
}
|
||||
protected abstract ImmutableArray<MemberDeclarationSyntax> FilterDeclaredMembers(TInfo info, ImmutableArray<MemberDeclarationSyntax> memberDeclarations);
|
||||
}
|
||||
|
@ -13,54 +13,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
/// </summary>
|
||||
internal static class DiagnosticDescriptors
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <see cref="INotifyPropertyChangedGenerator"/> failed to run on a given type.
|
||||
/// <para>
|
||||
/// Format: <c>"The generator INotifyPropertyChangedGenerator failed to execute on type {0}"</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor INotifyPropertyChangedGeneratorError = new(
|
||||
id: "MVVMTK0001",
|
||||
title: $"Internal error for {nameof(INotifyPropertyChangedGenerator)}",
|
||||
messageFormat: $"The generator {nameof(INotifyPropertyChangedGenerator)} failed to execute on type {{0}}",
|
||||
category: typeof(INotifyPropertyChangedGenerator).FullName,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: $"The {nameof(INotifyPropertyChangedGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <see cref="ObservableObjectGenerator"/> failed to run on a given type.
|
||||
/// <para>
|
||||
/// Format: <c>"The generator ObservableObjectGenerator failed to execute on type {0}"</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor ObservableObjectGeneratorError = new(
|
||||
id: "MVVMTK0002",
|
||||
title: $"Internal error for {nameof(ObservableObjectGenerator)}",
|
||||
messageFormat: $"The generator {nameof(ObservableObjectGenerator)} failed to execute on type {{0}}",
|
||||
category: typeof(ObservableObjectGenerator).FullName,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: $"The {nameof(ObservableObjectGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <see cref="ObservableRecipientGenerator"/> failed to run on a given type.
|
||||
/// <para>
|
||||
/// Format: <c>"The generator ObservableRecipientGenerator failed to execute on type {0}"</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor ObservableRecipientGeneratorError = new(
|
||||
id: "MVVMTK0003",
|
||||
title: $"Internal error for {nameof(ObservableRecipientGenerator)}",
|
||||
messageFormat: $"The generator {nameof(ObservableRecipientGenerator)} failed to execute on type {{0}}",
|
||||
category: typeof(ObservableRecipientGenerator).FullName,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: $"The {nameof(ObservableRecipientGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a duplicate declaration of <see cref="INotifyPropertyChanged"/> would happen.
|
||||
/// <para>
|
||||
@ -68,7 +20,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError = new(
|
||||
id: "MVVMTK0004",
|
||||
id: "MVVMTK0001",
|
||||
title: $"Duplicate {nameof(INotifyPropertyChanged)} definition",
|
||||
messageFormat: $"Cannot apply [INotifyPropertyChanged] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface",
|
||||
category: typeof(INotifyPropertyChangedGenerator).FullName,
|
||||
@ -84,7 +36,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError = new(
|
||||
id: "MVVMTK0005",
|
||||
id: "MVVMTK0002",
|
||||
title: $"Duplicate {nameof(INotifyPropertyChanged)} definition",
|
||||
messageFormat: $"Cannot apply [ObservableObject] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface",
|
||||
category: typeof(ObservableObjectGenerator).FullName,
|
||||
@ -100,7 +52,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError = new(
|
||||
id: "MVVMTK0006",
|
||||
id: "MVVMTK0003",
|
||||
title: $"Duplicate {nameof(INotifyPropertyChanging)} definition",
|
||||
messageFormat: $"Cannot apply [ObservableObject] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanging)} interface",
|
||||
category: typeof(ObservableObjectGenerator).FullName,
|
||||
@ -116,7 +68,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor DuplicateObservableRecipientError = new(
|
||||
id: "MVVMTK0007",
|
||||
id: "MVVMTK0004",
|
||||
title: "Duplicate ObservableRecipient definition",
|
||||
messageFormat: $"Cannot apply [ObservableRecipient] to type {{0}}, as it already inherits from the ObservableRecipient class",
|
||||
category: typeof(ObservableRecipientGenerator).FullName,
|
||||
@ -132,7 +84,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor MissingBaseObservableObjectFunctionalityError = new(
|
||||
id: "MVVMTK0008",
|
||||
id: "MVVMTK0005",
|
||||
title: "Missing base ObservableObject functionality",
|
||||
messageFormat: $"Cannot apply [ObservableRecipient] to type {{0}}, as it lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [ObservableObject] or [INotifyPropertyChanged])",
|
||||
category: typeof(ObservableRecipientGenerator).FullName,
|
||||
@ -148,7 +100,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceError = new(
|
||||
id: "MVVMTK0009",
|
||||
id: "MVVMTK0006",
|
||||
title: "Missing ObservableValidator inheritance",
|
||||
messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator",
|
||||
category: typeof(ObservablePropertyGenerator).FullName,
|
||||
@ -157,38 +109,6 @@ internal static class DiagnosticDescriptors
|
||||
description: $"Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <see cref="ObservablePropertyGenerator"/> failed to run on a given type.
|
||||
/// <para>
|
||||
/// Format: <c>"The generator ObservablePropertyGenerator failed to execute on type {0}"</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor ObservablePropertyGeneratorError = new(
|
||||
id: "MVVMTK0010",
|
||||
title: $"Internal error for {nameof(ObservablePropertyGenerator)}",
|
||||
messageFormat: $"The generator {nameof(ObservablePropertyGenerator)} failed to execute on type {{0}}",
|
||||
category: typeof(ObservableObjectGenerator).FullName,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: $"The {nameof(ObservablePropertyGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when <see cref="ICommandGenerator"/> failed to run on a given type.
|
||||
/// <para>
|
||||
/// Format: <c>"The generator ICommandGenerator failed to execute on type {0}"</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor ICommandGeneratorError = new(
|
||||
id: "MVVMTK0011",
|
||||
title: $"Internal error for {nameof(ICommandGenerator)}",
|
||||
messageFormat: $"The generator {nameof(ICommandGenerator)} failed to execute on type {{0}}",
|
||||
category: typeof(ICommandGenerator).FullName,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: $"The {nameof(ICommandGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when an annotated method to generate a command for has an invalid signature.
|
||||
/// <para>
|
||||
@ -196,7 +116,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor InvalidICommandMethodSignatureError = new(
|
||||
id: "MVVMTK0012",
|
||||
id: "MVVMTK0007",
|
||||
title: "Invalid ICommand method signature",
|
||||
messageFormat: "The method {0}.{1} cannot be used to generate a command property, as its signature isn't compatible with any of the existing relay command types",
|
||||
category: typeof(ICommandGenerator).FullName,
|
||||
@ -209,13 +129,13 @@ internal static class DiagnosticDescriptors
|
||||
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when an unsupported C# language version is being used.
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor UnsupportedCSharpLanguageVersionError = new(
|
||||
id: "MVVMTK0013",
|
||||
id: "MVVMTK0008",
|
||||
title: "Unsupported C# language version",
|
||||
messageFormat: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 9.0",
|
||||
messageFormat: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 8.0",
|
||||
category: typeof(CSharpParseOptions).FullName,
|
||||
defaultSeverity: DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true,
|
||||
description: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 9.0. Make sure to add <LangVersion>9.0</LangVersion> (or above) to your .csproj file.",
|
||||
description: "The source generator features from the MVVM Toolkit require consuming projects to set the C# language version to at least C# 8.0. Make sure to add <LangVersion>8.0</LangVersion> (or above) to your .csproj file.",
|
||||
helpLinkUri: "https://aka.ms/mvvmtoolkit");
|
||||
|
||||
/// <summary>
|
||||
@ -225,7 +145,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor InvalidCanExecuteMemberName = new(
|
||||
id: "MVVMTK0014",
|
||||
id: "MVVMTK0009",
|
||||
title: "Invalid ICommand.CanExecute member name",
|
||||
messageFormat: "The CanExecute name must refer to a valid member, but \"{0}\" has no matches in type {1}",
|
||||
category: typeof(ICommandGenerator).FullName,
|
||||
@ -241,7 +161,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor MultipleCanExecuteMemberNameMatches = new(
|
||||
id: "MVVMTK0015",
|
||||
id: "MVVMTK0010",
|
||||
title: "Multiple ICommand.CanExecute member name matches",
|
||||
messageFormat: "The CanExecute name must refer to a single member, but \"{0}\" has multiple matches in type {1}",
|
||||
category: typeof(ICommandGenerator).FullName,
|
||||
@ -257,7 +177,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor InvalidCanExecuteMember = new(
|
||||
id: "MVVMTK0016",
|
||||
id: "MVVMTK0011",
|
||||
title: "No valid ICommand.CanExecute member match",
|
||||
messageFormat: "The CanExecute name must refer to a compatible member, but no valid members were found for \"{0}\" in type {1}",
|
||||
category: typeof(ICommandGenerator).FullName,
|
||||
@ -273,7 +193,7 @@ internal static class DiagnosticDescriptors
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor InvalidConcurrentExecutionsParameterError = new(
|
||||
id: "MVVMTK0017",
|
||||
id: "MVVMTK0012",
|
||||
title: "Invalid concurrency control setting usage",
|
||||
messageFormat: "The method {0}.{1} cannot be annotated with the [ICommand] attribute specifying a concurrency control setting, as it maps to a non-asynchronous command type",
|
||||
category: typeof(ICommandGenerator).FullName,
|
||||
|
@ -2,6 +2,10 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
@ -13,34 +17,34 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
internal static class DiagnosticExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new diagnostics to the current compilation.
|
||||
/// Adds a new diagnostics to the target builder.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="GeneratorEditContext"/> instance currently in use.</param>
|
||||
/// <param name="diagnostics">The collection of produced <see cref="Diagnostic"/> instances.</param>
|
||||
/// <param name="descriptor">The input <see cref="DiagnosticDescriptor"/> for the diagnostics to create.</param>
|
||||
/// <param name="symbol">The source <see cref="ISymbol"/> to attach the diagnostics to.</param>
|
||||
/// <param name="args">The optional arguments for the formatted message to include.</param>
|
||||
public static void ReportDiagnostic(
|
||||
this GeneratorExecutionContext context,
|
||||
public static void Add(
|
||||
this ImmutableArray<Diagnostic>.Builder diagnostics,
|
||||
DiagnosticDescriptor descriptor,
|
||||
ISymbol symbol,
|
||||
params object[] args)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(descriptor, symbol.Locations.FirstOrDefault(), args));
|
||||
diagnostics.Add(Diagnostic.Create(descriptor, symbol.Locations.FirstOrDefault(), args));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new diagnostics to the current compilation.
|
||||
/// Registers an output node into an <see cref="IncrementalGeneratorInitializationContext"/> to output diagnostics.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="GeneratorEditContext"/> instance currently in use.</param>
|
||||
/// <param name="descriptor">The input <see cref="DiagnosticDescriptor"/> for the diagnostics to create.</param>
|
||||
/// <param name="node">The source <see cref="SyntaxNode"/> to attach the diagnostics to.</param>
|
||||
/// <param name="args">The optional arguments for the formatted message to include.</param>
|
||||
public static void ReportDiagnostic(
|
||||
this GeneratorExecutionContext context,
|
||||
DiagnosticDescriptor descriptor,
|
||||
SyntaxNode node,
|
||||
params object[] args)
|
||||
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> instance.</param>
|
||||
/// <param name="diagnostics">The input <see cref="IncrementalValuesProvider{TValues}"/> sequence of diagnostics.</param>
|
||||
public static void ReportDiagnostics(this IncrementalGeneratorInitializationContext context, IncrementalValuesProvider<ImmutableArray<Diagnostic>> diagnostics)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(descriptor, node.GetLocation(), args));
|
||||
context.RegisterSourceOutput(diagnostics, static (context, diagnostics) =>
|
||||
{
|
||||
foreach (Diagnostic diagnostic in diagnostics)
|
||||
{
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ protected bool SetProperty<TModel, T>(T oldValue, T newValue, global::System.Col
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier? taskNotifier, global::System.Threading.Tasks.Task? newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, static _ => { }, propertyName);
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -330,7 +330,7 @@ protected bool SetPropertyAndNotifyOnCompletion([global::System.Diagnostics.Code
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion<T>([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier<T>? taskNotifier, global::System.Threading.Tasks.Task<T>? newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, static _ => { }, propertyName);
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, null, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -360,10 +360,10 @@ protected bool SetPropertyAndNotifyOnCompletion<T>([global::System.Diagnostics.C
|
||||
/// <typeparam name="TTask">The type of <see cref="global::System.Threading.Tasks.Task"/> to set and monitor.</typeparam>
|
||||
/// <param name="taskNotifier">The field notifier.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="callback">(optional) A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, global::System.Action<TTask?> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, global::System.Action<TTask?>? callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
where TTask : global::System.Threading.Tasks.Task
|
||||
{
|
||||
if (ReferenceEquals(taskNotifier.Task, newValue))
|
||||
@ -379,7 +379,10 @@ private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNo
|
||||
|
||||
if (isAlreadyCompletedOrNull)
|
||||
{
|
||||
callback(newValue);
|
||||
if (callback != null)
|
||||
{
|
||||
callback(newValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -399,7 +402,10 @@ async void MonitorTask()
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
callback(newValue);
|
||||
if (callback != null)
|
||||
{
|
||||
callback(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
MonitorTask();
|
||||
|
@ -313,7 +313,7 @@ protected bool SetProperty<TModel, T>(T oldValue, T newValue, global::System.Col
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier? taskNotifier, global::System.Threading.Tasks.Task? newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, static _ => { }, propertyName);
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -374,7 +374,7 @@ protected bool SetPropertyAndNotifyOnCompletion([global::System.Diagnostics.Code
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion<T>([global::System.Diagnostics.CodeAnalysis.NotNull] ref TaskNotifier<T>? taskNotifier, global::System.Threading.Tasks.Task<T>? newValue, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, static _ => { }, propertyName);
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, null, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -406,10 +406,10 @@ protected bool SetPropertyAndNotifyOnCompletion<T>([global::System.Diagnostics.C
|
||||
/// <typeparam name="TTask">The type of <see cref="global::System.Threading.Tasks.Task"/> to set and monitor.</typeparam>
|
||||
/// <param name="taskNotifier">The field notifier.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="callback">(optional) A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, global::System.Action<TTask?> callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, global::System.Action<TTask?>? callback, [global::System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
|
||||
where TTask : global::System.Threading.Tasks.Task
|
||||
{
|
||||
if (ReferenceEquals(taskNotifier.Task, newValue))
|
||||
@ -427,7 +427,10 @@ private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNo
|
||||
|
||||
if (isAlreadyCompletedOrNull)
|
||||
{
|
||||
callback(newValue);
|
||||
if ((object?)callback != null)
|
||||
{
|
||||
callback(newValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -447,7 +450,10 @@ async void MonitorTask()
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
callback(newValue);
|
||||
if ((object?)callback != null)
|
||||
{
|
||||
callback(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
MonitorTask();
|
||||
|
@ -103,7 +103,7 @@ protected virtual void OnDeactivated()
|
||||
/// </remarks>
|
||||
protected virtual void Broadcast<T>(T oldValue, T newValue, string? propertyName)
|
||||
{
|
||||
global::CommunityToolkit.Mvvm.Messaging.Messages.PropertyChangedMessage<T> message = new(this, propertyName, oldValue, newValue);
|
||||
var message = new global::CommunityToolkit.Mvvm.Messaging.Messages.PropertyChangedMessage<T>(this, propertyName, oldValue, newValue);
|
||||
|
||||
_ = global::CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.Send(Messenger, message);
|
||||
}
|
||||
@ -129,8 +129,6 @@ protected bool SetProperty<T>([global::System.Diagnostics.CodeAnalysis.NotNullIf
|
||||
{
|
||||
T oldValue = field;
|
||||
|
||||
// We duplicate the code as in the base class here to leverage
|
||||
// the intrinsics support for EqualityComparer<T>.Default.Equals.
|
||||
bool propertyChanged = SetProperty(ref field, newValue, propertyName);
|
||||
|
||||
if (propertyChanged && broadcast)
|
||||
|
@ -2,13 +2,12 @@
|
||||
// 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;
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics.Contracts;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
@ -40,6 +39,24 @@ properties.Value.Value is T argumentValue &&
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a given named argument value from an <see cref="AttributeData"/> instance, or a fallback value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of argument to check.</typeparam>
|
||||
/// <param name="attributeData">The target <see cref="AttributeData"/> instance to check.</param>
|
||||
/// <param name="name">The name of the argument to check.</param>
|
||||
/// <param name="fallback">The fallback value to use if the named argument is not present.</param>
|
||||
/// <returns>The argument named <paramref name="name"/>, or a fallback value.</returns>
|
||||
public static T? GetNamedArgument<T>(this AttributeData attributeData, string name, T? fallback = default)
|
||||
{
|
||||
if (attributeData.TryGetNamedArgument(name, out T? value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a given named argument value from an <see cref="AttributeData"/> instance, if present.
|
||||
/// </summary>
|
||||
@ -99,76 +116,4 @@ static IEnumerable<T> Enumerate(IEnumerable<TypedConstant> constants)
|
||||
|
||||
return Enumerate(attributeData.ConstructorArguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AttributeSyntax"/> node that is equivalent to the input <see cref="AttributeData"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="attributeData">The input <see cref="AttributeData"/> instance to process.</param>
|
||||
/// <returns>An <see cref="AttributeSyntax"/> replicating the data in <paramref name="attributeData"/>.</returns>
|
||||
public static AttributeSyntax AsAttributeSyntax(this AttributeData attributeData)
|
||||
{
|
||||
IdentifierNameSyntax attributeType = IdentifierName(attributeData.AttributeClass!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
AttributeArgumentSyntax[] arguments =
|
||||
attributeData.ConstructorArguments
|
||||
.Select(static arg => AttributeArgument(ToExpression(arg))).Concat(
|
||||
attributeData.NamedArguments
|
||||
.Select(static arg =>
|
||||
AttributeArgument(ToExpression(arg.Value))
|
||||
.WithNameEquals(NameEquals(IdentifierName(arg.Key))))).ToArray();
|
||||
|
||||
return Attribute(attributeType, AttributeArgumentList(SeparatedList(SeparatedList(arguments))));
|
||||
|
||||
static ExpressionSyntax ToExpression(TypedConstant arg)
|
||||
{
|
||||
if (arg.IsNull)
|
||||
{
|
||||
return LiteralExpression(SyntaxKind.NullLiteralExpression);
|
||||
}
|
||||
|
||||
if (arg.Kind == TypedConstantKind.Array)
|
||||
{
|
||||
string elementType = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
return
|
||||
ArrayCreationExpression(
|
||||
ArrayType(IdentifierName(elementType))
|
||||
.AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList<ExpressionSyntax>(OmittedArraySizeExpression()))))
|
||||
.WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression)
|
||||
.AddExpressions(arg.Values.Select(ToExpression).ToArray()));
|
||||
}
|
||||
|
||||
switch ((arg.Kind, arg.Value))
|
||||
{
|
||||
case (TypedConstantKind.Primitive, string text):
|
||||
return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(text));
|
||||
case (TypedConstantKind.Primitive, bool flag) when flag:
|
||||
return LiteralExpression(SyntaxKind.TrueLiteralExpression);
|
||||
case (TypedConstantKind.Primitive, bool):
|
||||
return LiteralExpression(SyntaxKind.FalseLiteralExpression);
|
||||
case (TypedConstantKind.Primitive, object value):
|
||||
return LiteralExpression(SyntaxKind.NumericLiteralExpression, value switch
|
||||
{
|
||||
byte b => Literal(b),
|
||||
char c => Literal(c),
|
||||
double d => Literal(d),
|
||||
float f => Literal(f),
|
||||
int i => Literal(i),
|
||||
long l => Literal(l),
|
||||
sbyte sb => Literal(sb),
|
||||
short sh => Literal(sh),
|
||||
uint ui => Literal(ui),
|
||||
ulong ul => Literal(ul),
|
||||
ushort ush => Literal(ush),
|
||||
_ => throw new ArgumentException()
|
||||
});
|
||||
case (TypedConstantKind.Type, ITypeSymbol type):
|
||||
return TypeOfExpression(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
|
||||
case (TypedConstantKind.Enum, object value):
|
||||
return CastExpression(
|
||||
IdentifierName(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
LiteralExpression(SyntaxKind.NumericLiteralExpression, ParseToken(value.ToString())));
|
||||
default: throw new ArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
// 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.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="HashCode"/>.
|
||||
/// </summary>
|
||||
internal static class HashCodeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all items from a given <see cref="ImmutableArray{T}"/> instance to an hashcode.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items to hash.</typeparam>
|
||||
/// <param name="hashCode">The target <see cref="HashCode"/> instance.</param>
|
||||
/// <param name="items">The input items to hash.</param>
|
||||
public static void AddRange<T>(this ref HashCode hashCode, ImmutableArray<T> items)
|
||||
{
|
||||
foreach (T item in items)
|
||||
{
|
||||
hashCode.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all items from a given <see cref="ImmutableArray{T}"/> instance to an hashcode.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items to hash.</typeparam>
|
||||
/// <param name="hashCode">The target <see cref="HashCode"/> instance.</param>
|
||||
/// <param name="comparer">A comparer to get hashcodes for <typeparamref name="T"/> items.</param>
|
||||
/// <param name="items">The input items to hash.</param>
|
||||
public static void AddRange<T>(this ref HashCode hashCode, ImmutableArray<T> items, IEqualityComparer<T> comparer)
|
||||
{
|
||||
foreach (T item in items)
|
||||
{
|
||||
hashCode.Add(item, comparer);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
// 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.
|
||||
|
||||
// This file is ported from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IEqualityComparer{T}"/>.
|
||||
/// </summary>
|
||||
internal static class IEqualityComparerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an <see cref="IEqualityComparer{T}"/> for an <see cref="ImmutableArray{T}"/> sequence.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items to compare.</typeparam>
|
||||
/// <param name="comparer">The compare for individual <typeparamref name="T"/> items.</param>
|
||||
public static IEqualityComparer<ImmutableArray<T>> ForImmutableArray<T>(this IEqualityComparer<T> comparer)
|
||||
{
|
||||
return new Comparer<T>(comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for an <see cref="ImmutableArray{T}"/> value.
|
||||
/// </summary>
|
||||
private sealed class Comparer<T> : IEqualityComparer<ImmutableArray<T>>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <typeparamref name="T"/> comparer.
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<T> comparer;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Comparer{T}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="comparer">The <typeparamref name="T"/> comparer.</param>
|
||||
public Comparer(IEqualityComparer<T> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(ImmutableArray<T> x, ImmutableArray<T> y)
|
||||
{
|
||||
return
|
||||
x == y ||
|
||||
x.SequenceEqual(y, this.comparer);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode(ImmutableArray<T> obj)
|
||||
{
|
||||
HashCode hashCode = default;
|
||||
|
||||
hashCode.AddRange(obj, this.comparer);
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// 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.
|
||||
|
||||
@ -13,11 +13,11 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
internal static class INamedTypeSymbolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the full metadata name for a given <see cref="INamedTypeSymbol"/> instance.
|
||||
/// Gets a valid filename for a given <see cref="INamedTypeSymbol"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The input <see cref="INamedTypeSymbol"/> instance.</param>
|
||||
/// <returns>The full metadata name for <paramref name="symbol"/>.</returns>
|
||||
public static string GetFullMetadataName(this INamedTypeSymbol symbol)
|
||||
/// <returns>The full metadata name for <paramref name="symbol"/> that is also a valid filename.</returns>
|
||||
public static string GetFullMetadataNameForFileName(this INamedTypeSymbol symbol)
|
||||
{
|
||||
static StringBuilder BuildFrom(ISymbol? symbol, StringBuilder builder)
|
||||
{
|
||||
@ -33,32 +33,26 @@ static StringBuilder BuildFrom(ISymbol? symbol, StringBuilder builder)
|
||||
};
|
||||
}
|
||||
|
||||
return BuildFrom(symbol, new StringBuilder(256)).ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a valid filename for a given <see cref="INamedTypeSymbol"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The input <see cref="INamedTypeSymbol"/> instance.</param>
|
||||
/// <returns>The full metadata name for <paramref name="symbol"/> that is also a valid filename.</returns>
|
||||
public static string GetFullMetadataNameForFileName(this INamedTypeSymbol symbol)
|
||||
{
|
||||
return symbol.GetFullMetadataName().Replace('`', '-').Replace('+', '.');
|
||||
// Build the full metadata name by concatenating the metadata names of all symbols from the input
|
||||
// one to the outermost namespace, if any. Additionally, the ` and + symbols need to be replaced
|
||||
// to avoid errors when generating code. This is a known issue with source generators not accepting
|
||||
// those characters at the moment, see: https://github.com/dotnet/roslyn/issues/58476.
|
||||
return BuildFrom(symbol, new StringBuilder(256)).ToString().Replace('`', '-').Replace('+', '.');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not a given <see cref="INamedTypeSymbol"/> inherits from a specified type.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The target <see cref="INamedTypeSymbol"/> instance to check.</param>
|
||||
/// <param name="targetTypeSymbol">The type symbol of the type to check for inheritance.</param>
|
||||
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="targetTypeSymbol"/>.</returns>
|
||||
public static bool InheritsFrom(this INamedTypeSymbol typeSymbol, INamedTypeSymbol targetTypeSymbol)
|
||||
/// <param name="name">The full name of the type to check for inheritance.</param>
|
||||
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="name"/>.</returns>
|
||||
public static bool InheritsFrom(this INamedTypeSymbol typeSymbol, string name)
|
||||
{
|
||||
INamedTypeSymbol? baseType = typeSymbol.BaseType;
|
||||
|
||||
while (baseType != null)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(baseType, targetTypeSymbol))
|
||||
if (baseType.HasFullyQualifiedName(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
// 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;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="ISymbol"/> type.
|
||||
/// </summary>
|
||||
internal static class ISymbolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the fully qualified name for a given symbol.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The input <see cref="ISymbol"/> instance.</param>
|
||||
/// <returns>The fully qualified name for <paramref name="symbol"/>.</returns>
|
||||
public static string GetFullyQualifiedName(this ISymbol symbol)
|
||||
{
|
||||
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not a given type symbol has a specified full name.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The input <see cref="ISymbol"/> instance to check.</param>
|
||||
/// <param name="name">The full name to check.</param>
|
||||
/// <returns>Whether <paramref name="symbol"/> has a full name equals to <paramref name="name"/>.</returns>
|
||||
public static bool HasFullyQualifiedName(this ISymbol symbol, string name)
|
||||
{
|
||||
return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == name;
|
||||
}
|
||||
}
|
@ -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 Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IncrementalGeneratorInitializationContext"/>.
|
||||
/// </summary>
|
||||
internal static class IncrementalGeneratorInitializationContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a gate for a language version over items in an input <see cref="IncrementalValuesProvider{TValues}"/> source.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items in the input <see cref="IncrementalValuesProvider{TValues}"/> source.</typeparam>
|
||||
/// <param name="context">The input <see cref="IncrementalGeneratorInitializationContext"/> value being used.</param>
|
||||
/// <param name="source">The source <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
|
||||
/// <param name="languageVersion">The minimum language version to gate for.</param>
|
||||
/// <param name="diagnosticDescriptor">The <see cref="DiagnosticDescriptor"/> to emit if the gate detects invalid usage.</param>
|
||||
/// <remarks>
|
||||
/// Items in <paramref name="source"/> will be filtered out if the gate fails. If it passes, items will remain untouched.
|
||||
/// </remarks>
|
||||
public static void FilterWithLanguageVersion<T>(
|
||||
this IncrementalGeneratorInitializationContext context,
|
||||
ref IncrementalValuesProvider<T> source,
|
||||
LanguageVersion languageVersion,
|
||||
DiagnosticDescriptor diagnosticDescriptor)
|
||||
{
|
||||
// Check whether the target language version is supported
|
||||
IncrementalValueProvider<bool> isGeneratorSupported =
|
||||
context.ParseOptionsProvider
|
||||
.Select((item, _) => item is CSharpParseOptions options && options.LanguageVersion >= languageVersion);
|
||||
|
||||
// Combine each data item with the supported flag
|
||||
IncrementalValuesProvider<(T Data, bool IsGeneratorSupported)> dataWithSupportedInfo =
|
||||
source
|
||||
.Combine(isGeneratorSupported);
|
||||
|
||||
// Get a marker node to show whether an invalid attribute is used
|
||||
IncrementalValueProvider<bool> isUnsupportedAttributeUsed =
|
||||
dataWithSupportedInfo
|
||||
.Select(static (item, _) => item.IsGeneratorSupported)
|
||||
.Where(static item => !item)
|
||||
.Collect()
|
||||
.Select(static (item, _) => item.Length > 0);
|
||||
|
||||
// Report them to the output
|
||||
context.RegisterSourceOutput(isUnsupportedAttributeUsed, (context, diagnostic) =>
|
||||
{
|
||||
if (diagnostic)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, null));
|
||||
}
|
||||
});
|
||||
|
||||
// Only let data through if the minimum language version is supported
|
||||
source =
|
||||
dataWithSupportedInfo
|
||||
.Where(static item => item.IsGeneratorSupported)
|
||||
.Select(static (item, _) => item.Data);
|
||||
}
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
// 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.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IncrementalValuesProvider{TValues}"/>.
|
||||
/// </summary>
|
||||
internal static class IncrementalValuesProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Groups items in a given <see cref="IncrementalValuesProvider{TValue}"/> sequence by a specified key.
|
||||
/// </summary>
|
||||
/// <typeparam name="TLeft">The type of left items in each tuple.</typeparam>
|
||||
/// <typeparam name="TRight">The type of right items in each tuple.</typeparam>
|
||||
/// <param name="source">The input <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
|
||||
/// <param name="comparer">A <typeparamref name="TLeft"/> comparer.</param>
|
||||
/// <returns>An <see cref="IncrementalValuesProvider{TValues}"/> with the grouped results.</returns>
|
||||
public static IncrementalValuesProvider<(TLeft Left, ImmutableArray<TRight> Right)> GroupBy<TLeft, TRight>(
|
||||
this IncrementalValuesProvider<(TLeft Left, TRight Right)> source,
|
||||
IEqualityComparer<TLeft> comparer)
|
||||
{
|
||||
return source.Collect().SelectMany((item, _) =>
|
||||
{
|
||||
Dictionary<TLeft, ImmutableArray<TRight>.Builder> map = new(comparer);
|
||||
|
||||
foreach ((TLeft hierarchy, TRight info) in item)
|
||||
{
|
||||
if (!map.TryGetValue(hierarchy, out ImmutableArray<TRight>.Builder builder))
|
||||
{
|
||||
builder = ImmutableArray.CreateBuilder<TRight>();
|
||||
|
||||
map.Add(hierarchy, builder);
|
||||
}
|
||||
|
||||
builder.Add(info);
|
||||
}
|
||||
|
||||
ImmutableArray<(TLeft Hierarchy, ImmutableArray<TRight> Properties)>.Builder result =
|
||||
ImmutableArray.CreateBuilder<(TLeft, ImmutableArray<TRight>)>();
|
||||
|
||||
foreach (KeyValuePair<TLeft, ImmutableArray<TRight>.Builder> entry in map)
|
||||
{
|
||||
result.Add((entry.Key, entry.Value.ToImmutable()));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="IncrementalValuesProvider{TValues}"/> instance with a gven pair of comparers.
|
||||
/// </summary>
|
||||
/// <typeparam name="TLeft">The type of left items in each tuple.</typeparam>
|
||||
/// <typeparam name="TRight">The type of right items in each tuple.</typeparam>
|
||||
/// <param name="source">The input <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
|
||||
/// <param name="comparerLeft">An <see cref="IEqualityComparer{T}"/> instance for <typeparamref name="TLeft"/> items.</param>
|
||||
/// <param name="comparerRight">An <see cref="IEqualityComparer{T}"/> instance for <typeparamref name="TRight"/> items.</param>
|
||||
/// <returns>An <see cref="IncrementalValuesProvider{TValues}"/> with the specified comparers applied to each item.</returns>
|
||||
public static IncrementalValuesProvider<(TLeft Left, TRight Right)> WithComparers<TLeft, TRight>(
|
||||
this IncrementalValuesProvider<(TLeft Left, TRight Right)> source,
|
||||
IEqualityComparer<TLeft> comparerLeft,
|
||||
IEqualityComparer<TRight> comparerRight)
|
||||
{
|
||||
return source.WithComparer(new Comparer<TLeft, TRight>(comparerLeft, comparerRight));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="IncrementalValuesProvider{TValues}"/> instance with a gven pair of comparers.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of first items in each tuple.</typeparam>
|
||||
/// <typeparam name="T2">The type of second items in each tuple.</typeparam>
|
||||
/// <typeparam name="T3">The type of third items in each tuple.</typeparam>
|
||||
/// <param name="source">The input <see cref="IncrementalValuesProvider{TValues}"/> instance.</param>
|
||||
/// <param name="comparer1">An <see cref="IEqualityComparer{T}"/> instance for <typeparamref name="T1"/> items.</param>
|
||||
/// <param name="comparer2">An <see cref="IEqualityComparer{T}"/> instance for <typeparamref name="T2"/> items.</param>
|
||||
/// <param name="comparer3">An <see cref="IEqualityComparer{T}"/> instance for <typeparamref name="T3"/> items.</param>
|
||||
/// <returns>An <see cref="IncrementalValuesProvider{TValues}"/> with the specified comparers applied to each item.</returns>
|
||||
public static IncrementalValuesProvider<(T1, T2, T3)> WithComparers<T1, T2, T3>(
|
||||
this IncrementalValuesProvider<(T1, T2, T3)> source,
|
||||
IEqualityComparer<T1> comparer1,
|
||||
IEqualityComparer<T2> comparer2,
|
||||
IEqualityComparer<T3> comparer3)
|
||||
{
|
||||
return source.WithComparer(new Comparer<T1, T2, T3>(comparer1, comparer2, comparer3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for a value tuple.
|
||||
/// </summary>
|
||||
private sealed class Comparer<TLeft, TRight> : IEqualityComparer<(TLeft Left, TRight Right)>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <typeparamref name="TLeft"/> comparer.
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<TLeft> comparerLeft;
|
||||
|
||||
/// <summary>
|
||||
/// The <typeparamref name="TRight"/> comparer.
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<TRight> comparerRight;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Comparer{TLeft, TRight}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="comparerLeft">The <typeparamref name="TLeft"/> comparer.</param>
|
||||
/// <param name="comparerRight">The <typeparamref name="TRight"/> comparer.</param>
|
||||
public Comparer(IEqualityComparer<TLeft> comparerLeft, IEqualityComparer<TRight> comparerRight)
|
||||
{
|
||||
this.comparerLeft = comparerLeft;
|
||||
this.comparerRight = comparerRight;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals((TLeft Left, TRight Right) x, (TLeft Left, TRight Right) y)
|
||||
{
|
||||
return
|
||||
this.comparerLeft.Equals(x.Left, y.Left) &&
|
||||
this.comparerRight.Equals(x.Right, y.Right);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode((TLeft Left, TRight Right) obj)
|
||||
{
|
||||
return HashCode.Combine(
|
||||
this.comparerLeft.GetHashCode(obj.Left),
|
||||
this.comparerRight.GetHashCode(obj.Right));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for a value tuple.
|
||||
/// </summary>
|
||||
private sealed class Comparer<T1, T2, T3> : IEqualityComparer<(T1, T2, T3)>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <typeparamref name="T1"/> comparer.
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<T1> comparer1;
|
||||
|
||||
/// <summary>
|
||||
/// The <typeparamref name="T2"/> comparer.
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<T2> comparer2;
|
||||
|
||||
/// <summary>
|
||||
/// The <typeparamref name="T3"/> comparer.
|
||||
/// </summary>
|
||||
private readonly IEqualityComparer<T3> comparer3;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Comparer{T1, T2, T3}"/> instance with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="comparer1">The <typeparamref name="T1"/> comparer.</param>
|
||||
/// <param name="comparer2">The <typeparamref name="T2"/> comparer.</param>
|
||||
/// <param name="comparer3">The <typeparamref name="T3"/> comparer.</param>
|
||||
public Comparer(IEqualityComparer<T1> comparer1, IEqualityComparer<T2> comparer2, IEqualityComparer<T3> comparer3)
|
||||
{
|
||||
this.comparer1 = comparer1;
|
||||
this.comparer2 = comparer2;
|
||||
this.comparer3 = comparer3;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals((T1, T2, T3) x, (T1, T2, T3) y)
|
||||
{
|
||||
return
|
||||
this.comparer1.Equals(x.Item1, y.Item1) &&
|
||||
this.comparer2.Equals(x.Item2, y.Item2) &&
|
||||
this.comparer3.Equals(x.Item3, y.Item3);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode((T1, T2, T3) obj)
|
||||
{
|
||||
return HashCode.Combine(
|
||||
this.comparer1.GetHashCode(obj.Item1),
|
||||
this.comparer2.GetHashCode(obj.Item2),
|
||||
this.comparer3.GetHashCode(obj.Item3));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// A base <see cref="IEqualityComparer{T}"/> implementation for <typeparamref name="T"/> instances.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of items to compare.</typeparam>
|
||||
/// <typeparam name="TSelf">The concrete comparer type.</typeparam>
|
||||
internal abstract class Comparer<T, TSelf> : IEqualityComparer<T>
|
||||
where TSelf : Comparer<T, TSelf>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// The singleton <typeparamref name="TSelf"/> instance.
|
||||
/// </summary>
|
||||
public static TSelf Default { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(T? x, T? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return AreEqual(x, y);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetHashCode(T obj)
|
||||
{
|
||||
HashCode hashCode = default;
|
||||
|
||||
AddToHashCode(ref hashCode, obj);
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the current instance to an incremental <see cref="HashCode"/> value.
|
||||
/// </summary>
|
||||
/// <param name="hashCode">The target <see cref="HashCode"/> value.</param>
|
||||
/// <param name="obj">The <typeparamref name="T"/> instance being inspected.</param>
|
||||
protected abstract void AddToHashCode(ref HashCode hashCode, T obj);
|
||||
|
||||
/// <summary>
|
||||
/// Compares two <typeparamref name="T"/> instances for equality.
|
||||
/// </summary>
|
||||
/// <param name="x">The first <typeparamref name="T"/> instance to compare.</param>
|
||||
/// <param name="y">The second <typeparamref name="T"/> instance to compare.</param>
|
||||
/// <returns>Whether or not <paramref name="x"/> and <paramref name="y"/> are equal.</returns>
|
||||
protected abstract bool AreEqual(T x, T y);
|
||||
}
|
497
CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs
Normal file
497
CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs
Normal file
@ -0,0 +1,497 @@
|
||||
// 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.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
#pragma warning disable CS0809
|
||||
|
||||
namespace System;
|
||||
|
||||
/// <summary>
|
||||
/// A polyfill type that mirrors some methods from <see cref="HashCode"/> on .NET 6.
|
||||
/// </summary>
|
||||
internal struct HashCode
|
||||
{
|
||||
private const uint Prime1 = 2654435761U;
|
||||
private const uint Prime2 = 2246822519U;
|
||||
private const uint Prime3 = 3266489917U;
|
||||
private const uint Prime4 = 668265263U;
|
||||
private const uint Prime5 = 374761393U;
|
||||
|
||||
private static readonly uint seed = GenerateGlobalSeed();
|
||||
|
||||
private uint v1, v2, v3, v4;
|
||||
private uint queue1, queue2, queue3;
|
||||
private uint length;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the default seed.
|
||||
/// </summary>
|
||||
/// <returns>A random seed.</returns>
|
||||
private static unsafe uint GenerateGlobalSeed()
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
|
||||
RandomNumberGenerator.Create().GetBytes(bytes);
|
||||
|
||||
return BitConverter.ToUInt32(bytes, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines a value into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the value to combine into the hash code.</typeparam>
|
||||
/// <param name="value">The value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the value.</returns>
|
||||
public static int Combine<T1>(T1 value)
|
||||
{
|
||||
uint hc1 = (uint)(value?.GetHashCode() ?? 0);
|
||||
uint hash = MixEmptyState();
|
||||
|
||||
hash += 4;
|
||||
hash = QueueRound(hash, hc1);
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines two values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2>(T1 value1, T2 value2)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hash = MixEmptyState();
|
||||
|
||||
hash += 8;
|
||||
hash = QueueRound(hash, hc1);
|
||||
hash = QueueRound(hash, hc2);
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines three values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T3">The type of the third value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <param name="value3">The third value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hc3 = (uint)(value3?.GetHashCode() ?? 0);
|
||||
uint hash = MixEmptyState();
|
||||
|
||||
hash += 12;
|
||||
hash = QueueRound(hash, hc1);
|
||||
hash = QueueRound(hash, hc2);
|
||||
hash = QueueRound(hash, hc3);
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines four values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T3">The type of the third value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T4">The type of the fourth value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <param name="value3">The third value to combine into the hash code.</param>
|
||||
/// <param name="value4">The fourth value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hc3 = (uint)(value3?.GetHashCode() ?? 0);
|
||||
uint hc4 = (uint)(value4?.GetHashCode() ?? 0);
|
||||
|
||||
Initialize(out uint v1, out uint v2, out uint v3, out uint v4);
|
||||
|
||||
v1 = Round(v1, hc1);
|
||||
v2 = Round(v2, hc2);
|
||||
v3 = Round(v3, hc3);
|
||||
v4 = Round(v4, hc4);
|
||||
|
||||
uint hash = MixState(v1, v2, v3, v4);
|
||||
|
||||
hash += 16;
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines five values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T3">The type of the third value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T4">The type of the fourth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T5">The type of the fifth value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <param name="value3">The third value to combine into the hash code.</param>
|
||||
/// <param name="value4">The fourth value to combine into the hash code.</param>
|
||||
/// <param name="value5">The fifth value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hc3 = (uint)(value3?.GetHashCode() ?? 0);
|
||||
uint hc4 = (uint)(value4?.GetHashCode() ?? 0);
|
||||
uint hc5 = (uint)(value5?.GetHashCode() ?? 0);
|
||||
|
||||
Initialize(out uint v1, out uint v2, out uint v3, out uint v4);
|
||||
|
||||
v1 = Round(v1, hc1);
|
||||
v2 = Round(v2, hc2);
|
||||
v3 = Round(v3, hc3);
|
||||
v4 = Round(v4, hc4);
|
||||
|
||||
uint hash = MixState(v1, v2, v3, v4);
|
||||
|
||||
hash += 20;
|
||||
hash = QueueRound(hash, hc5);
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines six values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T3">The type of the third value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T4">The type of the fourth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T5">The type of the fifth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T6">The type of the sixth value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <param name="value3">The third value to combine into the hash code.</param>
|
||||
/// <param name="value4">The fourth value to combine into the hash code.</param>
|
||||
/// <param name="value5">The fifth value to combine into the hash code.</param>
|
||||
/// <param name="value6">The sixth value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hc3 = (uint)(value3?.GetHashCode() ?? 0);
|
||||
uint hc4 = (uint)(value4?.GetHashCode() ?? 0);
|
||||
uint hc5 = (uint)(value5?.GetHashCode() ?? 0);
|
||||
uint hc6 = (uint)(value6?.GetHashCode() ?? 0);
|
||||
|
||||
Initialize(out uint v1, out uint v2, out uint v3, out uint v4);
|
||||
|
||||
v1 = Round(v1, hc1);
|
||||
v2 = Round(v2, hc2);
|
||||
v3 = Round(v3, hc3);
|
||||
v4 = Round(v4, hc4);
|
||||
|
||||
uint hash = MixState(v1, v2, v3, v4);
|
||||
|
||||
hash += 24;
|
||||
hash = QueueRound(hash, hc5);
|
||||
hash = QueueRound(hash, hc6);
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines seven values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T3">The type of the third value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T4">The type of the fourth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T5">The type of the fifth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T6">The type of the sixth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T7">The type of the seventh value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <param name="value3">The third value to combine into the hash code.</param>
|
||||
/// <param name="value4">The fourth value to combine into the hash code.</param>
|
||||
/// <param name="value5">The fifth value to combine into the hash code.</param>
|
||||
/// <param name="value6">The sixth value to combine into the hash code.</param>
|
||||
/// <param name="value7">The seventh value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hc3 = (uint)(value3?.GetHashCode() ?? 0);
|
||||
uint hc4 = (uint)(value4?.GetHashCode() ?? 0);
|
||||
uint hc5 = (uint)(value5?.GetHashCode() ?? 0);
|
||||
uint hc6 = (uint)(value6?.GetHashCode() ?? 0);
|
||||
uint hc7 = (uint)(value7?.GetHashCode() ?? 0);
|
||||
|
||||
Initialize(out uint v1, out uint v2, out uint v3, out uint v4);
|
||||
|
||||
v1 = Round(v1, hc1);
|
||||
v2 = Round(v2, hc2);
|
||||
v3 = Round(v3, hc3);
|
||||
v4 = Round(v4, hc4);
|
||||
|
||||
uint hash = MixState(v1, v2, v3, v4);
|
||||
|
||||
hash += 28;
|
||||
hash = QueueRound(hash, hc5);
|
||||
hash = QueueRound(hash, hc6);
|
||||
hash = QueueRound(hash, hc7);
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines eight values into a hash code.
|
||||
/// </summary>
|
||||
/// <typeparam name="T1">The type of the first value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T2">The type of the second value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T3">The type of the third value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T4">The type of the fourth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T5">The type of the fifth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T6">The type of the sixth value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T7">The type of the seventh value to combine into the hash code.</typeparam>
|
||||
/// <typeparam name="T8">The type of the eigth value to combine into the hash code.</typeparam>
|
||||
/// <param name="value1">The first value to combine into the hash code.</param>
|
||||
/// <param name="value2">The second value to combine into the hash code.</param>
|
||||
/// <param name="value3">The third value to combine into the hash code.</param>
|
||||
/// <param name="value4">The fourth value to combine into the hash code.</param>
|
||||
/// <param name="value5">The fifth value to combine into the hash code.</param>
|
||||
/// <param name="value6">The sixth value to combine into the hash code.</param>
|
||||
/// <param name="value7">The seventh value to combine into the hash code.</param>
|
||||
/// <param name="value8">The eigth value to combine into the hash code.</param>
|
||||
/// <returns>The hash code that represents the values.</returns>
|
||||
public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8)
|
||||
{
|
||||
uint hc1 = (uint)(value1?.GetHashCode() ?? 0);
|
||||
uint hc2 = (uint)(value2?.GetHashCode() ?? 0);
|
||||
uint hc3 = (uint)(value3?.GetHashCode() ?? 0);
|
||||
uint hc4 = (uint)(value4?.GetHashCode() ?? 0);
|
||||
uint hc5 = (uint)(value5?.GetHashCode() ?? 0);
|
||||
uint hc6 = (uint)(value6?.GetHashCode() ?? 0);
|
||||
uint hc7 = (uint)(value7?.GetHashCode() ?? 0);
|
||||
uint hc8 = (uint)(value8?.GetHashCode() ?? 0);
|
||||
|
||||
Initialize(out uint v1, out uint v2, out uint v3, out uint v4);
|
||||
|
||||
v1 = Round(v1, hc1);
|
||||
v2 = Round(v2, hc2);
|
||||
v3 = Round(v3, hc3);
|
||||
v4 = Round(v4, hc4);
|
||||
|
||||
v1 = Round(v1, hc5);
|
||||
v2 = Round(v2, hc6);
|
||||
v3 = Round(v3, hc7);
|
||||
v4 = Round(v4, hc8);
|
||||
|
||||
uint hash = MixState(v1, v2, v3, v4);
|
||||
|
||||
hash += 32;
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single value to the current hash.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value to add into the hash code.</typeparam>
|
||||
/// <param name="value">The value to add into the hash code.</param>
|
||||
public void Add<T>(T value)
|
||||
{
|
||||
Add(value?.GetHashCode() ?? 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single value to the current hash.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value to add into the hash code.</typeparam>
|
||||
/// <param name="value">The value to add into the hash code.</param>
|
||||
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use.</param>
|
||||
public void Add<T>(T value, IEqualityComparer<T>? comparer)
|
||||
{
|
||||
Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a span of bytes to the hash code.
|
||||
/// </summary>
|
||||
/// <param name="value">The span.</param>
|
||||
public void AddBytes(ReadOnlySpan<byte> value)
|
||||
{
|
||||
ref byte pos = ref MemoryMarshal.GetReference(value);
|
||||
ref byte end = ref Unsafe.Add(ref pos, value.Length);
|
||||
|
||||
while ((nint)Unsafe.ByteOffset(ref pos, ref end) >= sizeof(int))
|
||||
{
|
||||
Add(Unsafe.ReadUnaligned<int>(ref pos));
|
||||
pos = ref Unsafe.Add(ref pos, sizeof(int));
|
||||
}
|
||||
|
||||
while (Unsafe.IsAddressLessThan(ref pos, ref end))
|
||||
{
|
||||
Add((int)pos);
|
||||
pos = ref Unsafe.Add(ref pos, 1);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
|
||||
{
|
||||
v1 = seed + Prime1 + Prime2;
|
||||
v2 = seed + Prime2;
|
||||
v3 = seed;
|
||||
v4 = seed - Prime1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static uint Round(uint hash, uint input)
|
||||
{
|
||||
return RotateLeft(hash + input * Prime2, 13) * Prime1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static uint QueueRound(uint hash, uint queuedValue)
|
||||
{
|
||||
return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static uint MixState(uint v1, uint v2, uint v3, uint v4)
|
||||
{
|
||||
return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static uint MixEmptyState()
|
||||
{
|
||||
return seed + Prime5;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static uint MixFinal(uint hash)
|
||||
{
|
||||
hash ^= hash >> 15;
|
||||
hash *= Prime2;
|
||||
hash ^= hash >> 13;
|
||||
hash *= Prime3;
|
||||
hash ^= hash >> 16;
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
private void Add(int value)
|
||||
{
|
||||
uint val = (uint)value;
|
||||
uint previousLength = length++;
|
||||
uint position = previousLength % 4;
|
||||
|
||||
if (position == 0)
|
||||
{
|
||||
queue1 = val;
|
||||
}
|
||||
else if (position == 1)
|
||||
{
|
||||
queue2 = val;
|
||||
}
|
||||
else if (position == 2)
|
||||
{
|
||||
queue3 = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousLength == 3)
|
||||
{
|
||||
Initialize(out v1, out v2, out v3, out v4);
|
||||
}
|
||||
|
||||
v1 = Round(v1, queue1);
|
||||
v2 = Round(v2, queue2);
|
||||
v3 = Round(v3, queue3);
|
||||
v4 = Round(v4, val);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resulting hashcode from the current instance.
|
||||
/// </summary>
|
||||
/// <returns>The resulting hashcode from the current instance.</returns>
|
||||
public int ToHashCode()
|
||||
{
|
||||
uint length = this.length;
|
||||
uint position = length % 4;
|
||||
uint hash = length < 4 ? MixEmptyState() : MixState(v1, v2, v3, v4);
|
||||
|
||||
hash += length * 4;
|
||||
|
||||
if (position > 0)
|
||||
{
|
||||
hash = QueueRound(hash, queue1);
|
||||
|
||||
if (position > 1)
|
||||
{
|
||||
hash = QueueRound(hash, queue2);
|
||||
|
||||
if (position > 2)
|
||||
{
|
||||
hash = QueueRound(hash, queue3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hash = MixFinal(hash);
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override int GetHashCode() => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public override bool Equals(object? obj) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// Rotates the specified value left by the specified number of bits.
|
||||
/// Similar in behavior to the x86 instruction ROL.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to rotate.</param>
|
||||
/// <param name="offset">The number of bits to rotate by.
|
||||
/// Any value outside the range [0..31] is treated as congruent mod 32.</param>
|
||||
/// <returns>The rotated value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static uint RotateLeft(uint value, int offset)
|
||||
{
|
||||
return (value << offset) | (value >> (32 - offset));
|
||||
}
|
||||
}
|
@ -0,0 +1,535 @@
|
||||
// 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;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial class ICommandGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for all the logic for <see cref="ICommandGenerator"/>.
|
||||
/// </summary>
|
||||
private static class Execute
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes a given target method.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
|
||||
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
|
||||
/// <returns>The resulting <see cref="CommandInfo"/> instance for <paramref name="methodSymbol"/>, if available.</returns>
|
||||
public static CommandInfo? GetInfo(IMethodSymbol methodSymbol, AttributeData attributeData, out ImmutableArray<Diagnostic> diagnostics)
|
||||
{
|
||||
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
|
||||
|
||||
// Get the command field and property names
|
||||
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(methodSymbol);
|
||||
|
||||
// Get the command type symbols
|
||||
if (!TryMapCommandTypesFromMethod(
|
||||
methodSymbol,
|
||||
builder,
|
||||
out string? commandInterfaceType,
|
||||
out string? commandClassType,
|
||||
out string? delegateType,
|
||||
out ImmutableArray<string> commandTypeArguments,
|
||||
out ImmutableArray<string> delegateTypeArguments))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
// Check the switch to allow concurrent executions
|
||||
if (!TryGetAllowConcurrentExecutionsSwitch(
|
||||
methodSymbol,
|
||||
attributeData,
|
||||
commandClassType,
|
||||
builder,
|
||||
out bool allowConcurrentExecutions))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
// Get the CanExecute expression type, if any
|
||||
if (!TryGetCanExecuteExpressionType(
|
||||
methodSymbol,
|
||||
attributeData,
|
||||
commandTypeArguments,
|
||||
builder,
|
||||
out string? canExecuteMemberName,
|
||||
out CanExecuteExpressionType? canExecuteExpressionType))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return new(
|
||||
methodSymbol.Name,
|
||||
fieldName,
|
||||
propertyName,
|
||||
commandInterfaceType,
|
||||
commandClassType,
|
||||
delegateType,
|
||||
commandTypeArguments,
|
||||
delegateTypeArguments,
|
||||
canExecuteMemberName,
|
||||
canExecuteExpressionType,
|
||||
allowConcurrentExecutions);
|
||||
|
||||
Failure:
|
||||
diagnostics = builder.ToImmutable();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="MemberDeclarationSyntax"/> instances for a specified command.
|
||||
/// </summary>
|
||||
/// <param name="commandInfo">The input <see cref="CommandInfo"/> instance with the info to generate the command.</param>
|
||||
/// <returns>The <see cref="MemberDeclarationSyntax"/> instances for the input command.</returns>
|
||||
public static ImmutableArray<MemberDeclarationSyntax> GetSyntax(CommandInfo commandInfo)
|
||||
{
|
||||
// Prepare all necessary type names with type arguments
|
||||
string commandInterfaceTypeXmlName = commandInfo.CommandTypeArguments.IsEmpty
|
||||
? commandInfo.CommandInterfaceType
|
||||
: commandInfo.CommandInterfaceType + "{T}";
|
||||
string commandClassTypeName = commandInfo.CommandTypeArguments.IsEmpty
|
||||
? commandInfo.CommandClassType
|
||||
: $"{commandInfo.CommandClassType}<{string.Join(", ", commandInfo.CommandTypeArguments)}>";
|
||||
string commandInterfaceTypeName = commandInfo.CommandTypeArguments.IsEmpty
|
||||
? commandInfo.CommandInterfaceType
|
||||
: $"{commandInfo.CommandInterfaceType}<{string.Join(", ", commandInfo.CommandTypeArguments)}>";
|
||||
string delegateTypeName = commandInfo.DelegateTypeArguments.IsEmpty
|
||||
? commandInfo.DelegateType
|
||||
: $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>";
|
||||
|
||||
// Construct the generated field as follows:
|
||||
//
|
||||
// <summary>The backing field for <see cref="<COMMAND_PROPERTY_NAME>"/></summary>
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
|
||||
FieldDeclarationSyntax fieldDeclaration =
|
||||
FieldDeclaration(
|
||||
VariableDeclaration(NullableType(IdentifierName(commandClassTypeName)))
|
||||
.AddVariables(VariableDeclarator(Identifier(commandInfo.FieldName))))
|
||||
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))
|
||||
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <summary>The backing field for <see cref=\"{commandInfo.PropertyName}\"/>.</summary>")), SyntaxKind.OpenBracketToken, TriviaList())));
|
||||
|
||||
// Prepares the argument to pass the underlying method to invoke
|
||||
ImmutableArray<ArgumentSyntax>.Builder commandCreationArguments = ImmutableArray.CreateBuilder<ArgumentSyntax>();
|
||||
|
||||
// The first argument is the execute method, which is always present
|
||||
commandCreationArguments.Add(
|
||||
Argument(
|
||||
ObjectCreationExpression(IdentifierName(delegateTypeName))
|
||||
.AddArgumentListArguments(Argument(IdentifierName(commandInfo.MethodName)))));
|
||||
|
||||
// Get the can execute expression, if available
|
||||
ExpressionSyntax? canExecuteExpression = commandInfo.CanExecuteExpressionType switch
|
||||
{
|
||||
// Create a lambda expression ignoring the input value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_METHOD>());
|
||||
CanExecuteExpressionType.MethodInvocationLambdaWithDiscard =>
|
||||
SimpleLambdaExpression(
|
||||
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
|
||||
.WithExpressionBody(InvocationExpression(IdentifierName(commandInfo.CanExecuteMemberName!))),
|
||||
|
||||
// Create a lambda expression returning the property value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, () => <CAN_EXECUTE_PROPERTY>);
|
||||
CanExecuteExpressionType.PropertyAccessLambda =>
|
||||
ParenthesizedLambdaExpression()
|
||||
.WithExpressionBody(IdentifierName(commandInfo.CanExecuteMemberName!)),
|
||||
|
||||
// Create a lambda expression again, but discarding the input value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_PROPERTY>);
|
||||
CanExecuteExpressionType.PropertyAccessLambdaWithDiscard =>
|
||||
SimpleLambdaExpression(
|
||||
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
|
||||
.WithExpressionBody(IdentifierName(commandInfo.CanExecuteMemberName!)),
|
||||
|
||||
// Create a method groupd expression, which will become:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, <CAN_EXECUTE_METHOD>);
|
||||
CanExecuteExpressionType.MethodGroup => IdentifierName(commandInfo.CanExecuteMemberName!),
|
||||
_ => null
|
||||
};
|
||||
|
||||
// Add the can execute expression to the arguments, if available
|
||||
if (canExecuteExpression is not null)
|
||||
{
|
||||
commandCreationArguments.Add(Argument(canExecuteExpression));
|
||||
}
|
||||
|
||||
// Disable concurrent executions, if requested
|
||||
if (!commandInfo.AllowConcurrentExecutions)
|
||||
{
|
||||
commandCreationArguments.Add(Argument(LiteralExpression(SyntaxKind.FalseLiteralExpression)));
|
||||
}
|
||||
|
||||
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
|
||||
//
|
||||
// <summary>Gets an <see cref="<COMMAND_INTERFACE_TYPE>" instance wrapping <see cref="<METHOD_NAME>"/> and <see cref="<OPTIONAL_CAN_EXECUTE>"/>.</summary>
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// public <COMMAND_TYPE> <COMMAND_PROPERTY_NAME> => <COMMAND_FIELD_NAME> ??= new <RELAY_COMMAND_TYPE>(<COMMAND_CREATION_ARGUMENTS>);
|
||||
PropertyDeclarationSyntax propertyDeclaration =
|
||||
PropertyDeclaration(
|
||||
IdentifierName(commandInterfaceTypeName),
|
||||
Identifier(commandInfo.PropertyName))
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))
|
||||
.WithOpenBracketToken(Token(TriviaList(Comment(
|
||||
$"/// <summary>Gets an <see cref=\"{commandInterfaceTypeXmlName}\"/> instance wrapping <see cref=\"{commandInfo.MethodName}\"/>.</summary>")),
|
||||
SyntaxKind.OpenBracketToken,
|
||||
TriviaList())),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
|
||||
.WithExpressionBody(
|
||||
ArrowExpressionClause(
|
||||
AssignmentExpression(
|
||||
SyntaxKind.CoalesceAssignmentExpression,
|
||||
IdentifierName(commandInfo.FieldName),
|
||||
ObjectCreationExpression(IdentifierName(commandClassTypeName))
|
||||
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
|
||||
|
||||
return ImmutableArray.Create<MemberDeclarationSyntax>(fieldDeclaration, propertyDeclaration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generated field and property names for the input method.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
|
||||
private static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(IMethodSymbol methodSymbol)
|
||||
{
|
||||
string propertyName = methodSymbol.Name;
|
||||
|
||||
if (methodSymbol.ReturnType.HasFullyQualifiedName("global::System.Threading.Tasks.Task") &&
|
||||
methodSymbol.Name.EndsWith("Async"))
|
||||
{
|
||||
propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length);
|
||||
}
|
||||
|
||||
propertyName += "Command";
|
||||
|
||||
string fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}";
|
||||
|
||||
return (fieldName, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type symbols for the input method, if supported.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
||||
/// <param name="commandInterfaceType">The command interface type name.</param>
|
||||
/// <param name="commandClassType">The command class type name.</param>
|
||||
/// <param name="delegateType">The delegate type name for the wrapped method.</param>
|
||||
/// <param name="commandTypeArguments">The type arguments for <paramref name="commandInterfaceType"/> and <paramref name="commandClassType"/>, if any.</param>
|
||||
/// <param name="delegateTypeArguments">The type arguments for <paramref name="delegateType"/>, if any.</param>
|
||||
/// <returns>Whether or not <paramref name="methodSymbol"/> was valid and the requested types have been set.</returns>
|
||||
private static bool TryMapCommandTypesFromMethod(
|
||||
IMethodSymbol methodSymbol,
|
||||
ImmutableArray<Diagnostic>.Builder diagnostics,
|
||||
[NotNullWhen(true)] out string? commandInterfaceType,
|
||||
[NotNullWhen(true)] out string? commandClassType,
|
||||
[NotNullWhen(true)] out string? delegateType,
|
||||
out ImmutableArray<string> commandTypeArguments,
|
||||
out ImmutableArray<string> delegateTypeArguments)
|
||||
{
|
||||
// Map <void, void> to IRelayCommand, RelayCommand, Action
|
||||
if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0)
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand";
|
||||
delegateType = "global::System.Action";
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray<string>.Empty;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, void> to IRelayCommand<T>, RelayCommand<T>, Action<T>
|
||||
if (methodSymbol.ReturnsVoid &&
|
||||
methodSymbol.Parameters.Length == 1 &&
|
||||
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } parameter)
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.RelayCommand";
|
||||
delegateType = "global::System.Action";
|
||||
commandTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName());
|
||||
delegateTypeArguments = ImmutableArray.Create(parameter.Type.GetFullyQualifiedName());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.HasFullyQualifiedName("global::System.Threading.Tasks.Task"))
|
||||
{
|
||||
// Map <void, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<Task>
|
||||
if (methodSymbol.Parameters.Length == 0)
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray.Create("global::System.Threading.Tasks.Task");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodSymbol.Parameters.Length == 1 &&
|
||||
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } singleParameter)
|
||||
{
|
||||
// Map <CancellationToken, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<CancellationToken, Task>
|
||||
if (singleParameter.Type.HasFullyQualifiedName("global::System.Threading.CancellationToken"))
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray.Create("global::System.Threading.CancellationToken", "global::System.Threading.Tasks.Task");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, Task>
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName());
|
||||
delegateTypeArguments = ImmutableArray.Create(singleParameter.Type.GetFullyQualifiedName(), "global::System.Threading.Tasks.Task");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, CancellationToken, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, CancellationToken, Task>
|
||||
if (methodSymbol.Parameters.Length == 2 &&
|
||||
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } firstParameter &&
|
||||
methodSymbol.Parameters[1] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } secondParameter &&
|
||||
secondParameter.Type.HasFullyQualifiedName("global::System.Threading.CancellationToken"))
|
||||
{
|
||||
commandInterfaceType = "global::CommunityToolkit.Mvvm.Input.IAsyncRelayCommand";
|
||||
commandClassType = "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand";
|
||||
delegateType = "global::System.Func";
|
||||
commandTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName());
|
||||
delegateTypeArguments = ImmutableArray.Create(firstParameter.Type.GetFullyQualifiedName(), secondParameter.Type.GetFullyQualifiedName(), "global::System.Threading.Tasks.Task");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics.Add(InvalidICommandMethodSignatureError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
|
||||
|
||||
commandInterfaceType = null;
|
||||
commandClassType = null;
|
||||
delegateType = null;
|
||||
commandTypeArguments = ImmutableArray<string>.Empty;
|
||||
delegateTypeArguments = ImmutableArray<string>.Empty;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether or not the user has requested to configure the handling of concurrent executions.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
|
||||
/// <param name="commandClassType">The command class type name.</param>
|
||||
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
||||
/// <param name="allowConcurrentExecutions">Whether or not concurrent executions have been disabled.</param>
|
||||
/// <returns>Whether or not a value for <paramref name="allowConcurrentExecutions"/> could be retrieved successfully.</returns>
|
||||
private static bool TryGetAllowConcurrentExecutionsSwitch(
|
||||
IMethodSymbol methodSymbol,
|
||||
AttributeData attributeData,
|
||||
string commandClassType,
|
||||
ImmutableArray<Diagnostic>.Builder diagnostics,
|
||||
out bool allowConcurrentExecutions)
|
||||
{
|
||||
// Try to get the custom switch for concurrent executions. If the switch is not present, the
|
||||
// default value is set to true, to avoid breaking backwards compatibility with the first release.
|
||||
if (!attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out allowConcurrentExecutions))
|
||||
{
|
||||
allowConcurrentExecutions = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the current type is an async command type and concurrent execution is disabled, pass that value to the constructor.
|
||||
// If concurrent executions are allowed, there is no need to add any additional argument, as that is the default value.
|
||||
if (commandClassType is "global::CommunityToolkit.Mvvm.Input.AsyncRelayCommand")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
diagnostics.Add(InvalidConcurrentExecutionsParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the expression type for the "CanExecute" property, if available.
|
||||
/// </summary>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> instance for <paramref name="methodSymbol"/>.</param>
|
||||
/// <param name="commandTypeArguments">The command type arguments, if any.</param>
|
||||
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
|
||||
/// <param name="canExecuteMemberName">The resulting can execute member name, if available.</param>
|
||||
/// <param name="canExecuteExpressionType">The resulting expression type, if available.</param>
|
||||
/// <returns>Whether or not a value for <paramref name="canExecuteMemberName"/> and <paramref name="canExecuteExpressionType"/> could be determined (may include <see langword="null"/>).</returns>
|
||||
private static bool TryGetCanExecuteExpressionType(
|
||||
IMethodSymbol methodSymbol,
|
||||
AttributeData attributeData,
|
||||
ImmutableArray<string> commandTypeArguments,
|
||||
ImmutableArray<Diagnostic>.Builder diagnostics,
|
||||
out string? canExecuteMemberName,
|
||||
out CanExecuteExpressionType? canExecuteExpressionType)
|
||||
{
|
||||
// Get the can execute member, if any
|
||||
if (!attributeData.TryGetNamedArgument("CanExecute", out string? memberName))
|
||||
{
|
||||
canExecuteMemberName = null;
|
||||
canExecuteExpressionType = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (memberName is null)
|
||||
{
|
||||
diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
|
||||
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
ImmutableArray<ISymbol> canExecuteSymbols = methodSymbol.ContainingType!.GetMembers(memberName);
|
||||
|
||||
if (canExecuteSymbols.IsEmpty)
|
||||
{
|
||||
diagnostics.Add(InvalidCanExecuteMemberName, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
else if (canExecuteSymbols.Length > 1)
|
||||
{
|
||||
diagnostics.Add(MultipleCanExecuteMemberNameMatches, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
else if (TryGetCanExecuteExpressionFromSymbol(canExecuteSymbols[0], commandTypeArguments, out canExecuteExpressionType))
|
||||
{
|
||||
canExecuteMemberName = memberName;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
diagnostics.Add(InvalidCanExecuteMember, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
|
||||
Failure:
|
||||
canExecuteMemberName = null;
|
||||
canExecuteExpressionType = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expression type for the can execute logic, if possible.
|
||||
/// </summary>
|
||||
/// <param name="canExecuteSymbol">The can execute member symbol (either a method or a property).</param>
|
||||
/// <param name="commandTypeArguments">The type arguments for the command interface, if any.</param>
|
||||
/// <param name="canExecuteExpressionType">The resulting can execute expression type, if available.</param>
|
||||
/// <returns>Whether or not <paramref name="canExecuteExpressionType"/> was set and the input symbol was valid.</returns>
|
||||
private static bool TryGetCanExecuteExpressionFromSymbol(
|
||||
ISymbol canExecuteSymbol,
|
||||
ImmutableArray<string> commandTypeArguments,
|
||||
[NotNullWhen(true)] out CanExecuteExpressionType? canExecuteExpressionType)
|
||||
{
|
||||
if (canExecuteSymbol is IMethodSymbol canExecuteMethodSymbol)
|
||||
{
|
||||
// The return type must always be a bool
|
||||
if (!canExecuteMethodSymbol.ReturnType.HasFullyQualifiedName("bool"))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
// Parameterless methods are always valid
|
||||
if (canExecuteMethodSymbol.Parameters.IsEmpty)
|
||||
{
|
||||
// If the command is generic, the input value is ignored
|
||||
if (commandTypeArguments.Length > 0)
|
||||
{
|
||||
canExecuteExpressionType = CanExecuteExpressionType.MethodInvocationLambdaWithDiscard;
|
||||
}
|
||||
else
|
||||
{
|
||||
canExecuteExpressionType = CanExecuteExpressionType.MethodGroup;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the method has parameters, it has to have a single one matching the command type
|
||||
if (canExecuteMethodSymbol.Parameters.Length == 1 &&
|
||||
commandTypeArguments.Length == 1 &&
|
||||
canExecuteMethodSymbol.Parameters[0].Type.HasFullyQualifiedName(commandTypeArguments[0]))
|
||||
{
|
||||
// Create a method group expression again
|
||||
canExecuteExpressionType = CanExecuteExpressionType.MethodGroup;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (canExecuteSymbol is IPropertySymbol { GetMethod: not null } canExecutePropertySymbol)
|
||||
{
|
||||
// The property type must always be a bool
|
||||
if (!canExecutePropertySymbol.Type.HasFullyQualifiedName("bool"))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
if (commandTypeArguments.Length > 0)
|
||||
{
|
||||
canExecuteExpressionType = CanExecuteExpressionType.PropertyAccessLambdaWithDiscard;
|
||||
}
|
||||
else
|
||||
{
|
||||
canExecuteExpressionType = CanExecuteExpressionType.PropertyAccessLambda;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Failure:
|
||||
canExecuteExpressionType = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc cref="ICommandGenerator"/>
|
||||
public sealed partial class ICommandGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISyntaxContextReceiver"/> that selects candidate nodes to process.
|
||||
/// </summary>
|
||||
private sealed class SyntaxReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of info gathered during exploration.
|
||||
/// </summary>
|
||||
private readonly List<Item> gatheredInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of gathered info to process.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Item> GatheredInfo => this.gatheredInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is MethodDeclarationSyntax methodDeclaration &&
|
||||
context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is IMethodSymbol methodSymbol &&
|
||||
context.SemanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.ICommandAttribute") is INamedTypeSymbol iCommandSymbol &&
|
||||
methodSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, iCommandSymbol)) is AttributeData attributeData)
|
||||
{
|
||||
this.gatheredInfo.Add(new Item(methodDeclaration.GetLeadingTrivia(), methodSymbol, attributeData));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A model for a group of item representing a discovered type to process.
|
||||
/// </summary>
|
||||
/// <param name="LeadingTrivia">The leading trivia for the field declaration.</param>
|
||||
/// <param name="MethodSymbol">The <see cref="IMethodSymbol"/> instance for the target method.</param>
|
||||
/// <param name="AttributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
|
||||
public sealed record Item(SyntaxTriviaList LeadingTrivia, IMethodSymbol MethodSymbol, AttributeData AttributeData);
|
||||
}
|
||||
}
|
@ -2,493 +2,78 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
|
||||
using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// A source generator for generating command properties from annotated methods.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed partial class ICommandGenerator : ISourceGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed partial class ICommandGenerator : IIncrementalGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
|
||||
}
|
||||
// Get all method declarations with at least one attribute
|
||||
IncrementalValuesProvider<IMethodSymbol> methodSymbols =
|
||||
context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
static (node, _) => node is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax, AttributeLists.Count: > 0 },
|
||||
static (context, _) => (IMethodSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
// Get the syntax receiver with the candidate nodes
|
||||
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
|
||||
syntaxReceiver.GatheredInfo.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Filter the methods using [ICommand]
|
||||
IncrementalValuesProvider<(IMethodSymbol Symbol, AttributeData Attribute)> methodSymbolsWithAttributeData =
|
||||
methodSymbols
|
||||
.Select(static (item, _) => (
|
||||
item,
|
||||
Attribute: item.GetAttributes().FirstOrDefault(a => a.AttributeClass?.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Input.ICommandAttribute") == true)))
|
||||
.Where(static item => item.Attribute is not null)!;
|
||||
|
||||
// Validate the language version
|
||||
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp9 })
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, null));
|
||||
}
|
||||
// Filter by language version
|
||||
context.FilterWithLanguageVersion(ref methodSymbolsWithAttributeData, LanguageVersion.CSharp8, UnsupportedCSharpLanguageVersionError);
|
||||
|
||||
foreach (IGrouping<INamedTypeSymbol, SyntaxReceiver.Item>? items in syntaxReceiver.GatheredInfo.GroupBy<SyntaxReceiver.Item, INamedTypeSymbol>(static item => item.MethodSymbol.ContainingType, SymbolEqualityComparer.Default))
|
||||
{
|
||||
if (items.Key.DeclaringSyntaxReferences.Length > 0 &&
|
||||
items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration)
|
||||
// Gather info for all annotated command methods
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result<CommandInfo?> Info)> commandInfoWithErrors =
|
||||
methodSymbolsWithAttributeData
|
||||
.Select(static (item, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
OnExecute(context, classDeclaration, items.Key, items);
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.ReportDiagnostic(ICommandGeneratorError, classDeclaration, items.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HierarchyInfo hierarchy = HierarchyInfo.From(item.Symbol.ContainingType);
|
||||
CommandInfo? commandInfo = Execute.GetInfo(item.Symbol, item.Attribute, out ImmutableArray<Diagnostic> diagnostics);
|
||||
|
||||
/// <summary>
|
||||
/// Processes a given target type.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="classDeclaration">The <see cref="ClassDeclarationSyntax"/> node to process.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for <paramref name="classDeclaration"/>.</param>
|
||||
/// <param name="items">The sequence of <see cref="IMethodSymbol"/> instances to process.</param>
|
||||
private static void OnExecute(
|
||||
GeneratorExecutionContext context,
|
||||
ClassDeclarationSyntax classDeclaration,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
IEnumerable<SyntaxReceiver.Item> items)
|
||||
{
|
||||
// Create the class declaration for the user type. This will produce a tree as follows:
|
||||
//
|
||||
// <MODIFIERS> <CLASS_NAME>
|
||||
// {
|
||||
// <MEMBERS>
|
||||
// }
|
||||
ClassDeclarationSyntax? classDeclarationSyntax =
|
||||
ClassDeclaration(classDeclarationSymbol.Name)
|
||||
.WithModifiers(classDeclaration.Modifiers)
|
||||
.AddMembers(items.Select(item => CreateCommandMembers(context, classDeclarationSymbol, item.LeadingTrivia, item.MethodSymbol, item.AttributeData)).SelectMany(static g => g).ToArray());
|
||||
return (hierarchy, new Result<CommandInfo?>(commandInfo, diagnostics));
|
||||
});
|
||||
|
||||
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
|
||||
// Output the diagnostics
|
||||
context.ReportDiagnostics(commandInfoWithErrors.Select(static (item, _) => item.Info.Errors));
|
||||
|
||||
// Add all parent types in ascending order, if any
|
||||
foreach (TypeDeclarationSyntax? parentType in classDeclaration.Ancestors().OfType<TypeDeclarationSyntax>())
|
||||
// Get the filtered sequence to enable caching
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, CommandInfo Info)> commandInfo =
|
||||
commandInfoWithErrors
|
||||
.Where(static item => item.Info.Value is not null)
|
||||
.Select(static (item, _) => (item.Hierarchy, item.Info.Value!))
|
||||
.WithComparers(HierarchyInfo.Comparer.Default, CommandInfo.Comparer.Default);
|
||||
|
||||
// Generate the commands
|
||||
context.RegisterSourceOutput(commandInfo, static (context, item) =>
|
||||
{
|
||||
typeDeclarationSyntax = parentType
|
||||
.WithMembers(SingletonList<MemberDeclarationSyntax>(typeDeclarationSyntax))
|
||||
.WithConstraintClauses(List<TypeParameterConstraintClauseSyntax>())
|
||||
.WithBaseList(null)
|
||||
.WithAttributeLists(List<AttributeListSyntax>())
|
||||
.WithoutTrivia();
|
||||
}
|
||||
ImmutableArray<MemberDeclarationSyntax> memberDeclarations = Execute.GetSyntax(item.Info);
|
||||
CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit(memberDeclarations);
|
||||
|
||||
// Create the compilation unit with the namespace and target member.
|
||||
// From this, we can finally generate the source code to output.
|
||||
string? namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces));
|
||||
|
||||
// Create the final compilation unit to generate (with leading trivia)
|
||||
string? source =
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))
|
||||
.AddMembers(typeDeclarationSyntax))
|
||||
.NormalizeWhitespace()
|
||||
.ToFullString();
|
||||
|
||||
// Add the partial type
|
||||
context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="MemberDeclarationSyntax"/> instances for a specified command.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="classDeclarationSymbol">The <see cref="INamedTypeSymbol"/> for the parent type.</param>
|
||||
/// <param name="leadingTrivia">The leading trivia for the field to process.</param>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="attributeData">The <see cref="AttributeData"/> instance the method was annotated with.</param>
|
||||
/// <returns>The <see cref="MemberDeclarationSyntax"/> instances for the input command.</returns>
|
||||
private static IEnumerable<MemberDeclarationSyntax> CreateCommandMembers(
|
||||
GeneratorExecutionContext context,
|
||||
INamedTypeSymbol classDeclarationSymbol,
|
||||
SyntaxTriviaList leadingTrivia,
|
||||
IMethodSymbol methodSymbol,
|
||||
AttributeData attributeData)
|
||||
{
|
||||
// Get the command member names
|
||||
(string fieldName, string propertyName) = GetGeneratedFieldAndPropertyNames(context, methodSymbol);
|
||||
|
||||
// Get the command type symbols
|
||||
if (!TryMapCommandTypesFromMethod(
|
||||
context,
|
||||
methodSymbol,
|
||||
out INamedTypeSymbol? commandInterfaceTypeSymbol,
|
||||
out INamedTypeSymbol? commandClassTypeSymbol,
|
||||
out INamedTypeSymbol? delegateTypeSymbol))
|
||||
{
|
||||
context.ReportDiagnostic(InvalidICommandMethodSignatureError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
|
||||
|
||||
return Array.Empty<MemberDeclarationSyntax>();
|
||||
}
|
||||
|
||||
// Prepares the argument to pass the underlying method to invoke
|
||||
List<ArgumentSyntax> commandCreationArguments = new()
|
||||
{
|
||||
Argument(
|
||||
ObjectCreationExpression(IdentifierName(delegateTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
|
||||
.AddArgumentListArguments(Argument(IdentifierName(methodSymbol.Name))))
|
||||
};
|
||||
|
||||
// Get the can execute member, if any
|
||||
if (attributeData.TryGetNamedArgument("CanExecute", out string? memberName))
|
||||
{
|
||||
if (memberName is null)
|
||||
{
|
||||
context.ReportDiagnostic(InvalidCanExecuteMemberName, methodSymbol, memberName ?? string.Empty, methodSymbol.ContainingType);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImmutableArray<ISymbol> canExecuteSymbols = classDeclarationSymbol.GetMembers(memberName);
|
||||
|
||||
if (canExecuteSymbols.IsEmpty)
|
||||
{
|
||||
context.ReportDiagnostic(InvalidCanExecuteMemberName, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
else if (canExecuteSymbols.Length > 1)
|
||||
{
|
||||
context.ReportDiagnostic(MultipleCanExecuteMemberNameMatches, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
else if (TryGetCanExecuteExpressionFromSymbol(context, memberName, canExecuteSymbols[0], commandInterfaceTypeSymbol, out ExpressionSyntax? canExecuteExpression))
|
||||
{
|
||||
commandCreationArguments.Add(Argument(canExecuteExpression));
|
||||
}
|
||||
else
|
||||
{
|
||||
context.ReportDiagnostic(InvalidCanExecuteMember, methodSymbol, memberName, methodSymbol.ContainingType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the generated field as follows:
|
||||
//
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// private <COMMAND_TYPE>? <COMMAND_FIELD_NAME>;
|
||||
FieldDeclarationSyntax fieldDeclaration =
|
||||
FieldDeclaration(
|
||||
VariableDeclaration(NullableType(IdentifierName(commandClassTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))))
|
||||
.AddVariables(VariableDeclarator(Identifier(fieldName))))
|
||||
.AddModifiers(Token(SyntaxKind.PrivateKeyword))
|
||||
.AddAttributeLists(AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))));
|
||||
|
||||
SyntaxTriviaList summaryTrivia = SyntaxTriviaList.Empty;
|
||||
|
||||
// Parse the <summary> docs, if present
|
||||
foreach (SyntaxTrivia trivia in leadingTrivia)
|
||||
{
|
||||
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) ||
|
||||
trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia))
|
||||
{
|
||||
string text = trivia.ToString();
|
||||
|
||||
Match match = Regex.Match(text, @"<summary>.*?<\/summary>", RegexOptions.Singleline);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
summaryTrivia = TriviaList(Comment($"/// {match.Value}"));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the current type is an async command type and concurrent execution is disabled, pass that value to the constructor.
|
||||
// If concurrent executions are allowed, there is no need to add any additional argument, as that is the default value.
|
||||
if (commandClassTypeSymbol.Name is "AsyncRelayCommand" or "AsyncRelayCommand`1")
|
||||
{
|
||||
if (attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out bool allowConcurrentExecutions) &&
|
||||
!allowConcurrentExecutions)
|
||||
{
|
||||
commandCreationArguments.Add(Argument(LiteralExpression(SyntaxKind.FalseLiteralExpression)));
|
||||
}
|
||||
}
|
||||
else if (attributeData.TryGetNamedArgument("AllowConcurrentExecutions", out bool _))
|
||||
{
|
||||
context.ReportDiagnostic(InvalidConcurrentExecutionsParameterError, methodSymbol, methodSymbol.ContainingType, methodSymbol);
|
||||
}
|
||||
|
||||
// Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts):
|
||||
//
|
||||
// <METHOD_SUMMARY>
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// public <COMMAND_TYPE> <COMMAND_PROPERTY_NAME> => <COMMAND_FIELD_NAME> ??= new <RELAY_COMMAND_TYPE>(new <DELEGATE_TYPE>(<METHOD_NAME>), <OPTIONAL_CAN_EXECUTE>, <OPTIONAL_ALLOW_CONCURRENT_EXECUTIONS>);
|
||||
PropertyDeclarationSyntax propertyDeclaration =
|
||||
PropertyDeclaration(
|
||||
IdentifierName(commandInterfaceTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
Identifier(propertyName))
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))
|
||||
.WithOpenBracketToken(Token(summaryTrivia, SyntaxKind.OpenBracketToken, TriviaList())),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
|
||||
.WithExpressionBody(
|
||||
ArrowExpressionClause(
|
||||
AssignmentExpression(
|
||||
SyntaxKind.CoalesceAssignmentExpression,
|
||||
IdentifierName(fieldName),
|
||||
ObjectCreationExpression(IdentifierName(commandClassTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))
|
||||
.AddArgumentListArguments(commandCreationArguments.ToArray()))))
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
|
||||
|
||||
return new MemberDeclarationSyntax[] { fieldDeclaration, propertyDeclaration };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the generated field and property names for the input method.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <returns>The generated field and property names for <paramref name="methodSymbol"/>.</returns>
|
||||
private static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(GeneratorExecutionContext context, IMethodSymbol methodSymbol)
|
||||
{
|
||||
string propertyName = methodSymbol.Name;
|
||||
|
||||
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")) &&
|
||||
methodSymbol.Name.EndsWith("Async"))
|
||||
{
|
||||
propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length);
|
||||
}
|
||||
|
||||
propertyName += "Command";
|
||||
|
||||
string fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}";
|
||||
|
||||
return (fieldName, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type symbols for the input method, if supported.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="methodSymbol">The input <see cref="IMethodSymbol"/> instance to process.</param>
|
||||
/// <param name="commandInterfaceTypeSymbol">The command interface type symbol.</param>
|
||||
/// <param name="commandClassTypeSymbol">The command class type symbol.</param>
|
||||
/// <param name="delegateTypeSymbol">The delegate type symbol for the wrapped method.</param>
|
||||
/// <returns>Whether or not <paramref name="methodSymbol"/> was valid and the requested types have been set.</returns>
|
||||
private static bool TryMapCommandTypesFromMethod(
|
||||
GeneratorExecutionContext context,
|
||||
IMethodSymbol methodSymbol,
|
||||
[NotNullWhen(true)] out INamedTypeSymbol? commandInterfaceTypeSymbol,
|
||||
[NotNullWhen(true)] out INamedTypeSymbol? commandClassTypeSymbol,
|
||||
[NotNullWhen(true)] out INamedTypeSymbol? delegateTypeSymbol)
|
||||
{
|
||||
// Map <void, void> to IRelayCommand, RelayCommand, Action
|
||||
if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0)
|
||||
{
|
||||
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IRelayCommand")!;
|
||||
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommand")!;
|
||||
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action")!;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, void> to IRelayCommand<T>, RelayCommand<T>, Action<T>
|
||||
if (methodSymbol.ReturnsVoid &&
|
||||
methodSymbol.Parameters.Length == 1 &&
|
||||
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } parameter)
|
||||
{
|
||||
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IRelayCommand`1")!.Construct(parameter.Type);
|
||||
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommand`1")!.Construct(parameter.Type);
|
||||
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action`1")!.Construct(parameter.Type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!))
|
||||
{
|
||||
// Map <void, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<Task>
|
||||
if (methodSymbol.Parameters.Length == 0)
|
||||
{
|
||||
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand")!;
|
||||
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand")!;
|
||||
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`1")!.Construct(context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodSymbol.Parameters.Length == 1 &&
|
||||
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } singleParameter)
|
||||
{
|
||||
// Map <CancellationToken, Task> to IAsyncRelayCommand, AsyncRelayCommand, Func<CancellationToken, Task>
|
||||
if (SymbolEqualityComparer.Default.Equals(singleParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!))
|
||||
{
|
||||
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand")!;
|
||||
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand")!;
|
||||
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct(
|
||||
context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!,
|
||||
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, Task>
|
||||
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(singleParameter.Type);
|
||||
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(singleParameter.Type);
|
||||
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct(
|
||||
singleParameter.Type,
|
||||
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map <T, CancellationToken, Task> to IAsyncRelayCommand<T>, AsyncRelayCommand<T>, Func<T, CancellationToken, Task>
|
||||
if (methodSymbol.Parameters.Length == 2 &&
|
||||
methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } firstParameter &&
|
||||
methodSymbol.Parameters[1] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } secondParameter &&
|
||||
SymbolEqualityComparer.Default.Equals(secondParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!))
|
||||
{
|
||||
commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(firstParameter.Type);
|
||||
commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(firstParameter.Type);
|
||||
delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`3")!.Construct(
|
||||
firstParameter.Type,
|
||||
secondParameter.Type,
|
||||
context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
commandInterfaceTypeSymbol = null;
|
||||
commandClassTypeSymbol = null;
|
||||
delegateTypeSymbol = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expression for the can execute logic, if possible.
|
||||
/// </summary>
|
||||
/// <param name="context">The input <see cref="GeneratorExecutionContext"/> instance to use.</param>
|
||||
/// <param name="canExecuteMemberName">The name of the can execute member name being used.</param>
|
||||
/// <param name="canExecuteSymbol">The can execute member symbol (either a method or a property).</param>
|
||||
/// <param name="commandInterfaceTypeSymbol">The command interface type symbol.</param>
|
||||
/// <param name="canExecuteExpression">The resulting can execute expression, if available.</param>
|
||||
/// <returns>Whether or not <paramref name="canExecuteExpression"/> was set and the input symbol was valid.</returns>
|
||||
private static bool TryGetCanExecuteExpressionFromSymbol(
|
||||
GeneratorExecutionContext context,
|
||||
string canExecuteMemberName,
|
||||
ISymbol canExecuteSymbol,
|
||||
INamedTypeSymbol commandInterfaceTypeSymbol,
|
||||
[NotNullWhen(true)] out ExpressionSyntax? canExecuteExpression)
|
||||
{
|
||||
if (canExecuteSymbol is IMethodSymbol canExecuteMethodSymbol)
|
||||
{
|
||||
// The return type must always be a bool
|
||||
if (!SymbolEqualityComparer.Default.Equals(canExecuteMethodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Boolean")))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
// Parameterless methods are always valid
|
||||
if (canExecuteMethodSymbol.Parameters.IsEmpty)
|
||||
{
|
||||
// If the command is generic, the input value is ignored
|
||||
if (commandInterfaceTypeSymbol.IsGenericType)
|
||||
{
|
||||
// Create a lambda expression ignoring the input value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_METHOD>());
|
||||
canExecuteExpression =
|
||||
SimpleLambdaExpression(
|
||||
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
|
||||
.WithExpressionBody(InvocationExpression(IdentifierName(canExecuteMemberName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a method groupd expression, which will become:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, <CAN_EXECUTE_METHOD>);
|
||||
canExecuteExpression = IdentifierName(canExecuteMemberName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the method has parameters, it has to have a single one matching the command type
|
||||
if (canExecuteMethodSymbol.Parameters.Length == 1 &&
|
||||
commandInterfaceTypeSymbol.IsGenericType &&
|
||||
SymbolEqualityComparer.Default.Equals(canExecuteMethodSymbol.Parameters[0].Type, commandInterfaceTypeSymbol.TypeArguments[0]))
|
||||
{
|
||||
// Create a method groupd expression again
|
||||
canExecuteExpression = IdentifierName(canExecuteMemberName);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (canExecuteSymbol is IPropertySymbol { GetMethod: not null } canExecutePropertySymbol)
|
||||
{
|
||||
// The property type must always be a bool
|
||||
if (!SymbolEqualityComparer.Default.Equals(canExecutePropertySymbol.Type, context.Compilation.GetTypeByMetadataName("System.Boolean")))
|
||||
{
|
||||
goto Failure;
|
||||
}
|
||||
|
||||
if (commandInterfaceTypeSymbol.IsGenericType)
|
||||
{
|
||||
// Create a lambda expression again, but discarding the input value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, _ => <CAN_EXECUTE_PROPERTY>);
|
||||
canExecuteExpression =
|
||||
SimpleLambdaExpression(
|
||||
Parameter(Identifier(TriviaList(), SyntaxKind.UnderscoreToken, "_", "_", TriviaList())))
|
||||
.WithExpressionBody(IdentifierName(canExecuteMemberName));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a lambda expression returning the property value:
|
||||
//
|
||||
// new <RELAY_COMMAND_TYPE>(<METHOD_EXPRESSION>, () => <CAN_EXECUTE_PROPERTY>);
|
||||
canExecuteExpression = ParenthesizedLambdaExpression().WithExpressionBody(IdentifierName(canExecuteMemberName));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Failure:
|
||||
canExecuteExpression = null;
|
||||
|
||||
return false;
|
||||
context.AddSource(
|
||||
hintName: $"{item.Hierarchy.FilenameHint}.{item.Info.MethodName}.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
// 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.
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A type describing the type of expression for the "CanExecute" property of a command.
|
||||
/// </summary>
|
||||
public enum CanExecuteExpressionType
|
||||
{
|
||||
/// <summary>
|
||||
/// A method invocation lambda with discard: <c>_ => Method()</c>.
|
||||
/// </summary>
|
||||
MethodInvocationLambdaWithDiscard,
|
||||
|
||||
/// <summary>
|
||||
/// A property access lambda: <c>() => Property</c>.
|
||||
/// </summary>
|
||||
PropertyAccessLambda,
|
||||
|
||||
/// <summary>
|
||||
/// A property access lambda with discard: <c>_ => Property</c>.
|
||||
/// </summary>
|
||||
PropertyAccessLambdaWithDiscard,
|
||||
|
||||
/// <summary>
|
||||
/// A method group expression: <c>Method</c>.
|
||||
/// </summary>
|
||||
MethodGroup
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model with gathered info on a given command method.
|
||||
/// </summary>
|
||||
/// <param name="MethodName">The name of the target method.</param>
|
||||
/// <param name="FieldName">The resulting field name for the generated command.</param>
|
||||
/// <param name="PropertyName">The resulting property name for the generated command.</param>
|
||||
/// <param name="CommandInterfaceType">The command interface type name.</param>
|
||||
/// <param name="CommandClassType">The command class type name.</param>
|
||||
/// <param name="DelegateType">The delegate type name for the wrapped method.</param>
|
||||
/// <param name="CommandTypeArguments">The type arguments for <paramref name="CommandInterfaceType"/> and <paramref name="CommandClassType"/>, if any.</param>
|
||||
/// <param name="DelegateTypeArguments">The type arguments for <paramref name="DelegateType"/>, if any.</param>
|
||||
/// <param name="CanExecuteMemberName">The member name for the can execute check, if available.</param>
|
||||
/// <param name="CanExecuteExpressionType">The can execute expression type, if available.</param>
|
||||
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been disabled.</param>
|
||||
internal sealed record CommandInfo(
|
||||
string MethodName,
|
||||
string FieldName,
|
||||
string PropertyName,
|
||||
string CommandInterfaceType,
|
||||
string CommandClassType,
|
||||
string DelegateType,
|
||||
ImmutableArray<string> CommandTypeArguments,
|
||||
ImmutableArray<string> DelegateTypeArguments,
|
||||
string? CanExecuteMemberName,
|
||||
CanExecuteExpressionType? CanExecuteExpressionType,
|
||||
bool AllowConcurrentExecutions)
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="CommandInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<CommandInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, CommandInfo obj)
|
||||
{
|
||||
hashCode.Add(obj.MethodName);
|
||||
hashCode.Add(obj.FieldName);
|
||||
hashCode.Add(obj.PropertyName);
|
||||
hashCode.Add(obj.CommandInterfaceType);
|
||||
hashCode.Add(obj.CommandClassType);
|
||||
hashCode.Add(obj.DelegateType);
|
||||
hashCode.AddRange(obj.CommandTypeArguments);
|
||||
hashCode.AddRange(obj.DelegateTypeArguments);
|
||||
hashCode.Add(obj.CanExecuteMemberName);
|
||||
hashCode.Add(obj.CanExecuteExpressionType);
|
||||
hashCode.Add(obj.AllowConcurrentExecutions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(CommandInfo x, CommandInfo y)
|
||||
{
|
||||
return
|
||||
x.MethodName == y.MethodName &&
|
||||
x.FieldName == y.FieldName &&
|
||||
x.PropertyName == y.PropertyName &&
|
||||
x.CommandInterfaceType == y.CommandInterfaceType &&
|
||||
x.CommandClassType == y.CommandClassType &&
|
||||
x.DelegateType == y.DelegateType &&
|
||||
x.CommandTypeArguments.SequenceEqual(y.CommandTypeArguments) &&
|
||||
x.DelegateTypeArguments.SequenceEqual(y.CommandTypeArguments) &&
|
||||
x.CanExecuteMemberName == y.CanExecuteMemberName &&
|
||||
x.CanExecuteExpressionType == y.CanExecuteExpressionType &&
|
||||
x.AllowConcurrentExecutions == y.AllowConcurrentExecutions;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
// 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 CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial class IMessengerRegisterAllGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for all the logic for <see cref="IMessengerRegisterAllGenerator"/>.
|
||||
/// </summary>
|
||||
private static class Execute
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <c>IRecipient<TMessage></c> interfaces from <paramref name="typeSymbol"/>, if any.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to inspect.</param>
|
||||
/// <returns>An array of interface type symbols.</returns>
|
||||
public static ImmutableArray<INamedTypeSymbol> GetInterfaces(INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
ImmutableArray<INamedTypeSymbol>.Builder iRecipientInterfaces = ImmutableArray.CreateBuilder<INamedTypeSymbol>();
|
||||
|
||||
foreach (INamedTypeSymbol interfaceSymbol in typeSymbol.AllInterfaces)
|
||||
{
|
||||
if (interfaceSymbol.MetadataName is "IRecipient`1" &&
|
||||
interfaceSymbol.OriginalDefinition.HasFullyQualifiedName("global::CommunityToolkit.Mvvm.Messaging.IRecipient<TMessage>"))
|
||||
{
|
||||
iRecipientInterfaces.Add(interfaceSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
return iRecipientInterfaces.ToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="RecipientInfo"/> instance from the given info.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The type symbol for the target type being inspected.</param>
|
||||
/// <param name="interfaceSymbols">The input array of interface type symbols being handled.</param>
|
||||
/// <returns>A <see cref="RecipientInfo"/> instance for the current type being inspected.</returns>
|
||||
public static RecipientInfo GetInfo(INamedTypeSymbol typeSymbol, ImmutableArray<INamedTypeSymbol> interfaceSymbols)
|
||||
{
|
||||
ImmutableArray<string>.Builder names = ImmutableArray.CreateBuilder<string>(interfaceSymbols.Length);
|
||||
|
||||
foreach (INamedTypeSymbol interfaceSymbol in interfaceSymbols)
|
||||
{
|
||||
names.Add(interfaceSymbol.TypeArguments[0].GetFullyQualifiedName());
|
||||
}
|
||||
|
||||
return new(
|
||||
typeSymbol.GetFullMetadataNameForFileName(),
|
||||
typeSymbol.GetFullyQualifiedName(),
|
||||
names.MoveToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the head <see cref="CompilationUnitSyntax"/> instance.
|
||||
/// </summary>
|
||||
/// <returns>The head <see cref="CompilationUnitSyntax"/> instance with the type attributes.</returns>
|
||||
public static CompilationUnitSyntax GetSyntax()
|
||||
{
|
||||
// This code produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
// #pragma warning disable
|
||||
// namespace CommunityToolkit.Mvvm.Messaging.__Internals
|
||||
// {
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
||||
// internal static partial class __IMessengerExtensions
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__IMessengerExtensions").AddModifiers(
|
||||
Token(SyntaxKind.InternalKeyword),
|
||||
Token(SyntaxKind.StaticKeyword),
|
||||
Token(SyntaxKind.PartialKeyword))
|
||||
.AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(IMessengerRegisterAllGenerator).FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(IMessengerRegisterAllGenerator).Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This type is not intended to be used directly by user code")))))))))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="CompilationUnitSyntax"/> instance for the input recipient.
|
||||
/// </summary>
|
||||
/// <param name="recipientInfo">The input <see cref="RecipientInfo"/> instance to process.</param>
|
||||
/// <returns>The generated <see cref="CompilationUnitSyntax"/> instance for <paramref name="recipientInfo"/>.</returns>
|
||||
public static CompilationUnitSyntax GetSyntax(RecipientInfo recipientInfo)
|
||||
{
|
||||
// Create a static factory method to register all messages for a given recipient type.
|
||||
// This follows the same pattern used in ObservableValidatorValidateAllPropertiesGenerator,
|
||||
// with the same advantages mentioned there (type safety, more AOT-friendly, etc.).
|
||||
// This is the first overload being generated: a non-generic method doing the registration
|
||||
// with no tokens, which is the most common scenario and will help particularly with AOT.
|
||||
// This code will produce a syntax tree as follows:
|
||||
//
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This method is not intended to be called directly by user code")]
|
||||
// public static global::System.Action<global::CommunityToolkit.Mvvm.Messaging.IMessenger, object> CreateAllMessagesRegistrator(<RECIPIENT_TYPE> _)
|
||||
// {
|
||||
// static void RegisterAll(global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, object obj)
|
||||
// {
|
||||
// var recipient = (<INSTANCE_TYPE>)obj;
|
||||
// <BODY>
|
||||
// }
|
||||
//
|
||||
// return RegisterAll;
|
||||
// }
|
||||
MethodDeclarationSyntax defaultChannelMethodDeclaration =
|
||||
MethodDeclaration(
|
||||
GenericName("global::System.Action").AddTypeArgumentListArguments(
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger"),
|
||||
PredefinedType(Token(SyntaxKind.ObjectKeyword))),
|
||||
Identifier("CreateAllMessagesRegistrator")).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
|
||||
Parameter(Identifier("_")).WithType(IdentifierName(recipientInfo.TypeName)))
|
||||
.WithBody(Block(
|
||||
LocalFunctionStatement(
|
||||
PredefinedType(Token(SyntaxKind.VoidKeyword)),
|
||||
Identifier("RegisterAll"))
|
||||
.AddModifiers(Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(
|
||||
Parameter(Identifier("messenger")).WithType(IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger")),
|
||||
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))))
|
||||
.WithBody(Block(
|
||||
LocalDeclarationStatement(
|
||||
VariableDeclaration(IdentifierName("var"))
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier("recipient"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
CastExpression(
|
||||
IdentifierName(recipientInfo.TypeName),
|
||||
IdentifierName("obj")))))))
|
||||
.AddStatements(EnumerateRegistrationStatements(recipientInfo).ToArray())),
|
||||
ReturnStatement(IdentifierName("RegisterAll"))));
|
||||
|
||||
// Create a generic version that will support all other cases with custom tokens.
|
||||
// Note: the generic overload has a different name to simplify the lookup with reflection.
|
||||
// This code will produce a syntax tree as follows:
|
||||
//
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This method is not intended to be called directly by user code")]
|
||||
// public static global::System.Action<global::CommunityToolkit.Mvvm.Messaging.IMessenger, object, TToken> CreateAllMessagesRegistratorWithToken<TToken>(<RECIPIENT_TYPE> _)
|
||||
// where TToken : global::System.IEquatable<TToken>
|
||||
// {
|
||||
// static void RegisterAll(global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, object obj, TToken token)
|
||||
// {
|
||||
// var recipient = (<INSTANCE_TYPE>)obj;
|
||||
// <BODY>
|
||||
// }
|
||||
//
|
||||
// return RegisterAll;
|
||||
// }
|
||||
MethodDeclarationSyntax customChannelMethodDeclaration =
|
||||
MethodDeclaration(
|
||||
GenericName("global::System.Action").AddTypeArgumentListArguments(
|
||||
IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger"),
|
||||
PredefinedType(Token(SyntaxKind.ObjectKeyword)),
|
||||
IdentifierName("TToken")),
|
||||
Identifier("CreateAllMessagesRegistratorWithToken")).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
|
||||
Parameter(Identifier("_")).WithType(IdentifierName(recipientInfo.TypeName)))
|
||||
.AddTypeParameterListParameters(TypeParameter("TToken"))
|
||||
.AddConstraintClauses(
|
||||
TypeParameterConstraintClause("TToken")
|
||||
.AddConstraints(TypeConstraint(GenericName("global::System.IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken")))))
|
||||
.WithBody(Block(
|
||||
LocalFunctionStatement(
|
||||
PredefinedType(Token(SyntaxKind.VoidKeyword)),
|
||||
Identifier("RegisterAll"))
|
||||
.AddModifiers(Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(
|
||||
Parameter(Identifier("messenger")).WithType(IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger")),
|
||||
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))),
|
||||
Parameter(Identifier("token")).WithType(IdentifierName("TToken")))
|
||||
.WithBody(Block(
|
||||
LocalDeclarationStatement(
|
||||
VariableDeclaration(IdentifierName("var"))
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier("recipient"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
CastExpression(
|
||||
IdentifierName(recipientInfo.TypeName),
|
||||
IdentifierName("obj")))))))
|
||||
.AddStatements(EnumerateRegistrationStatementsWithTokens(recipientInfo).ToArray())),
|
||||
ReturnStatement(IdentifierName("RegisterAll"))));
|
||||
|
||||
// This code produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
// #pragma warning disable
|
||||
// namespace CommunityToolkit.Mvvm.Messaging.__Internals
|
||||
// {
|
||||
// partial class __IMessengerExtensions
|
||||
// {
|
||||
// <GENERATED_MEMBERS>
|
||||
// }
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__IMessengerExtensions").AddModifiers(Token(SyntaxKind.PartialKeyword))
|
||||
.AddMembers(defaultChannelMethodDeclaration, customChannelMethodDeclaration)))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sequence of statements to register declared message handlers.
|
||||
/// </summary>
|
||||
/// <param name="recipientInfo">The input <see cref="RecipientInfo"/> instance to process.</param>
|
||||
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to register message handlers.</returns>
|
||||
private static ImmutableArray<StatementSyntax> EnumerateRegistrationStatements(RecipientInfo recipientInfo)
|
||||
{
|
||||
ImmutableArray<StatementSyntax>.Builder statements = ImmutableArray.CreateBuilder<StatementSyntax>(recipientInfo.MessageTypes.Length);
|
||||
|
||||
// This loop produces a sequence of statements as follows:
|
||||
//
|
||||
// messenger.Register<<TYPE_0>>(recipient);
|
||||
// messenger.Register<<TYPE_1>>(recipient);
|
||||
// ...
|
||||
// messenger.Register<<TYPE_N>>(recipient);
|
||||
foreach (string messageType in recipientInfo.MessageTypes)
|
||||
{
|
||||
statements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("messenger"),
|
||||
GenericName(Identifier("Register"))
|
||||
.AddTypeArgumentListArguments(IdentifierName(messageType))))
|
||||
.AddArgumentListArguments(Argument(IdentifierName("recipient")))));
|
||||
}
|
||||
|
||||
return statements.MoveToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sequence of statements to register declared message handlers with a custom token.
|
||||
/// </summary>
|
||||
/// <param name="recipientInfo">The input <see cref="RecipientInfo"/> instance to process.</param>
|
||||
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to register message handlers.</returns>
|
||||
private static ImmutableArray<StatementSyntax> EnumerateRegistrationStatementsWithTokens(RecipientInfo recipientInfo)
|
||||
{
|
||||
ImmutableArray<StatementSyntax>.Builder statements = ImmutableArray.CreateBuilder<StatementSyntax>(recipientInfo.MessageTypes.Length);
|
||||
|
||||
// This loop produces a sequence of statements as follows:
|
||||
//
|
||||
// messenger.Register<<TYPE_0>, TToken>(recipient, token);
|
||||
// messenger.Register<<TYPE_1>, TToken>(recipient, token);
|
||||
// ...
|
||||
// messenger.Register<<TYPE_N>, TToken>(recipient, token);
|
||||
foreach (string messageType in recipientInfo.MessageTypes)
|
||||
{
|
||||
statements.Add(
|
||||
ExpressionStatement(
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("messenger"),
|
||||
GenericName(Identifier("Register"))
|
||||
.AddTypeArgumentListArguments(IdentifierName(messageType), IdentifierName("TToken"))))
|
||||
.AddArgumentListArguments(Argument(IdentifierName("recipient")), Argument(IdentifierName("token")))));
|
||||
}
|
||||
|
||||
return statements.MoveToImmutable();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// 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.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <inheritdoc cref="IMessengerRegisterAllGenerator"/>
|
||||
public sealed partial class IMessengerRegisterAllGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISyntaxContextReceiver"/> that selects candidate nodes to process.
|
||||
/// </summary>
|
||||
private sealed class SyntaxReceiver : ISyntaxContextReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of info gathered during exploration.
|
||||
/// </summary>
|
||||
private readonly List<INamedTypeSymbol> gatheredInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of gathered info to process.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<INamedTypeSymbol> GatheredInfo => this.gatheredInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
|
||||
{
|
||||
if (context.Node is ClassDeclarationSyntax classDeclaration &&
|
||||
context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol { IsGenericType: false } classSymbol &&
|
||||
context.SemanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Messaging.IRecipient`1") is INamedTypeSymbol iRecipientSymbol &&
|
||||
classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iRecipientSymbol)))
|
||||
{
|
||||
this.gatheredInfo.Add(classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,282 +2,69 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators;
|
||||
|
||||
/// <summary>
|
||||
/// A source generator for message registration without relying on compiled LINQ expressions.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed partial class IMessengerRegisterAllGenerator : ISourceGenerator
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed partial class IMessengerRegisterAllGenerator : IIncrementalGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver());
|
||||
}
|
||||
// Get all class declarations
|
||||
IncrementalValuesProvider<INamedTypeSymbol> typeSymbols =
|
||||
context.SyntaxProvider
|
||||
.CreateSyntaxProvider(
|
||||
static (node, _) => node is ClassDeclarationSyntax,
|
||||
static (context, _) => (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node)!);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
{
|
||||
// Get the syntax receiver with the candidate nodes
|
||||
if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver ||
|
||||
syntaxReceiver.GatheredInfo.Count == 0)
|
||||
// Get the target IRecipient<TMessage> interfaces and filter out other types
|
||||
IncrementalValuesProvider<(INamedTypeSymbol Type, ImmutableArray<INamedTypeSymbol> Interfaces)> typeAndInterfaceSymbols =
|
||||
typeSymbols
|
||||
.Select(static (item, _) => (item, Interfaces: Execute.GetInterfaces(item)))
|
||||
.Where(static item => !item.Interfaces.IsEmpty);
|
||||
|
||||
// Get the recipient info for all target types
|
||||
IncrementalValuesProvider<RecipientInfo> recipientInfo =
|
||||
typeAndInterfaceSymbols
|
||||
.Select(static (item, _) => Execute.GetInfo(item.Type, item.Interfaces))
|
||||
.WithComparer(RecipientInfo.Comparer.Default);
|
||||
|
||||
// Check whether the header file is needed
|
||||
IncrementalValueProvider<bool> isHeaderFileNeeded =
|
||||
recipientInfo
|
||||
.Collect()
|
||||
.Select(static (item, _) => item.Length > 0);
|
||||
|
||||
// Generate the header file with the attributes
|
||||
context.RegisterImplementationSourceOutput(isHeaderFileNeeded, static (context, item) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
CompilationUnitSyntax compilationUnit = Execute.GetSyntax();
|
||||
|
||||
// Like in the ObservableValidator.ValidateALlProperties generator, execution is skipped if C# >= 8.0 isn't available
|
||||
if (context.ParseOptions is not CSharpParseOptions { LanguageVersion: >= LanguageVersion.CSharp8 })
|
||||
context.AddSource(
|
||||
hintName: "__IMessengerExtensions.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
|
||||
// Generate the class with all registration methods
|
||||
context.RegisterImplementationSourceOutput(recipientInfo, static (context, item) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
CompilationUnitSyntax compilationUnit = Execute.GetSyntax(item);
|
||||
|
||||
// Get the symbol for the IRecipient<T> interface type
|
||||
INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Messaging.IRecipient`1")!;
|
||||
|
||||
// Prepare the attributes to add to the first class declaration
|
||||
AttributeListSyntax[] classAttributes = new[]
|
||||
{
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode"))
|
||||
.AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))),
|
||||
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))),
|
||||
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This type is not intended to be used directly by user code"))))))
|
||||
};
|
||||
|
||||
foreach (INamedTypeSymbol classSymbol in syntaxReceiver.GatheredInfo)
|
||||
{
|
||||
// Create a static factory method to register all messages for a given recipient type.
|
||||
// This follows the same pattern used in ObservableValidatorValidateAllPropertiesGenerator,
|
||||
// with the same advantages mentioned there (type safety, more AOT-friendly, etc.).
|
||||
// There are two versions that are generated: a non-generic one doing the registration
|
||||
// with no tokens, which is the most common scenario and will help particularly in AOT
|
||||
// scenarios, and a generic version that will support all other cases with custom tokens.
|
||||
// Note: the generic overload has a different name to simplify the lookup with reflection.
|
||||
// This code takes a class symbol and produces a compilation unit as follows:
|
||||
//
|
||||
// // <auto-generated/>
|
||||
//
|
||||
// #pragma warning disable
|
||||
//
|
||||
// namespace CommunityToolkit.Mvvm.Messaging.__Internals
|
||||
// {
|
||||
// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
|
||||
// [global::System.Diagnostics.DebuggerNonUserCode]
|
||||
// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This type is not intended to be used directly by user code")]
|
||||
// internal static partial class __IMessengerExtensions
|
||||
// {
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This method is not intended to be called directly by user code")]
|
||||
// public static global::System.Action<IMessenger, object> CreateAllMessagesRegistrator(<RECIPIENT_TYPE> _)
|
||||
// {
|
||||
// static void RegisterAll(IMessenger messenger, object obj)
|
||||
// {
|
||||
// var recipient = (<INSTANCE_TYPE>)obj;
|
||||
// <BODY>
|
||||
// }
|
||||
//
|
||||
// return RegisterAll;
|
||||
// }
|
||||
//
|
||||
// [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
|
||||
// [global::System.Obsolete("This method is not intended to be called directly by user code")]
|
||||
// public static global::System.Action<IMessenger, object, TToken> CreateAllMessagesRegistratorWithToken<TToken>(<RECIPIENT_TYPE> _)
|
||||
// where TToken : global::System.IEquatable<TToken>
|
||||
// {
|
||||
// static void RegisterAll(IMessenger messenger, object obj, TToken token)
|
||||
// {
|
||||
// var recipient = (<INSTANCE_TYPE>)obj;
|
||||
// <BODY>
|
||||
// }
|
||||
//
|
||||
// return RegisterAll;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
string? source =
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging.__Internals")).WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers(
|
||||
ClassDeclaration("__IMessengerExtensions").AddModifiers(
|
||||
Token(SyntaxKind.InternalKeyword),
|
||||
Token(SyntaxKind.StaticKeyword),
|
||||
Token(SyntaxKind.PartialKeyword)).AddAttributeLists(classAttributes).AddMembers(
|
||||
MethodDeclaration(
|
||||
GenericName("global::System.Action").AddTypeArgumentListArguments(
|
||||
IdentifierName("IMessenger"),
|
||||
PredefinedType(Token(SyntaxKind.ObjectKeyword))),
|
||||
Identifier("CreateAllMessagesRegistrator")).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
|
||||
Parameter(Identifier("_")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))))
|
||||
.WithBody(Block(
|
||||
LocalFunctionStatement(
|
||||
PredefinedType(Token(SyntaxKind.VoidKeyword)),
|
||||
Identifier("RegisterAll"))
|
||||
.AddModifiers(Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(
|
||||
Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")),
|
||||
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))))
|
||||
.WithBody(Block(
|
||||
LocalDeclarationStatement(
|
||||
VariableDeclaration(IdentifierName("var"))
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier("recipient"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
CastExpression(
|
||||
IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
IdentifierName("obj")))))))
|
||||
.AddStatements(EnumerateRegistrationStatements(classSymbol, iRecipientSymbol).ToArray())),
|
||||
ReturnStatement(IdentifierName("RegisterAll")))),
|
||||
MethodDeclaration(
|
||||
GenericName("global::System.Action").AddTypeArgumentListArguments(
|
||||
IdentifierName("IMessenger"),
|
||||
PredefinedType(Token(SyntaxKind.ObjectKeyword)),
|
||||
IdentifierName("TToken")),
|
||||
Identifier("CreateAllMessagesRegistratorWithToken")).AddAttributeLists(
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments(
|
||||
AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))),
|
||||
AttributeList(SingletonSeparatedList(
|
||||
Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments(
|
||||
AttributeArgument(LiteralExpression(
|
||||
SyntaxKind.StringLiteralExpression,
|
||||
Literal("This method is not intended to be called directly by user code"))))))).AddModifiers(
|
||||
Token(SyntaxKind.PublicKeyword),
|
||||
Token(SyntaxKind.StaticKeyword)).AddParameterListParameters(
|
||||
Parameter(Identifier("_")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))))
|
||||
.AddTypeParameterListParameters(TypeParameter("TToken"))
|
||||
.AddConstraintClauses(
|
||||
TypeParameterConstraintClause("TToken")
|
||||
.AddConstraints(TypeConstraint(GenericName("global::System.IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken")))))
|
||||
.WithBody(Block(
|
||||
LocalFunctionStatement(
|
||||
PredefinedType(Token(SyntaxKind.VoidKeyword)),
|
||||
Identifier("RegisterAll"))
|
||||
.AddModifiers(Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(
|
||||
Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")),
|
||||
Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))),
|
||||
Parameter(Identifier("token")).WithType(IdentifierName("TToken")))
|
||||
.WithBody(Block(
|
||||
LocalDeclarationStatement(
|
||||
VariableDeclaration(IdentifierName("var"))
|
||||
.AddVariables(
|
||||
VariableDeclarator(Identifier("recipient"))
|
||||
.WithInitializer(EqualsValueClause(
|
||||
CastExpression(
|
||||
IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
IdentifierName("obj")))))))
|
||||
.AddStatements(EnumerateRegistrationStatementsWithTokens(classSymbol, iRecipientSymbol).ToArray())),
|
||||
ReturnStatement(IdentifierName("RegisterAll")))))))
|
||||
.NormalizeWhitespace()
|
||||
.ToFullString();
|
||||
|
||||
// Reset the attributes list (so the same class doesn't get duplicate attributes)
|
||||
classAttributes = Array.Empty<AttributeListSyntax>();
|
||||
|
||||
// Add the partial type
|
||||
context.AddSource($"{classSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sequence of statements to register declared message handlers.
|
||||
/// </summary>
|
||||
/// <param name="classSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
|
||||
/// <param name="iRecipientSymbol">The type symbol for the <c>IRecipient<T></c> interface.</param>
|
||||
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to register message handleers.</returns>
|
||||
private static IEnumerable<StatementSyntax> EnumerateRegistrationStatements(INamedTypeSymbol classSymbol, INamedTypeSymbol iRecipientSymbol)
|
||||
{
|
||||
foreach (INamedTypeSymbol? interfaceSymbol in classSymbol.AllInterfaces)
|
||||
{
|
||||
if (!SymbolEqualityComparer.Default.Equals(interfaceSymbol.OriginalDefinition, iRecipientSymbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// This enumerator produces a sequence of statements as follows:
|
||||
//
|
||||
// messenger.Register<<TYPE_0>>(recipient);
|
||||
// messenger.Register<<TYPE_1>>(recipient);
|
||||
// ...
|
||||
// messenger.Register<<TYPE_N>>(recipient);
|
||||
yield return
|
||||
ExpressionStatement(
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("messenger"),
|
||||
GenericName(Identifier("Register")).AddTypeArgumentListArguments(
|
||||
IdentifierName(interfaceSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))))
|
||||
.AddArgumentListArguments(Argument(IdentifierName("recipient"))));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a sequence of statements to register declared message handlers with custom tokens.
|
||||
/// </summary>
|
||||
/// <param name="classSymbol">The input <see cref="INamedTypeSymbol"/> instance to process.</param>
|
||||
/// <param name="iRecipientSymbol">The type symbol for the <c>IRecipient<T></c> interface.</param>
|
||||
/// <returns>The sequence of <see cref="StatementSyntax"/> instances to register message handleers.</returns>
|
||||
private static IEnumerable<StatementSyntax> EnumerateRegistrationStatementsWithTokens(INamedTypeSymbol classSymbol, INamedTypeSymbol iRecipientSymbol)
|
||||
{
|
||||
foreach (INamedTypeSymbol? interfaceSymbol in classSymbol.AllInterfaces)
|
||||
{
|
||||
if (!SymbolEqualityComparer.Default.Equals(interfaceSymbol.OriginalDefinition, iRecipientSymbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// This enumerator produces a sequence of statements as follows:
|
||||
//
|
||||
// messenger.Register<<TYPE_0>, TToken>(recipient, token);
|
||||
// messenger.Register<<TYPE_1>, TToken>(recipient, token);
|
||||
// ...
|
||||
// messenger.Register<<TYPE_N>, TToken>(recipient, token);
|
||||
yield return
|
||||
ExpressionStatement(
|
||||
InvocationExpression(
|
||||
MemberAccessExpression(
|
||||
SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("messenger"),
|
||||
GenericName(Identifier("Register")).AddTypeArgumentListArguments(
|
||||
IdentifierName(interfaceSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||
IdentifierName("TToken"))))
|
||||
.AddArgumentListArguments(Argument(IdentifierName("recipient")), Argument(IdentifierName("token"))));
|
||||
}
|
||||
context.AddSource(
|
||||
hintName: $"{item.FilenameHint}.cs",
|
||||
sourceText: SourceText.From(compilationUnit.ToFullString(), Encoding.UTF8));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model with gathered info on all message types being handled by a recipient.
|
||||
/// </summary>
|
||||
/// <param name="FilenameHint">The filename hint for the current type.</param>
|
||||
/// <param name="TypeName">The fully qualified type name of the target type.</param>
|
||||
/// <param name="MessageTypes">The name of messages being received.</param>
|
||||
internal sealed record RecipientInfo(
|
||||
string FilenameHint,
|
||||
string TypeName,
|
||||
ImmutableArray<string> MessageTypes)
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="RecipientInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<RecipientInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, RecipientInfo obj)
|
||||
{
|
||||
hashCode.Add(obj.FilenameHint);
|
||||
hashCode.Add(obj.TypeName);
|
||||
hashCode.AddRange(obj.MessageTypes);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(RecipientInfo x, RecipientInfo y)
|
||||
{
|
||||
return
|
||||
x.FilenameHint == y.FilenameHint &&
|
||||
x.TypeName == y.TypeName &&
|
||||
x.MessageTypes.SequenceEqual(y.MessageTypes);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
// 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.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
|
||||
/// <inheritdoc/>
|
||||
partial record HierarchyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="CompilationUnitSyntax"/> instance wrapping the given members.
|
||||
/// </summary>
|
||||
/// <param name="memberDeclarations">The input <see cref="MemberDeclarationSyntax"/> instances to use.</param>
|
||||
/// <param name="baseList">The optional <see cref="BaseListSyntax"/> instance to add to generated types.</param>
|
||||
/// <returns>A <see cref="CompilationUnitSyntax"/> object wrapping <paramref name="memberDeclarations"/>.</returns>
|
||||
public CompilationUnitSyntax GetCompilationUnit(
|
||||
ImmutableArray<MemberDeclarationSyntax> memberDeclarations,
|
||||
BaseListSyntax? baseList = null)
|
||||
{
|
||||
// Create the partial type declaration with the given member declarations.
|
||||
// This code produces a class declaration as follows:
|
||||
//
|
||||
// partial class <TYPE_NAME>
|
||||
// {
|
||||
// <MEMBERS>
|
||||
// }
|
||||
ClassDeclarationSyntax classDeclarationSyntax =
|
||||
ClassDeclaration(Names[0])
|
||||
.AddModifiers(Token(SyntaxKind.PartialKeyword))
|
||||
.AddMembers(memberDeclarations.ToArray());
|
||||
|
||||
// Add the base list, if present
|
||||
if (baseList is not null)
|
||||
{
|
||||
classDeclarationSyntax = classDeclarationSyntax.WithBaseList(baseList);
|
||||
}
|
||||
|
||||
TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax;
|
||||
|
||||
// Add all parent types in ascending order, if any
|
||||
foreach (string parentType in Names.AsSpan().Slice(1))
|
||||
{
|
||||
typeDeclarationSyntax =
|
||||
ClassDeclaration(parentType)
|
||||
.AddModifiers(Token(SyntaxKind.PartialKeyword))
|
||||
.AddMembers(typeDeclarationSyntax);
|
||||
}
|
||||
|
||||
// Create the compilation unit with disabled warnings, target namespace and generated type.
|
||||
// This will produce code as follows:
|
||||
//
|
||||
// <auto-generated/>
|
||||
// #pragma warning disable
|
||||
// #nullable enable
|
||||
// namespace <NAMESPACE>
|
||||
// {
|
||||
// <TYPE_HIERARCHY>
|
||||
// }
|
||||
return
|
||||
CompilationUnit().AddMembers(
|
||||
NamespaceDeclaration(IdentifierName(Namespace))
|
||||
.WithLeadingTrivia(TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)),
|
||||
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))))
|
||||
.AddMembers(typeDeclarationSyntax))
|
||||
.NormalizeWhitespace(eol: "\n");
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
|
||||
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model describing the hierarchy info for a specific type.
|
||||
/// </summary>
|
||||
/// <param name="FilenameHint">The filename hint for the current type.</param>
|
||||
/// <param name="MetadataName">The metadata name for the current type.</param>
|
||||
/// <param name="Namespace">Gets the namespace for the current type.</param>
|
||||
/// <param name="Names">Gets the sequence of type definitions containing the current type.</param>
|
||||
internal sealed partial record HierarchyInfo(string FilenameHint, string MetadataName, string Namespace, ImmutableArray<string> Names)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HierarchyInfo"/> instance from a given <see cref="INamedTypeSymbol"/>.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The input <see cref="INamedTypeSymbol"/> instance to gather info for.</param>
|
||||
/// <returns>A <see cref="HierarchyInfo"/> instance describing <paramref name="typeSymbol"/>.</returns>
|
||||
public static HierarchyInfo From(INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
ImmutableArray<string>.Builder names = ImmutableArray.CreateBuilder<string>();
|
||||
|
||||
for (INamedTypeSymbol? parent = typeSymbol;
|
||||
parent is not null;
|
||||
parent = parent.ContainingType)
|
||||
{
|
||||
names.Add(parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
|
||||
}
|
||||
|
||||
return new(
|
||||
typeSymbol.GetFullMetadataNameForFileName(),
|
||||
typeSymbol.MetadataName,
|
||||
typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)),
|
||||
names.ToImmutable());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="IEqualityComparer{T}"/> implementation for <see cref="HierarchyInfo"/>.
|
||||
/// </summary>
|
||||
public sealed class Comparer : Comparer<HierarchyInfo, Comparer>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override void AddToHashCode(ref HashCode hashCode, HierarchyInfo obj)
|
||||
{
|
||||
hashCode.Add(obj.FilenameHint);
|
||||
hashCode.Add(obj.MetadataName);
|
||||
hashCode.Add(obj.Namespace);
|
||||
hashCode.AddRange(obj.Names);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool AreEqual(HierarchyInfo x, HierarchyInfo y)
|
||||
{
|
||||
return
|
||||
x.FilenameHint == y.FilenameHint &&
|
||||
x.MetadataName == y.MetadataName &&
|
||||
x.Namespace == y.Namespace &&
|
||||
x.Names.SequenceEqual(y.Names);
|
||||
}
|
||||
}
|
||||
}
|
19
CommunityToolkit.Mvvm.SourceGenerators/Models/Result.cs
Normal file
19
CommunityToolkit.Mvvm.SourceGenerators/Models/Result.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.
|
||||
|
||||
// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp),
|
||||
// more info in ThirdPartyNotices.txt in the root of the project.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace CommunityToolkit.Mvvm.SourceGenerators.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A model representing a value and an associated set of diagnostic errors.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the wrapped value.</typeparam>
|
||||
/// <param name="Value">The wrapped value for the current result.</param>
|
||||
/// <param name="Errors">The associated diagnostic errors, if any.</param>
|
||||
internal sealed record Result<TValue>(TValue Value, ImmutableArray<Diagnostic> Errors);
|
@ -18,7 +18,6 @@
|
||||
- Ioc: a helper class to configure dependency injection service containers.
|
||||
</Description>
|
||||
<PackageTags>MVVM;Toolkit;MVVMToolkit;INotifyPropertyChanged;Observable;IOC;DI;Dependency Injection;Object Messaging;Extensions;Helpers</PackageTags>
|
||||
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CopyAnalyzerProjectReferencesToPackage</TargetsForTfmSpecificContentInPackage>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- .NET Standard 2.0 doesn't have the Span<T> and IAsyncEnumerable<T> types -->
|
||||
@ -36,22 +35,23 @@
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Source generator project reference for packing -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.csproj" PrivateAssets="all" />
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<ProjectReference Include="..\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.csproj" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Target to pack the source generator into the "analyzers\dotnet\cs" package folder.
|
||||
The condition is set to .NET Standard 2.0 so that the analyzer is only added to the package
|
||||
when that target is being built, instead of once for all three targets, which would fail.
|
||||
It will still be available for all targets anyway though, as analyzers don't have a target.
|
||||
-->
|
||||
<Target Name="CopyAnalyzerProjectReferencesToPackage" DependsOnTargets="BuildOnlySettings;ResolveReferences" Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<ItemGroup>
|
||||
<TfmSpecificPackageFile Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))">
|
||||
<PackagePath>analyzers\dotnet\cs</PackagePath>
|
||||
</TfmSpecificPackageFile>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
<ItemGroup Label="Package">
|
||||
|
||||
<!-- Include the custom .targets file to check the source generator (.NET 6 is not needed as it guarantees Roslyn 4.x) -->
|
||||
<None Include="CommunityToolkit.Mvvm.targets" PackagePath="buildTransitive\netstandard2.0" Pack="true" />
|
||||
<None Include="CommunityToolkit.Mvvm.targets" PackagePath="buildTransitive\netstandard2.1" Pack="true" />
|
||||
<None Include="CommunityToolkit.Mvvm.targets" PackagePath="build\netstandard2.0" Pack="true" />
|
||||
<None Include="CommunityToolkit.Mvvm.targets" PackagePath="build\netstandard2.1" Pack="true" />
|
||||
|
||||
<!-- Pack the source generator to the right package folder -->
|
||||
<None Include="..\CommunityToolkit.Mvvm.SourceGenerators\bin\$(Configuration)\netstandard2.0\CommunityToolkit.Mvvm.SourceGenerators.dll"
|
||||
PackagePath="analyzers\dotnet\roslyn4.0\cs"
|
||||
Pack="true"
|
||||
Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
21
CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets
Normal file
21
CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets
Normal file
@ -0,0 +1,21 @@
|
||||
<Project>
|
||||
|
||||
<!-- Get the analyzer from the CommunityToolkit.Mvvm NuGet package -->
|
||||
<Target Name="_MVVMToolkitGatherAnalyzers">
|
||||
<ItemGroup>
|
||||
<_MVVMToolkitAnalyzer Include="@(Analyzer)" Condition="'%(Analyzer.NuGetPackageId)' == 'CommunityToolkit.Mvvm'" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- Remove the analyzer if using Roslyn 3.x (incremental generators require Roslyn 4.x) -->
|
||||
<Target Name="_MVVMToolkitRemoveAnalyzersForRoslyn3"
|
||||
Condition="'$(SupportsRoslynComponentVersioning)' != 'true'"
|
||||
AfterTargets="ResolvePackageDependenciesForBuild;ResolveNuGetPackageAssets"
|
||||
DependsOnTargets="_MVVMToolkitGatherAnalyzers">
|
||||
<ItemGroup>
|
||||
<Analyzer Remove="@(_MVVMToolkitAnalyzer)"/>
|
||||
</ItemGroup>
|
||||
<Warning Text="The MVVM Toolkit source generators have been disabled on the current configuration, as they need Roslyn 4.x in order to work. The MVVM Toolkit will work just fine, but features relying on the source generators will not be available."/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
@ -38,7 +38,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel;
|
||||
///
|
||||
/// public bool IsEnabled
|
||||
/// {
|
||||
/// get => name;
|
||||
/// get => isEnabled;
|
||||
/// set => SetProperty(ref isEnabled, value);
|
||||
/// }
|
||||
/// }
|
||||
|
@ -377,7 +377,7 @@ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? task
|
||||
// instance. This will result in no further allocations after the first time this method is called for a given
|
||||
// generic type. We only pay the cost of the virtual call to the delegate, but this is not performance critical
|
||||
// code and that overhead would still be much lower than the rest of the method anyway, so that's fine.
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, static _ => { }, propertyName);
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -441,7 +441,7 @@ protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? task
|
||||
/// </remarks>
|
||||
protected bool SetPropertyAndNotifyOnCompletion<T>([NotNull] ref TaskNotifier<T>? taskNotifier, Task<T>? newValue, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, static _ => { }, propertyName);
|
||||
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier<T>(), newValue, null, propertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -476,10 +476,10 @@ protected bool SetPropertyAndNotifyOnCompletion<T>([NotNull] ref TaskNotifier<T>
|
||||
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
|
||||
/// <param name="taskNotifier">The field notifier.</param>
|
||||
/// <param name="newValue">The property's value after the change occurred.</param>
|
||||
/// <param name="callback">A callback to invoke to update the property value.</param>
|
||||
/// <param name="callback">(optional) A callback to invoke to update the property value.</param>
|
||||
/// <param name="propertyName">(optional) The name of the property that changed.</param>
|
||||
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?> callback, [CallerMemberName] string? propertyName = null)
|
||||
private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNotifier, TTask? newValue, Action<TTask?>? callback, [CallerMemberName] string? propertyName = null)
|
||||
where TTask : Task
|
||||
{
|
||||
if (ReferenceEquals(taskNotifier.Task, newValue))
|
||||
@ -507,7 +507,10 @@ private bool SetPropertyAndNotifyOnCompletion<TTask>(ITaskNotifier<TTask> taskNo
|
||||
// This mirrors the return value of all the other synchronous Set methods as well.
|
||||
if (isAlreadyCompletedOrNull)
|
||||
{
|
||||
callback(newValue);
|
||||
if (callback is not null)
|
||||
{
|
||||
callback(newValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -537,7 +540,10 @@ async void MonitorTask()
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
callback(newValue);
|
||||
if (callback is not null)
|
||||
{
|
||||
callback(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
MonitorTask();
|
||||
|
@ -11,6 +11,7 @@ This project incorporates components from the projects listed below. The origina
|
||||
4. PrivateObject/PrivateType (https://github.com/microsoft/testfx/tree/664ac7c2ac9dbfbee9d2a0ef560cfd72449dfe34/src/TestFramework/Extension.Desktop), included in UnitTests.
|
||||
5. QuinnDamerell/UniversalMarkdown (https://github.com/QuinnDamerell/UniversalMarkdown) contributed by Quinn Damerell and Paul Bartrum for the MarkdownTextBlock control, relicensed to this .NET Foundation project under the MIT license upon contribution in https://github.com/CommunityToolkit/WindowsCommunityToolkit/pull/772.
|
||||
6. qmatteoq/DesktopBridgeHelpers commit e278153 (https://github.com/qmatteoq/DesktopBridgeHelpers), contributed by Matteo Pagani to identify if running with identity in DesktopNotificationManagerCompat.cs and DesktopBridgeHelpers.cs, relicensed to this .NET Foundation project under the MIT license upon contribution in https://github.com/CommunityToolkit/WindowsCommunityToolkit/pull/3457.
|
||||
7. Sergio0694/ComputeSharp (https://github.com/Sergio0694/ComputeSharp), contributed by Sergio Pedri to reuse some helpers to support incremental generators in the MVVM Toolkit.
|
||||
|
||||
%% PedroLamas/DeferredEvents NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
@ -113,4 +114,30 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
=========================================
|
||||
END OF PrivateOject/PrivateType NOTICES AND INFORMATION
|
||||
END OF PrivateOject/PrivateType NOTICES AND INFORMATION
|
||||
|
||||
%% Sergio0694/ComputeSharp NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Sergio Pedri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
=========================================
|
||||
END OF Sergio0694/ComputeSharp NOTICES AND INFORMATION
|
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.11.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
|
@ -33,7 +33,7 @@ public partial class SampleViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0004");
|
||||
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0001");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -60,7 +60,7 @@ public partial class SampleViewModel : ObservableObject
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0004");
|
||||
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(source, "MVVMTK0001");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -79,7 +79,7 @@ public partial class SampleViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0005");
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0002");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -105,7 +105,7 @@ public partial class SampleViewModel : ObservableObject
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0005");
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0002");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -124,7 +124,7 @@ public partial class SampleViewModel : INotifyPropertyChanging
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0006");
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0003");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -147,7 +147,7 @@ public partial class SampleViewModel : MyBaseViewModel
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0006");
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(source, "MVVMTK0003");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -171,7 +171,7 @@ public partial class SampleViewModel : ObservableRecipient
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableRecipientGenerator>(source, "MVVMTK0007");
|
||||
VerifyGeneratedDiagnostics<ObservableRecipientGenerator>(source, "MVVMTK0004");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -188,7 +188,7 @@ public partial class SampleViewModel
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableRecipientGenerator>(source, "MVVMTK0008");
|
||||
VerifyGeneratedDiagnostics<ObservableRecipientGenerator>(source, "MVVMTK0005");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -209,7 +209,7 @@ public partial class SampleViewModel
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0009");
|
||||
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(source, "MVVMTK0006");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -227,7 +227,7 @@ public partial class SampleViewModel
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0012");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0007");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -246,7 +246,7 @@ public partial class SampleViewModel
|
||||
|
||||
VerifyGeneratedDiagnostics<INotifyPropertyChangedGenerator>(
|
||||
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
|
||||
"MVVMTK0013");
|
||||
"MVVMTK0008");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -265,7 +265,7 @@ public partial class SampleViewModel
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservableObjectGenerator>(
|
||||
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
|
||||
"MVVMTK0013");
|
||||
"MVVMTK0008");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -286,7 +286,7 @@ public partial class SampleViewModel
|
||||
|
||||
VerifyGeneratedDiagnostics<ObservablePropertyGenerator>(
|
||||
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
|
||||
"MVVMTK0013");
|
||||
"MVVMTK0008");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -304,7 +304,7 @@ public partial class SampleViewModel : ObservableValidator
|
||||
}
|
||||
}";
|
||||
|
||||
// This is explicitly allowed in C# < 9.0, as it doesn't use any new features
|
||||
// This is explicitly allowed in C# < 8.0, as it doesn't use any new features
|
||||
VerifyGeneratedDiagnostics<ObservableValidatorValidateAllPropertiesGenerator>(
|
||||
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)));
|
||||
}
|
||||
@ -328,7 +328,7 @@ private void GreetUser(object value)
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(
|
||||
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)),
|
||||
"MVVMTK0013");
|
||||
"MVVMTK0008");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -351,7 +351,7 @@ public void Receive(MyMessage message)
|
||||
}
|
||||
}";
|
||||
|
||||
// This is explicitly allowed in C# < 9.0, as it doesn't use any new features
|
||||
// This is explicitly allowed in C# < 8.0, as it doesn't use any new features
|
||||
VerifyGeneratedDiagnostics<IMessengerRegisterAllGenerator>(
|
||||
CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp7_3)));
|
||||
}
|
||||
@ -375,7 +375,7 @@ private void GreetUser()
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0014");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0009");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -399,7 +399,7 @@ private void GreetUser()
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0015");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0010");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -421,7 +421,7 @@ private void GreetUser()
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0016");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0011");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -443,7 +443,7 @@ private void GreetUser()
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0016");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0011");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -465,7 +465,7 @@ private void GreetUser()
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0016");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0011");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -487,7 +487,7 @@ private void GreetUser()
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0016");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0011");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -509,7 +509,7 @@ private void GreetUser(string name)
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0016");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0011");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -531,7 +531,7 @@ private void GreetUser(string name)
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0016");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0011");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@ -551,7 +551,7 @@ private void GreetUser(User user)
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0017");
|
||||
VerifyGeneratedDiagnostics<ICommandGenerator>(source, "MVVMTK0012");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -561,7 +561,7 @@ private void GreetUser(User user)
|
||||
/// <param name="source">The input source to process.</param>
|
||||
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
|
||||
private static void VerifyGeneratedDiagnostics<TGenerator>(string source, params string[] diagnosticsIds)
|
||||
where TGenerator : class, ISourceGenerator, new()
|
||||
where TGenerator : class, IIncrementalGenerator, new()
|
||||
{
|
||||
VerifyGeneratedDiagnostics<TGenerator>(CSharpSyntaxTree.ParseText(source), diagnosticsIds);
|
||||
}
|
||||
@ -573,7 +573,7 @@ private static void VerifyGeneratedDiagnostics<TGenerator>(string source, params
|
||||
/// <param name="syntaxTree">The input source tree to process.</param>
|
||||
/// <param name="diagnosticsIds">The diagnostic ids to expect for the input source code.</param>
|
||||
private static void VerifyGeneratedDiagnostics<TGenerator>(SyntaxTree syntaxTree, params string[] diagnosticsIds)
|
||||
where TGenerator : class, ISourceGenerator, new()
|
||||
where TGenerator : class, IIncrementalGenerator, new()
|
||||
{
|
||||
Type observableObjectType = typeof(ObservableObject);
|
||||
Type validationAttributeType = typeof(ValidationAttribute);
|
||||
@ -590,9 +590,9 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies()
|
||||
references,
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
ISourceGenerator generator = new TGenerator();
|
||||
IIncrementalGenerator generator = new TGenerator();
|
||||
|
||||
CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: (CSharpParseOptions)syntaxTree.Options);
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator).WithUpdatedParseOptions((CSharpParseOptions)syntaxTree.Options);
|
||||
|
||||
_ = driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray<Diagnostic> diagnostics);
|
||||
|
||||
|
@ -270,6 +270,30 @@ public async Task Test_ICommandAttribute_ConcurrencyControl_AsyncRelayCommandOfT
|
||||
Assert.IsTrue(model.AwaitForInputTaskCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
// See https://github.com/CommunityToolkit/dotnet/issues/13
|
||||
[TestMethod]
|
||||
public void Test_ICommandAttribute_ViewModelRightAfterRegion()
|
||||
{
|
||||
ViewModelForIssue13 model = new();
|
||||
|
||||
Assert.IsNotNull(model.GreetCommand);
|
||||
Assert.IsInstanceOfType(model.GreetCommand, typeof(RelayCommand));
|
||||
}
|
||||
|
||||
#region Region
|
||||
public class Region
|
||||
{
|
||||
}
|
||||
#endregion
|
||||
|
||||
public partial class ViewModelForIssue13
|
||||
{
|
||||
[ICommand]
|
||||
private void Greet()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class MyViewModel
|
||||
{
|
||||
public Task? ExternalTask { get; set; }
|
||||
|
Loading…
Reference in New Issue
Block a user