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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<File Path="azure-pipelines.yml" />
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
<File Path="nuget.config" />
<File Path="toolkit.snk" />
<File Path="version.json" />
</Folder>
Expand All @@ -30,6 +31,7 @@
<Project Path="tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj" />
<Project Path="tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj" />
<Project Path="tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj" />
<Project Path="tests/CommunityToolkit.Mvvm.Aot.UnitTests/CommunityToolkit.Mvvm.Aot.UnitTests.csproj" />
<Project Path="tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj" />
<Project Path="tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj" />
<Project Path="tests/CommunityToolkit.Mvvm.ExternalAssembly/CommunityToolkit.Mvvm.ExternalAssembly.shproj" />
Expand Down
19 changes: 19 additions & 0 deletions nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<!-- MSTest early access packages. See: https://aka.ms/mstest/preview -->
<add key="test-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/test-tools/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="test-tools">
<package pattern="MSTest.*" />
<package pattern="Microsoft.Testing.*" />
</packageSource>
</packageSourceMapping>
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
<Compile Include="$(MSBuildThisFileDirectory)AsyncVoidReturningRelayCommandMethodCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)FieldReferenceForObservablePropertyFieldCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MakeObservableValidatorTypePartialCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePartialPropertyForSemiAutoPropertyCodeFixer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UsePartialPropertyForObservablePropertyCodeFixer.cs" />
</ItemGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.CodeFixers.props" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -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.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Text;

namespace CommunityToolkit.Mvvm.CodeFixers;

/// <summary>
/// A code fixer that makes types partial for generated ObservableValidator validation support.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public sealed class MakeObservableValidatorTypePartialCodeFixer : CodeFixProvider
{
/// <inheritdoc/>
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.ObservableValidatorTypeMustBePartialId);

/// <inheritdoc/>
public override FixAllProvider? GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}

/// <inheritdoc/>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
Diagnostic diagnostic = context.Diagnostics[0];
TextSpan diagnosticSpan = context.Span;

SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

if (root?.FindNode(diagnosticSpan).FirstAncestorOrSelf<TypeDeclarationSyntax>() is TypeDeclarationSyntax typeDeclaration)
{
context.RegisterCodeFix(
CodeAction.Create(
title: "Make type partial",
createChangedDocument: token => MakeTypePartialAsync(context.Document, root, typeDeclaration),
equivalenceKey: "Make type partial"),
diagnostic);
}
}

/// <summary>
/// Applies the code fix to a target type declaration and returns an updated document.
/// </summary>
/// <param name="document">The original document being fixed.</param>
/// <param name="root">The original tree root belonging to the current document.</param>
/// <param name="typeDeclaration">The <see cref="TypeDeclarationSyntax"/> to update.</param>
/// <returns>An updated document with the applied code fix.</returns>
private static Task<Document> MakeTypePartialAsync(Document document, SyntaxNode root, TypeDeclarationSyntax typeDeclaration)
{
SyntaxGenerator generator = SyntaxGenerator.GetGenerator(document);
TypeDeclarationSyntax updatedTypeDeclaration = (TypeDeclarationSyntax)generator.WithModifiers(typeDeclaration, generator.GetModifiers(typeDeclaration).WithPartial(true));

return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(typeDeclaration, updatedTypeDeclaration)));
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
MVVMTK0057 | CommunityToolkit.Mvvm.SourceGenerators.ObservableValidatorValidationGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0057
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\INotifyPropertyChangedInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\ObservableRecipientInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\PropertyInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\PropertyValidationInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\TypedConstantInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\TypedConstantInfo.Factory.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\ValidationInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\Models\ValidationTypeInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableObjectGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservablePropertyGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservablePropertyGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableRecipientGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidateAllPropertiesGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidationGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\ObservableValidatorValidationGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ComponentModel\TransitiveMembersGenerator.Execute.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs" />
Expand All @@ -59,6 +61,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\RequiresCSharpLanguageVersion14OrPreviewAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\ObservableValidatorValidationGeneratorPartialTypeAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UseObservablePropertyOnPartialPropertyAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UnsupportedCSharpLanguageVersionAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs" />
Expand Down Expand Up @@ -113,4 +116,4 @@
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.SourceGenerators.props" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// <param name="NotifiedCommandNames">The sequence of commands to notify.</param>
/// <param name="NotifyPropertyChangedRecipients">Whether or not the generated property also broadcasts changes.</param>
/// <param name="NotifyDataErrorInfo">Whether or not the generated property also validates its value.</param>
/// <param name="HasValidationAttributes">Whether or not the generated property has validation attributes on the effective generated property.</param>
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
/// <param name="IsReferenceTypeOrUnconstrainedTypeParameter">Indicates whether the property is of a reference type or an unconstrained type parameter.</param>
/// <param name="IncludeMemberNotNullOnSetAccessor">Indicates whether to include nullability annotations on the setter.</param>
/// <param name="IncludeRequiresUnreferencedCodeOnSetAccessor">Indicates whether to annotate the setter as requiring unreferenced code.</param>
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
internal sealed record PropertyInfo(
SyntaxKind AnnotatedMemberKind,
Expand All @@ -43,8 +43,8 @@ internal sealed record PropertyInfo(
EquatableArray<string> NotifiedCommandNames,
bool NotifyPropertyChangedRecipients,
bool NotifyDataErrorInfo,
bool HasValidationAttributes,
bool IsOldPropertyValueDirectlyReferenced,
bool IsReferenceTypeOrUnconstrainedTypeParameter,
bool IncludeMemberNotNullOnSetAccessor,
bool IncludeRequiresUnreferencedCodeOnSetAccessor,
EquatableArray<AttributeInfo> ForwardedAttributes);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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 locally declared validatable property.
/// </summary>
/// <param name="PropertyName">The name of the property to validate.</param>
/// <param name="HasDisplayAttribute">Whether the property has a <c>DisplayAttribute</c>.</param>
internal sealed record PropertyValidationInfo(string PropertyName, bool HasDisplayAttribute);
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
// See the LICENSE file in the project root for more information.

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

namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.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="Hierarchy">The hierarchy 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, EquatableArray<string> PropertyNames);
/// <param name="Properties">The locally declared validatable properties for the current type.</param>
internal sealed record ValidationInfo(HierarchyInfo Hierarchy, string TypeName, EquatableArray<PropertyValidationInfo> Properties);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 CommunityToolkit.Mvvm.SourceGenerators.Models;

namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;

/// <summary>
/// A model with identity information for a type requiring generated validation hooks.
/// </summary>
/// <param name="Hierarchy">The hierarchy for the target type.</param>
/// <param name="TypeName">The fully qualified type name for the target type.</param>
internal sealed record ValidationTypeInfo(HierarchyInfo Hierarchy, string TypeName);
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ public static bool TryGetInfo(
memberSyntax,
memberSymbol,
semanticModel,
ref hasAnyValidationAttributes,
in forwardedAttributes,
in builder,
token);
Expand Down Expand Up @@ -359,13 +360,6 @@ public static bool TryGetInfo(

token.ThrowIfCancellationRequested();

// We should generate [RequiresUnreferencedCode] on the setter if [NotifyDataErrorInfo] was used and the attribute is available
bool includeRequiresUnreferencedCodeOnSetAccessor =
notifyDataErrorInfo &&
semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute");

token.ThrowIfCancellationRequested();

// Prepare the effective property changing/changed names. For the property changing names,
// there are two possible cases: if the mode is disabled, then there are no names to report
// at all. If the mode is enabled, then the list is just the same as for property changed.
Expand Down Expand Up @@ -411,10 +405,10 @@ public static bool TryGetInfo(
notifiedCommandNames.ToImmutable(),
notifyRecipients,
notifyDataErrorInfo,
hasAnyValidationAttributes,
isOldPropertyValueDirectlyReferenced,
isReferenceTypeOrUnconstrainedTypeParameter,
includeMemberNotNullOnSetAccessor,
includeRequiresUnreferencedCodeOnSetAccessor,
forwardedAttributes.ToImmutable());

diagnostics = builder.ToImmutable();
Expand Down Expand Up @@ -885,13 +879,15 @@ private static void GetNullabilityInfo(
/// <param name="memberSyntax">The <see cref="MemberDeclarationSyntax"/> instance to process.</param>
/// <param name="memberSymbol">The input <see cref="ISymbol"/> instance to process.</param>
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
/// <param name="hasAnyValidationAttributes">Tracks whether the effective generated property has validation attributes.</param>
/// <param name="forwardedAttributes">The collection of forwarded attributes to add new ones to.</param>
/// <param name="diagnostics">The current collection of gathered diagnostics.</param>
/// <param name="token">The cancellation token for the current operation.</param>
private static void GatherLegacyForwardedAttributes(
MemberDeclarationSyntax memberSyntax,
ISymbol memberSymbol,
SemanticModel semanticModel,
ref bool hasAnyValidationAttributes,
in ImmutableArrayBuilder<AttributeInfo> forwardedAttributes,
in ImmutableArrayBuilder<DiagnosticInfo> diagnostics,
CancellationToken token)
Expand Down Expand Up @@ -965,6 +961,12 @@ private static void GatherLegacyForwardedAttributes(
continue;
}

if (targetIdentifier.IsKind(SyntaxKind.PropertyKeyword) &&
attributeTypeSymbol.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"))
{
hasAnyValidationAttributes = true;
}

IEnumerable<AttributeArgumentSyntax> attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>();

// Try to extract the forwarded attribute
Expand Down Expand Up @@ -1376,19 +1378,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName)))))));
}

// Add the [RequiresUnreferencedCode] attribute if needed:
//
// [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
// <SET_ACCESSOR>
if (propertyInfo.IncludeRequiresUnreferencedCodeOnSetAccessor)
{
setAccessor = setAccessor.AddAttributeLists(
AttributeList(SingletonSeparatedList(
Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode"))
.AddArgumentListArguments(
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered.")))))));
}

// Also add any forwarded attributes
setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes);

Expand Down
Loading