diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5bc1b1792..c4cbb324e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -55,6 +55,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index c6bf051a4..6aefd581a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -35,6 +36,7 @@ internal static class Execute /// The instance to process. /// The input instance to process. /// The instance for the current run. + /// The options in use for the generator. /// The cancellation token for the current operation. /// The resulting value, if successfully retrieved. /// The resulting diagnostics from the processing operation. @@ -43,6 +45,7 @@ public static bool TryGetInfo( FieldDeclarationSyntax fieldSyntax, IFieldSymbol fieldSymbol, SemanticModel semanticModel, + AnalyzerConfigOptions options, CancellationToken token, [NotNullWhen(true)] out PropertyInfo? propertyInfo, out ImmutableArray diagnostics) @@ -66,6 +69,11 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Override the property changing support if explicitly disabled + shouldInvokeOnPropertyChanging &= GetEnableINotifyPropertyChangingSupport(options); + + token.ThrowIfCancellationRequested(); + // Get the property type and name string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); string fieldName = fieldSymbol.Name; @@ -320,6 +328,27 @@ public static bool TryGetInfo( return true; } + /// + /// Gets the value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property. + /// + /// The options in use for the generator. + /// The value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property. + public static bool GetEnableINotifyPropertyChangingSupport(AnalyzerConfigOptions options) + { + if (options.TryGetValue("build_property.MvvmToolkitEnableINotifyPropertyChangingSupport", out string? propertyValue)) + { + if (bool.TryParse(propertyValue, out bool enableINotifyPropertyChangingSupport)) + { + return enableINotifyPropertyChangingSupport; + } + } + + // This setting is enabled by default, for backwards compatibility. + // Note that this path should never be reached, as the default + // value is also set in a .targets file bundled in the package. + return true; + } + /// /// Validates the containing type for a given field being annotated. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index ad7e5fe03..68801d449 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -25,8 +25,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { // Gather info for all annotated fields IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = - context.SyntaxProvider - .ForAttributeWithMetadataName( + context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute", static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, static (context, token) => @@ -44,7 +43,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - _ = Execute.TryGetInfo(fieldDeclaration, fieldSymbol, context.SemanticModel, token, out PropertyInfo? propertyInfo, out ImmutableArray diagnostics); + _ = Execute.TryGetInfo( + fieldDeclaration, + fieldSymbol, + context.SemanticModel, + context.GlobalOptions, + token, + out PropertyInfo? propertyInfo, + out ImmutableArray diagnostics); token.ThrowIfCancellationRequested(); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/GeneratorAttributeSyntaxContextWithOptions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/GeneratorAttributeSyntaxContextWithOptions.cs new file mode 100644 index 000000000..2af137911 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/GeneratorAttributeSyntaxContextWithOptions.cs @@ -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 System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// +/// +/// The original value. +/// The original value. +internal readonly struct GeneratorAttributeSyntaxContextWithOptions( + GeneratorAttributeSyntaxContext syntaxContext, + AnalyzerConfigOptions globalOptions) +{ + /// + public SyntaxNode TargetNode { get; } = syntaxContext.TargetNode; + + /// + public ISymbol TargetSymbol { get; } = syntaxContext.TargetSymbol; + + /// + public SemanticModel SemanticModel { get; } = syntaxContext.SemanticModel; + + /// + public ImmutableArray Attributes { get; } = syntaxContext.Attributes; + + /// + public AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs index d009810eb..1c627db6d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs @@ -4,7 +4,11 @@ using System; using System.Collections.Immutable; +using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +#pragma warning disable CS1574 namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -13,6 +17,31 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; /// internal static class IncrementalGeneratorInitializationContextExtensions { + /// + public static IncrementalValuesProvider ForAttributeWithMetadataNameAndOptions( + this IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName, + Func predicate, + Func transform) + { + // Invoke 'ForAttributeWithMetadataName' normally, but just return the context directly + IncrementalValuesProvider syntaxContext = context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate, + static (context, token) => context); + + // Do the same for the analyzer config options + IncrementalValueProvider configOptions = context.AnalyzerConfigOptionsProvider.Select(static (provider, token) => provider.GlobalOptions); + + // Merge the two and invoke the provided transform on these two values. Neither value + // is equatable, meaning the pipeline will always re-run until this point. This is + // intentional: we don't want any symbols or other expensive objects to be kept alive + // across incremental steps, especially if they could cause entire compilations to be + // rooted, which would significantly increase memory use and introduce more GC pauses. + // In this specific case, flowing non equatable values in a pipeline is therefore fine. + return syntaxContext.Combine(configOptions).Select((input, token) => transform(new GeneratorAttributeSyntaxContextWithOptions(input.Left, input.Right), token)); + } + /// /// Conditionally invokes /// if the value produced by the input is . diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets index 19c3c56c0..bf6d6ed6c 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets @@ -5,6 +5,14 @@ true + + + + +