From 054506c57318fa4a2dbd05dd264f6f3af3d703cc Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 16:55:13 -0600 Subject: [PATCH 001/200] Generated DependencyPropertyGenerator component from template --- .../OpenSolution.bat | 3 ++ .../samples/Assets/icon.png | Bin 0 -> 2216 bytes .../samples/Dependencies.props | 31 +++++++++++++++++ ...DependencyPropertyGenerator.Samples.csproj | 10 ++++++ .../samples/DependencyPropertyGenerator.md | 32 ++++++++++++++++++ ...pendencyPropertyGeneratorCustomSample.xaml | 8 +++++ ...dencyPropertyGeneratorCustomSample.xaml.cs | 30 ++++++++++++++++ ...ontrols.DependencyPropertyGenerator.csproj | 15 ++++++++ .../src/MultiTarget.props | 9 +++++ ...ependencyPropertyGenerator.Tests.projitems | 23 +++++++++++++ .../DependencyPropertyGenerator.Tests.shproj | 13 +++++++ tooling | 2 +- 12 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/OpenSolution.bat create mode 100644 components/DependencyPropertyGenerator/samples/Assets/icon.png create mode 100644 components/DependencyPropertyGenerator/samples/Dependencies.props create mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj create mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md create mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml create mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs create mode 100644 components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj create mode 100644 components/DependencyPropertyGenerator/src/MultiTarget.props create mode 100644 components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems create mode 100644 components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj diff --git a/components/DependencyPropertyGenerator/OpenSolution.bat b/components/DependencyPropertyGenerator/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/DependencyPropertyGenerator/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/samples/Assets/icon.png b/components/DependencyPropertyGenerator/samples/Assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8435bcaa9fc371ca8e92db07ae596e0d57c8b9b0 GIT binary patch literal 2216 zcmV;Z2v_%sP);M1&0drDELIAGL9O(c600d`2O+f$vv5yP=YKs(Je9p7&Ka&|n*ecc!Pix~iV~>Yi6*wXL?Fq6O}_ef#!O2;q#X&d1-r zFW&c8*L5NOYWz*lA^xUE>kGNx-uL6v^z@vr)V_TA(vQ#Yoo1$I^Fv;IKkA`?U*Z6OoBe{XX@DLpL{F3+>RV2oP732rn@P@98F ziH~#f`tA7f<8t}(<$sLlUkdm_STvOKGYXY{9St3tGiu1>duN9F9aTe*kTXOCkoLYj zr)5cJP?i}f+kBoB8b~Q1rbJi`730DXGLx}aCV-6tC522QjZs)X03S%#=jXopQNe7G z@dlToiDbGDSy!a_DD)NYWiV?sGap0Dq-qgnA&~LHECw(NL8T=) z1ct(ga2;|14nD@qHwmVQ0;2|YUomW^!U#`7l>>&Bh;u)pT<|$jFoqk6%HTc~^RQ@( z5hZ5X^n7`vt!*nY9@rFRqF{^wF`}&H4I4JdfddC*W@e@$oS$Q04aThBn*gT3)URMp z>G{o@H*)RTHCb6%8B?H}yjcXcUm9p(K=nWD0vP!PaCP$@(k!31bkrIJ!2)-tl96*+&}@I6!3M8qT~Q2?u8 zcy@MHS+IzlwjvzRGx~+*l>(Gi6$!C8Jgi;2)=XVKe*CB(K745TS`)G2;nuBN9cqsP zh3?9y5yI0j3rZt%Q;V3i*L;-@bj}#EBDL zi-O{(`k1i!rOBH%ZPF-Qh^8PnZ{F0;pFa!x1_lMnUFbI&&8gFC}WVB;VU)DL&bgeyDBGT1Uw=yFE1xQ zlXdIXN%Xx|Sv6HKV+J+vFk{k20ni@>s(7s{7```cTQ%Vf8y`xM()#6VjL@l-2UZ)1 zMAlp(2n!()MJZ+A7DT29I(lXL!EfSTFx}nSy)d`h{3}%2w00Npq^YO)x9XqDcq0Kb`t5*xfQqpFjCI=5f3~jf78p^G}no2Fzdc;)IysSSZVr(fukQEprykDy< zqbZnxBN8(`!7W?1$e}}r1c|2>qh?{>d-v{@?c2BeJQ<2<$;_cXbnDiw*59|?yLU^< zSA=eeDMyH&Dn;O?U<@moWoql!uTK{z=_+aO*s+8Ar_R9^^Jcn6#~@GNDp>Q(&lq|B z{JGq_cdrQ3>E_6hBQiff?~J4|4F&T## ze2Ot?F7i1RStkmn!!cL^bKdFtd(+&zckeRXgNN#PA!`aD zjaC7J&2fZoFH{s(d8}#I6o7s`O)w?K`{bKiENsKV!h(MK^r(|LX> z*~rJxUHVoXzp-XhUqnbQUAiRq@89q5e?*H1Nj(o2FJ5%B>%JZY`Gw<0&+dhGuj!C7 z?uYbkO5XY{KIV*@{VRoX8s`fm<068)Y!=w) zn?l)IstDTf!(Iu(8i;vTaeMOuE%QV$RY7$1cP-aqS07(jX4W{bFHp!#3m}TTAaaF3L~n9Q)s^3xP5nw5 zM&HvBw5z{TbdA3!8Di)wIs`5S;W!fVdWDbiH|P}wlVfDMuB&#@so4j+uC6L7Q#M38 z*q?Pnc_gqfql3rn?qh)V@~B|(78+7U zKOCcocD&A`ELB-^?%cVvanNF%vtSH&^_LVuBqdkprWDoUyaLCf$s5zvc?rH#NGXk= qmFA_t_5FR}!i7I&wXL?Ful)xU?DJJ%Hwu*i0000 + + + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj new file mode 100644 index 000000000..c7af9907c --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.Samples.csproj @@ -0,0 +1,10 @@ + + + + + DependencyPropertyGenerator + + + + + diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md new file mode 100644 index 000000000..35e7939c9 --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md @@ -0,0 +1,32 @@ +--- +title: DependencyPropertyGenerator +author: githubaccount +description: TODO: Your experiment's description here +keywords: DependencyPropertyGenerator, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- + + + + + + + + + +# DependencyPropertyGenerator + +TODO: Fill in information about this experiment and how to get started here... + +## Custom Control + +You can inherit from an existing component as well, like `Panel`, this example shows a control without a +XAML Style that will be more light-weight to consume by an app developer: + +> [!Sample DependencyPropertyGeneratorCustomSample] diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml new file mode 100644 index 000000000..0d45a88ec --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml @@ -0,0 +1,8 @@ + + + + diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs new file mode 100644 index 000000000..1b07afdd3 --- /dev/null +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs @@ -0,0 +1,30 @@ +// 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.WinUI.Controls; + +namespace DependencyPropertyGeneratorExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// +[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] +[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] + +[ToolkitSample(id: nameof(DependencyPropertyGeneratorCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(DependencyPropertyGenerator)} custom control.")] +public sealed partial class DependencyPropertyGeneratorCustomSample : Page +{ + public DependencyPropertyGeneratorCustomSample() + { + this.InitializeComponent(); + } + + // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 + public static Orientation ConvertStringToOrientation(string orientation) => orientation switch + { + "Vertical" => Orientation.Vertical, + "Horizontal" => Orientation.Horizontal, + _ => throw new System.NotImplementedException(), + }; +} diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj new file mode 100644 index 000000000..84f505cc8 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj @@ -0,0 +1,15 @@ + + + + + DependencyPropertyGenerator + This package contains DependencyPropertyGenerator. + + + CommunityToolkit.WinUI.Controls.DependencyPropertyGeneratorRns + false + + + + + diff --git a/components/DependencyPropertyGenerator/src/MultiTarget.props b/components/DependencyPropertyGenerator/src/MultiTarget.props new file mode 100644 index 000000000..b11c19426 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems new file mode 100644 index 000000000..3156f4a9f --- /dev/null +++ b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 22BE50B3-9810-4304-899E-6D7AF9D3147A + + + DependencyPropertyGeneratorExperiment.Tests + + + + + ExampleDependencyPropertyGeneratorTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj new file mode 100644 index 000000000..65d342261 --- /dev/null +++ b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.shproj @@ -0,0 +1,13 @@ + + + + 22BE50B3-9810-4304-899E-6D7AF9D3147A + 14.0 + + + + + + + + diff --git a/tooling b/tooling index d71b08b2d..2ec08a780 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit d71b08b2dccf94c3ceaeda99526679bc0cfc3b8a +Subproject commit 2ec08a780daaecadd9705a2ab0e4cdde4c6168e2 From 4ed617295954a67806dbe8668e63bd01266fde92 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 15:07:48 -0800 Subject: [PATCH 002/200] Port files from private repo --- .../AnalyzerReleases.Shipped.md | 20 + .../AnalyzerReleases.Unshipped.md | 2 + ...dDependencyProperty.SourceGenerator.csproj | 30 + .../Constants/WellKnownTrackingNames.cs | 26 + .../Constants/WellKnownTypeNames.cs | 47 + .../DependencyPropertyGenerator.Execute.cs | 763 +++++++ .../DependencyPropertyGenerator.cs | 174 ++ ...dPropertyConflictingDeclarationAnalyzer.cs | 70 + ...opertyContainingTypeDeclarationAnalyzer.cs | 65 + ...nvalidPropertyDefaultValueTypeAttribute.cs | 128 ++ ...dPropertyNonNullableDeclarationAnalyzer.cs | 70 + ...nvalidPropertySymbolDeclarationAnalyzer.cs | 100 + ...nvalidPropertySyntaxDeclarationAnalyzer.cs | 102 + ...nsupportedCSharpLanguageVersionAnalyzer.cs | 78 + .../Diagnostics/DiagnosticDescriptors.cs | 156 ++ .../Extensions/AccessibilityExtensions.cs | 32 + .../Extensions/AttributeDataExtensions.cs | 117 + .../Extensions/CompilationExtensions.cs | 45 + .../Extensions/ISymbolExtensions.cs | 94 + .../Extensions/ITypeSymbolExtensions.cs | 139 ++ .../IncrementalValueProviderExtensions.cs | 71 + .../IndentedTextWriterExtensions.cs | 104 + .../Extensions/SyntaxNodeExtensions.cs | 76 + .../Helpers/EquatableArray{T}.cs | 216 ++ .../Helpers/HashCode.cs | 503 +++++ .../Helpers/ImmutableArrayBuilder{T}.cs | 365 +++ .../Helpers/IndentedTextWriter.cs | 515 +++++ .../Helpers/ObjectPool{T}.cs | 154 ++ .../Models/DependencyPropertyInfo.cs | 40 + .../Models/HierarchyInfo.cs | 140 ++ .../Models/TypeInfo.cs | 32 + .../Models/TypedConstantInfo.Factory.cs | 60 + .../Models/TypedConstantInfo.cs | 187 ++ ...t.GeneratedDependencyProperty.Tests.csproj | 41 + .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 60 + .../CSharpGeneratorTest{TGenerator}.cs | 220 ++ .../Test_Analyzers.cs | 746 +++++++ ...ncyPropertyGenerator.PostInitialization.cs | 48 + .../Test_DependencyPropertyGenerator.cs | 1962 +++++++++++++++++ ...Toolkit.GeneratedDependencyProperty.csproj | 10 + ...oolkit.GeneratedDependencyProperty.targets | 70 + .../src/GeneratedDependencyProperty.cs | 24 + .../GeneratedDependencyPropertyAttribute.cs | 63 + 43 files changed, 7965 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Shipped.md create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Unshipped.md create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTrackingNames.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTypeNames.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.Execute.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AccessibilityExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AttributeDataExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/CompilationExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ISymbolExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ITypeSymbolExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IncrementalValueProviderExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IndentedTextWriterExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/SyntaxNodeExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/EquatableArray{T}.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/HashCode.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ImmutableArrayBuilder{T}.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/IndentedTextWriter.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ObjectPool{T}.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/DependencyPropertyInfo.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/HierarchyInfo.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypeInfo.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.Factory.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_Analyzers.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.cs create mode 100644 components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj create mode 100644 components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.targets create mode 100644 components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs create mode 100644 components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..a7615b210 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Shipped.md @@ -0,0 +1,20 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +WCTDP0001 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0002 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0003 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0004 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0005 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0006 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0007 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0008 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0009 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0010 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0011 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Unshipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..6a6dd08d1 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,2 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj new file mode 100644 index 000000000..b64446802 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj @@ -0,0 +1,30 @@ + + + netstandard2.0 + 13.0 + enable + true + true + $(DefineConstants);WINDOWS_UWP + + + $(NoWarn);IDE0130 + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTrackingNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTrackingNames.cs new file mode 100644 index 000000000..1539c7ea0 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTrackingNames.cs @@ -0,0 +1,26 @@ +// 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.GeneratedDependencyProperty.Constants; + +/// +/// The well known names for tracking steps, to test the incremental generators. +/// +internal static class WellKnownTrackingNames +{ + /// + /// The initial transform node. + /// + public const string Execute = nameof(Execute); + + /// + /// The filtered transform with just output diagnostics. + /// + public const string Diagnostics = nameof(Diagnostics); + + /// + /// The filtered transform with just output sources. + /// + public const string Output = nameof(Output); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTypeNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTypeNames.cs new file mode 100644 index 000000000..cc2808537 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTypeNames.cs @@ -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. + +namespace CommunityToolkit.GeneratedDependencyProperty.Constants; + +/// +/// The well known names for types used by source generators and analyzers. +/// +internal static class WellKnownTypeNames +{ + /// + /// The fully qualified type name for the [GeneratedDependencyProperty] type. + /// + public const string GeneratedDependencyPropertyAttribute = "CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"; + + /// + /// The fully qualified type name for the XAML namespace. + /// + public const string XamlNamespace = +#if WINDOWS_UWP + "Windows.UI.Xaml"; + +#else + "Microsoft.UI.Xaml"; +#endif + + /// + /// The fully qualified type name for the DependencyObject type. + /// + public const string DependencyObject = $"{XamlNamespace}.{nameof(DependencyObject)}"; + + /// + /// The fully qualified type name for the DependencyProperty type. + /// + public const string DependencyProperty = $"{XamlNamespace}.{nameof(DependencyProperty)}"; + + /// + /// The fully qualified type name for the DependencyPropertyChangedEventArgs type. + /// + public const string DependencyPropertyChangedEventArgs = $"{XamlNamespace}.{nameof(DependencyPropertyChangedEventArgs)}"; + + /// + /// The fully qualified type name for the PropertyMetadata type. + /// + public const string PropertyMetadata = $"{XamlNamespace}.{nameof(PropertyMetadata)}"; +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.Execute.cs new file mode 100644 index 000000000..aa5164dba --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.Execute.cs @@ -0,0 +1,763 @@ +// 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.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +partial class DependencyPropertyGenerator +{ + /// + /// A container for all the logic for . + /// + private static partial class Execute + { + /// + /// Placeholder for . + /// + private static readonly TypedConstantInfo.Null NullInfo = new(); + + /// + /// Placeholder for the unset value of a given property type. + /// + private static readonly TypedConstantInfo.UnsetValue UnsetValueInfo = new(); + + /// + /// Generates the sources for the embedded types, for PrivateAssets="all" scenarios. + /// + /// The input value to use to emit sources. + public static void GeneratePostInitializationSources(IncrementalGeneratorPostInitializationContext context) + { + void GenerateSource(string typeName) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + string fileName = $"{typeName}.g.cs"; + string sourceText; + + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)) + using (StreamReader reader = new(stream)) + { + sourceText = reader.ReadToEnd(); + } + + context.CancellationToken.ThrowIfCancellationRequested(); + + string updatedSourceText = sourceText + .Replace("", GeneratorName) + .Replace("", typeof(Execute).Assembly.GetName().Version.ToString()); + + context.CancellationToken.ThrowIfCancellationRequested(); + + context.AddSource(fileName, updatedSourceText); + } + + GenerateSource("GeneratedDependencyProperty"); + GenerateSource("GeneratedDependencyPropertyAttribute"); + } + + /// + /// Checks whether an input syntax node is a candidate property declaration for the generator. + /// + /// The input syntax node to check. + /// The used to cancel the operation, if needed. + /// Whether is a candidate property declaration. + public static bool IsCandidateSyntaxValid(SyntaxNode node, CancellationToken token) + { + // Initial check that's identical to the analyzer + if (!InvalidPropertySyntaxDeclarationAnalyzer.IsValidPropertyDeclaration(node)) + { + return false; + } + + // Make sure that all containing types are partial, otherwise declaring a partial property + // would not be valid. We don't need to emit diagnostics here, the compiler will handle that. + for (TypeDeclarationSyntax? parentNode = node.FirstAncestor(); + parentNode is not null; + parentNode = parentNode.FirstAncestor()) + { + if (!parentNode.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return false; + } + } + + // Here we can also easily filter out ref-returning properties just using syntax + if (((PropertyDeclarationSyntax)node).Type.IsKind(SyntaxKind.RefType)) + { + return false; + } + + return true; + } + + /// + /// Checks whether an input symbol is a candidate property declaration for the generator. + /// + /// The input symbol to check. + /// Whether is a candidate property declaration. + public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol) + { + // Ensure that the property declaration is a partial definition with no implementation + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + return false; + } + + // Also ignore all properties that have an invalid declaration + if (propertySymbol.IsStatic || propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly || propertySymbol.Type.IsRefLikeType) + { + return false; + } + + // Ensure we do have a valid containing + if (propertySymbol.ContainingType is not { } typeSymbol) + { + return false; + } + + // Ensure that the containing type derives from 'DependencyObject' + if (!typeSymbol.InheritsFromFullyQualifiedMetadataName(WellKnownTypeNames.DependencyObject)) + { + return false; + } + + // If the generated property name is called "Property" and the type is either object or 'DependencyPropertyChangedEventArgs', + // consider it invalid. This is needed because if such a property was generated, the partial 'OnChanged' + // methods would conflict. + if (propertySymbol.Name == "Property") + { + bool propertyTypeWouldCauseConflicts = + propertySymbol.Type.SpecialType == SpecialType.System_Object || + propertySymbol.Type.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs); + + return !propertyTypeWouldCauseConflicts; + } + + return true; + } + + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// + /// The input node. + /// The input instance. + /// The accessibility of the property, if available. + /// The accessibility of the accessor, if available. + /// The accessibility of the accessor, if available. + /// Whether the property was valid and the accessibilities could be retrieved. + public static bool TryGetAccessibilityModifiers( + PropertyDeclarationSyntax node, + IPropertySymbol propertySymbol, + out Accessibility declaredAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility) + { + declaredAccessibility = Accessibility.NotApplicable; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + // Ensure that we have a getter and a setter, and that the setter is not init-only + if (propertySymbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // Track the property accessibility if explicitly set + if (node.Modifiers.Count > 0) + { + declaredAccessibility = propertySymbol.DeclaredAccessibility; + } + + // Track the accessors accessibility, if explicitly set + foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? []) + { + if (accessor.Modifiers.Count == 0) + { + continue; + } + + switch (accessor.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + getterAccessibility = getMethod.DeclaredAccessibility; + break; + case SyntaxKind.SetAccessorDeclaration: + setterAccessibility = setMethod.DeclaredAccessibility; + break; + } + } + + return true; + } + + /// + /// Gets the default value to use to initialize the generated property, if explicitly specified. + /// + /// The input that triggered the annotation. + /// The input instance. + /// The for the current compilation. + /// The used to cancel the operation, if needed. + /// The default value to use to initialize the generated property. + public static TypedConstantInfo GetDefaultValue( + AttributeData attributeData, + IPropertySymbol propertySymbol, + SemanticModel semanticModel, + CancellationToken token) + { + // First, check whether the default value is explicitly set or not + if (attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + // If the explicit value is anything other than 'null', we can return it directly + if (!defaultValue.IsNull) + { + return TypedConstantInfo.Create(defaultValue); + } + + // Handle 'UnsetValue' as well + if (InvalidPropertyDefaultValueTypeAttribute.IsDependencyPropertyUnsetValue(attributeData, semanticModel, token)) + { + return UnsetValueInfo; + } + + // Otherwise, the value has been explicitly set to 'null', so let's respect that + return NullInfo; + } + + // In all other cases, we'll automatically use the default value of the type in question. + // First we need to special case non nullable values, as for those we need 'default'. + if (propertySymbol.Type is { IsValueType: true } and not INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }) + { + return new TypedConstantInfo.Default(propertySymbol.Type.GetFullyQualifiedName()); + } + + // For all other ones, we can just use the 'null' placeholder again + return NullInfo; + } + + /// + /// Checks whether the generated code has to register the property changed callback with WinRT. + /// + /// The input that triggered the annotation. + /// Whether the generated should register the property changed callback. + public static bool IsLocalCachingEnabled(AttributeData attributeData) + { + return attributeData.GetNamedArgument("IsLocalCacheEnabled", defaultValue: false); + } + + /// + /// Checks whether the generated code has to register the property changed callback with WinRT. + /// + /// The input instance to process. + /// Whether the generated should register the property changed callback. + public static bool IsPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol) + { + // Check for any 'OnChanged' methods + foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}PropertyChanged")) + { + // We're looking for methods with one parameters, so filter on that first + if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol argsType }] }) + { + continue; + } + + // There might be other property changed callback methods when field caching is enabled, or in other scenarios. + // Because the callback method existing adds overhead (since we have to register it with WinRT), we want to + // avoid false positives. To do that, we check that the parameter type is exactly the one we need. + if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether the generated code has to register the shared property changed callback with WinRT. + /// + /// The input instance to process. + /// Whether the generated should register the shared property changed callback. + public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol) + { + // Check for any 'OnPropertyChanged' methods + foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers("OnPropertyChanged")) + { + // Same filter as above + if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol argsType }] }) + { + continue; + } + + // Also same actual check as above + if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether an input property is required. + /// + /// The input instance to process. + /// Whether is required. + public static bool IsRequiredProperty(IPropertySymbol propertySymbol) + { + return propertySymbol.IsRequired; + } + + /// + /// Writes all implementations of partial dependency property declarations. + /// + /// The input set of declared dependency properties. + /// The instance to write into. + public static void WritePropertyDeclarations(EquatableArray propertyInfos, IndentedTextWriter writer) + { + // Helper to get the nullable type name for the initial property value + static string GetOldValueTypeNameAsNullable(DependencyPropertyInfo propertyInfo) + { + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference + // type, the previous value might be null even if the property type is not nullable, as the first invocation would + // happen when the property is first set to some value that is not null (but the backing field would still be so). + // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability + // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. + return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch + { + true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") + => $"{propertyInfo.TypeNameWithNullabilityAnnotations}?", + _ => propertyInfo.TypeNameWithNullabilityAnnotations + }; + } + + // Helper to get the accessibility with a trailing space + static string GetExpressionWithTrailingSpace(Accessibility accessibility) + { + return accessibility.GetExpression() switch + { + { Length: > 0 } expression => expression + " ", + _ => "" + }; + } + + string typeQualifiedName = propertyInfos[0].Hierarchy.Hierarchy[0].QualifiedName; + + // First, generate all the actual dependency property fields + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + string typeMetadata = propertyInfo switch + { + // Shared codegen + { DefaultValue: TypedConstantInfo.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => "null", + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue})", + + // Codegen for legacy UWP + { IsNet8OrGreater: false } => propertyInfo switch + { + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } + => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } + => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e))", + { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } + => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, static (d, e) => {{ (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e); (({typeQualifiedName})d).OnPropertyChanged(e); }})", + _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), + }, + + // Codegen for .NET 8 or greater + { DefaultValue: TypedConstantInfo.Null } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) + => $"new global::{WellKnownTypeNames.PropertyMetadata}(null, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) + => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), + }; + + writer.WriteLine($$""" + /// + /// The backing instance for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($$""" + public static readonly global::{{WellKnownTypeNames.DependencyProperty}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty}}.Register( + name: "{{propertyInfo.PropertyName}}", + propertyType: typeof({{propertyInfo.TypeName}}), + ownerType: typeof({{typeQualifiedName}}), + typeMetadata: {{typeMetadata}}); + """, isMultiline: true); + writer.WriteLine(); + } + + // After the properties, generate all partial property implementations at the top of the partial type declaration + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo); + + writer.WriteLine(skipIfPresent: true); + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(GeneratorName); + writer.Write(GetExpressionWithTrailingSpace(propertyInfo.DeclaredAccessibility)); + writer.WriteIf(propertyInfo.IsRequired, "required "); + writer.WriteLine($"partial {propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.PropertyName}"); + + using (writer.WriteBlock()) + { + // We need very different codegen depending on whether local caching is enabled or not + if (propertyInfo.IsLocalCachingEnabled) + { + writer.WriteLine($$""" + get => field; + set + { + On{{propertyInfo.PropertyName}}Set(ref value); + + if (global::System.Collections.Generic.EqualityComparer<{{oldValueTypeNameAsNullable}}>.Default.Equals(field, value)) + { + return; + } + + {{oldValueTypeNameAsNullable}} __oldValue = field; + + On{{propertyInfo.PropertyName}}Changing(value); + On{{propertyInfo.PropertyName}}Changing(__oldValue, value); + + field = value; + + object? __boxedValue = value; + """, isMultiline: true); + writer.WriteLineIf(propertyInfo.TypeName != "object", $""" + + On{propertyInfo.PropertyName}Set(ref __boxedValue); + """, isMultiline: true); + writer.Write($$""" + + SetValue({{propertyInfo.PropertyName}}Property, __boxedValue); + + On{{propertyInfo.PropertyName}}Changed(value); + On{{propertyInfo.PropertyName}}Changed(__oldValue, value); + } + """, isMultiline: true); + + // If the default value is not what the default field value would be, add an initializer + if (propertyInfo.DefaultValue is not (TypedConstantInfo.Null or TypedConstantInfo.Default)) + { + writer.Write($" = {propertyInfo.DefaultValue};"); + } + + // Always leave a newline after the end of the property declaration, in either case + writer.WriteLine(); + } + else if (propertyInfo.TypeName == "object") + { + // If local caching is not enabled, we simply relay to the 'DependencyProperty' value. We cannot raise any methods + // to explicitly notify of changes that rely on the previous value. Retrieving it to conditionally invoke the methods + // would introduce a lot of overhead. If callers really do want to have a callback being invoked, they can implement + // the one wired up to the property metadata directly. We can still invoke the ones only using the new value, though. + writer.WriteLine($$""" + get + { + object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property); + + On{{propertyInfo.PropertyName}}Get(ref __boxedValue); + + return __boxedValue; + } + set + { + On{{propertyInfo.PropertyName}}Set(ref value); + + SetValue({{propertyInfo.PropertyName}}Property, value); + + On{{propertyInfo.PropertyName}}Changed(value); + } + """, isMultiline: true); + } + else + { + // Same as above but with the extra typed hook for both accessors + writer.WriteLine($$""" + get + { + object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property); + + On{{propertyInfo.PropertyName}}Get(ref __boxedValue); + + {{propertyInfo.TypeNameWithNullabilityAnnotations}} __unboxedValue = ({{propertyInfo.TypeNameWithNullabilityAnnotations}})__boxedValue; + + On{{propertyInfo.PropertyName}}Get(ref __unboxedValue); + + return __unboxedValue; + } + set + { + On{{propertyInfo.PropertyName}}Set(ref value); + + object? __boxedValue = value; + + On{{propertyInfo.PropertyName}}Set(ref __boxedValue); + + SetValue({{propertyInfo.PropertyName}}Property, __boxedValue); + + On{{propertyInfo.PropertyName}}Changed(value); + } + """, isMultiline: true); + + } + } + } + + // Next, emit all partial method declarations at the bottom of the file + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo); + string objectTypeNameWithNullabilityAnnotation = propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") ? "object?" : "object"; + + if (!propertyInfo.IsLocalCachingEnabled) + { + // OnGet 'object' overload (only without local caching, as otherwise we just return the field value) + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Get(ref {objectTypeNameWithNullabilityAnnotation} propertyValue);"); + + // OnGet typed overload + if (propertyInfo.TypeName != "object") + { + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Get(ref {propertyInfo.TypeNameWithNullabilityAnnotations} propertyValue);"); + } + } + + // OnSet 'object' overload + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Set(ref {objectTypeNameWithNullabilityAnnotation} propertyValue);"); + + if (propertyInfo.TypeName != "object") + { + // OnSet typed overload + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Set(ref {propertyInfo.TypeNameWithNullabilityAnnotations} propertyValue);"); + } + + // We can only generate the direct callback methods when using local caching (see notes above) + if (propertyInfo.IsLocalCachingEnabled) + { + // OnChanging, only with new value + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changing({propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + + // OnChanging, with both values + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changing({oldValueTypeNameAsNullable} oldValue, {propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + } + + // OnChanged, only with new value (this is always supported) + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($""" + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changed({propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + + // OnChanged, with both values (once again, this is only supported when local caching is enabled) + if (propertyInfo.IsLocalCachingEnabled) + { + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}Changed({oldValueTypeNameAsNullable} oldValue, {propertyInfo.TypeNameWithNullabilityAnnotations} newValue);"); + } + + // OnChanged, for the property metadata callback + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}PropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs} e);"); + } + + // OnPropertyChanged, for the shared property metadata callback + writer.WriteLine(); + writer.WriteLine($""" + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + writer.WriteLine($"partial void OnPropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs} e);"); + } + + /// + /// Checks whether additional types are required for the input set of properties. + /// + /// The input set of declared dependency properties. + /// Whether additional types are required. + public static bool RequiresAdditionalTypes(EquatableArray propertyInfos) + { + // If the target is not .NET 8, we never need additional types (as '[UnsafeAccessor]' is not available) + if (!propertyInfos[0].IsNet8OrGreater) + { + return false; + } + + // We need the additional type holding the generated callbacks if at least one WinRT-based callback is present + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + if (propertyInfo.IsPropertyChangedCallbackImplemented || propertyInfo.IsSharedPropertyChangedCallbackImplemented) + { + return true; + } + } + + return false; + } + + /// + /// Registers a callback to generate additional types, if needed. + /// + /// The input set of declared dependency properties. + /// The instance to write into. + public static void WriteAdditionalTypes(EquatableArray propertyInfos, IndentedTextWriter writer) + { + writer.WriteLine("using global::System.Runtime.CompilerServices;"); + writer.WriteLine($"using global::{WellKnownTypeNames.XamlNamespace};"); + writer.WriteLine(); + writer.WriteLine($$""" + /// + /// Contains shared property changed callbacks for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName); + writer.WriteLine("file static class PropertyChangedCallbacks"); + + using (writer.WriteBlock()) + { + string fullyQualifiedTypeName = propertyInfos[0].Hierarchy.GetFullyQualifiedTypeName(); + + // Write the public accessors to use in property initializers + writer.WriteLineSeparatedMembers(propertyInfos.AsSpan(), (propertyInfo, writer) => + { + writer.WriteLine($$""" + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback {{propertyInfo.PropertyName}}() + { + static void Invoke(object d, DependencyPropertyChangedEventArgs e) + { + {{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d; + + """, isMultiline: true); + writer.IncreaseIndent(); + writer.IncreaseIndent(); + + // Per-property callback, if present + if (propertyInfo.IsPropertyChangedCallbackImplemented) + { + writer.WriteLine($"On{propertyInfo.PropertyName}PropertyChanged(__this, e);"); + } + + // Shared callback, if present + if (propertyInfo.IsSharedPropertyChangedCallbackImplemented) + { + writer.WriteLine("OnPropertyChanged(__this, e);"); + } + + // Close the method and return the 'Invoke' method as a delegate (just one allocation here) + writer.DecreaseIndent(); + writer.DecreaseIndent(); + writer.WriteLine(""" + } + + return new(Invoke); + } + """, isMultiline: true); + }); + + // Write the accessors for all WinRT-based callbacks (not the shared one) + foreach (DependencyPropertyInfo propertyInfo in propertyInfos.Where(static property => property.IsPropertyChangedCallbackImplemented)) + { + writer.WriteLine(); + writer.WriteLine($""" + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "On{propertyInfo.PropertyName}PropertyChanged")] + private static extern void On{propertyInfo.PropertyName}PropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); + """, isMultiline: true); + } + + // Also emit one for the shared callback, if it's ever used + if (propertyInfos.Any(static property => property.IsSharedPropertyChangedCallbackImplemented)) + { + writer.WriteLine(); + writer.WriteLine($""" + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + private static extern void OnPropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); + """, isMultiline: true); + } + } + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.cs new file mode 100644 index 000000000..d82418f23 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.cs @@ -0,0 +1,174 @@ +// 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.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A source generator creating implementations of dependency properties. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class DependencyPropertyGenerator : IIncrementalGenerator +{ + /// + /// The name of generator to include in the generated code. + /// + internal const string GeneratorName = "CommunityToolkit.WinUI.DependencyPropertyGenerator"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Generate the sources for the 'PrivateAssets="all"' mode + context.RegisterPostInitializationOutput(Execute.GeneratePostInitializationSources); + + // Get the info on all dependency properties to generate + IncrementalValuesProvider propertyInfo = + context.SyntaxProvider + .ForAttributeWithMetadataName( + WellKnownTypeNames.GeneratedDependencyPropertyAttribute, + Execute.IsCandidateSyntaxValid, + static (context, token) => + { + // We need C# 13, double check that it's the case + if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)) + { + return null; + } + + bool isLocalCachingEnabled = Execute.IsLocalCachingEnabled(context.Attributes[0]); + + // This generator requires C# preview to be used (due to the use of the 'field' keyword). + // The 'field' keyword is actually only used when local caching is enabled, so filter to that. + if (!isLocalCachingEnabled && !context.SemanticModel.Compilation.IsLanguageVersionPreview()) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Ensure we do have a property + if (context.TargetSymbol is not IPropertySymbol propertySymbol) + { + return null; + } + + // Do an initial filtering on the symbol as well + if (!Execute.IsCandidateSymbolValid(propertySymbol)) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Get the accessibility values, if the property is valid + if (!Execute.TryGetAccessibilityModifiers( + node: (PropertyDeclarationSyntax)context.TargetNode, + propertySymbol: propertySymbol, + out Accessibility declaredAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility)) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + string typeName = propertySymbol.Type.GetFullyQualifiedName(); + string typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + + token.ThrowIfCancellationRequested(); + + bool isRequired = Execute.IsRequiredProperty(propertySymbol); + bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol); + bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol); + bool isNet8OrGreater = !context.SemanticModel.Compilation.IsWindowsRuntimeApplication(); + + token.ThrowIfCancellationRequested(); + + // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. + // This will cover both reference types as well T when the constraints are not struct or unmanaged. + // If this is true, it means the field storage can potentially be in a null state (even if not annotated). + bool isReferenceTypeOrUnconstraindTypeParameter = !propertySymbol.Type.IsValueType; + + // Also get the default value (this might be slightly expensive, so do it towards the end) + TypedConstantInfo defaultValue = Execute.GetDefaultValue( + context.Attributes[0], + propertySymbol, + context.SemanticModel, + token); + + // The 'UnsetValue' can only be used when local caching is disabled + if (defaultValue is TypedConstantInfo.UnsetValue && isLocalCachingEnabled) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Finally, get the hierarchy too + HierarchyInfo hierarchyInfo = HierarchyInfo.From(propertySymbol.ContainingType); + + token.ThrowIfCancellationRequested(); + + return new DependencyPropertyInfo( + Hierarchy: hierarchyInfo, + PropertyName: propertySymbol.Name, + DeclaredAccessibility: declaredAccessibility, + GetterAccessibility: getterAccessibility, + SetterAccessibility: setterAccessibility, + TypeName: typeName, + TypeNameWithNullabilityAnnotations: typeNameWithNullabilityAnnotations, + DefaultValue: defaultValue, + IsReferenceTypeOrUnconstraindTypeParameter: isReferenceTypeOrUnconstraindTypeParameter, + IsRequired: isRequired, + IsLocalCachingEnabled: isLocalCachingEnabled, + IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, + IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, + IsNet8OrGreater: isNet8OrGreater); + }) + .WithTrackingName(WellKnownTrackingNames.Execute) + .Where(static item => item is not null)!; + + // Split and group by containing type + IncrementalValuesProvider> groupedPropertyInfo = + propertyInfo + .GroupBy( + keySelector: static item => item.Hierarchy, + elementSelector: static item => item, + resultSelector: static item => item.Values) + .WithTrackingName(WellKnownTrackingNames.Output); + + // Generate the source files, if any + context.RegisterSourceOutput(groupedPropertyInfo, static (context, item) => + { + using IndentedTextWriter writer = new(); + + item[0].Hierarchy.WriteSyntax( + state: item, + writer: writer, + baseTypes: [], + memberCallbacks: [Execute.WritePropertyDeclarations]); + + if (Execute.RequiresAdditionalTypes(item)) + { + writer.WriteLine(); + writer.WriteLine($"namespace {GeneratorName}"); + + using (writer.WriteBlock()) + { + Execute.WriteAdditionalTypes(item, writer); + } + } + + context.AddSource($"{item[0].Hierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs new file mode 100644 index 000000000..1206c2fef --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs @@ -0,0 +1,70 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when a property with [GeneratedDependencyProperty] would generate conflicts. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyConflictingDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [InvalidPropertyDeclarationWouldCauseConflicts]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + // Get the 'DependencyPropertyChangedEventArgs' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs) is not { } dependencyPropertyChangedEventArgsSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a property + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Same logic as 'IsCandidateSymbolValid' in the generator + if (propertySymbol.Name == "Property") + { + // Check for collisions with the generated helpers and the property, only happens with these 2 types + if (propertySymbol.Type.SpecialType == SpecialType.System_Object || + SymbolEqualityComparer.Default.Equals(propertySymbol.Type, dependencyPropertyChangedEventArgsSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationWouldCauseConflicts, + attributeData.GetLocation(), + propertySymbol)); + } + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs new file mode 100644 index 000000000..b99a9f5f0 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs @@ -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 System.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when a property with [GeneratedDependencyProperty] is in an invalid type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyContainingTypeDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [InvalidPropertyDeclarationContainingTypeIsNotDependencyObject]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + // Get the 'DependencyObject' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject) is not { } dependencyObjectSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a property + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Emit the diagnostic if the target is not valid + if (!propertySymbol.ContainingType.InheritsFromType(dependencyObjectSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationContainingTypeIsNotDependencyObject, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs new file mode 100644 index 000000000..457c6a353 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs @@ -0,0 +1,128 @@ +// 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.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used with an invalid default value type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyDefaultValueTypeAttribute : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDefaultValueNull, + InvalidPropertyDefaultValueType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property isn't using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Get the default value, if present (if it's not set, nothing to do) + if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + return; + } + + // Skip 'UnsetValue', that's special + + bool isNullableValueType = propertySymbol.Type is { IsValueType: true } and not INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + bool isNullableType = !propertySymbol.Type.IsValueType || isNullableValueType; + + // Check for invalid 'null' default values + if (defaultValue.IsNull && !isNullableType) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDefaultValueNull, + attributeData.GetLocation(), + propertySymbol, + propertySymbol.Type)); + + return; + } + + + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidPropertyDeclaration(propertyNode)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclaration, + attributeData.GetLocation(), + propertySymbol)); + + return; + } + } + }, SymbolKind.Property); + }); + } + + internal static bool IsDependencyPropertyUnsetValue( + AttributeData attributeData, + SemanticModel semanticModel, + CancellationToken token) + { + // If we do have a default value, we also want to check whether it's the special 'UnsetValue' placeholder. + // To do so, we get the application syntax, find the argument, then get the operation and inspect it. + if (attributeData.ApplicationSyntaxReference?.GetSyntax(token) is AttributeSyntax attributeSyntax) + { + foreach (AttributeArgumentSyntax attributeArgumentSyntax in attributeSyntax.ArgumentList?.Arguments ?? []) + { + // Let's see whether the current argument is the one that set the 'DefaultValue' property + if (attributeArgumentSyntax.NameEquals?.Name.Identifier.Text is "DefaultValue") + { + IOperation? operation = semanticModel.GetOperation(attributeArgumentSyntax.Expression, token); + + // Double check that it's a constant field reference (it could also be a literal of some kind, etc.) + if (operation is IFieldReferenceOperation { Field: { Name: "UnsetValue" } fieldSymbol }) + { + // Last step: we want to validate that the reference is actually to the special placeholder + if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName("CommunityToolkit.WinUI.GeneratedDependencyProperty")) + { + return true; + } + } + } + } + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs new file mode 100644 index 000000000..a7d99b6ff --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs @@ -0,0 +1,70 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning when a property with [GeneratedDependencyProperty] would generate a nullability annotations violation. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyNonNullableDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [NonNullablePropertyDeclarationIsNotEnforced]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + // Attempt to also get the '[MaybeNull]' symbols (there might be multiples, due to polyfills) + ImmutableArray maybeNullAttributeSymbol = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); + + context.RegisterSymbolAction(context => + { + // Validate that we do have a property, and that it is of some type that can be explicitly nullable. + // We're intentionally ignoring 'Nullable' values here, since those are by defintiion nullable. + // Additionally, we only care about properties that are explicitly marked as not nullable. + // Lastly, we can skip required properties, since for those it's completely fine to be non-nullable. + if (context.Symbol is not IPropertySymbol { Type.IsValueType: false, NullableAnnotation: NullableAnnotation.NotAnnotated, IsRequired: false } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // If the property has '[MaybeNull]', we never need to emit a diagnostic + if (propertySymbol.HasAttributeWithAnyType(maybeNullAttributeSymbol)) + { + return; + } + + // Emit a diagnostic if there is no default value, or if it's 'null' + if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue) || defaultValue.IsNull) + { + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs new file mode 100644 index 000000000..c82c02df0 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs @@ -0,0 +1,100 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertySymbolDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDeclarationIsNotIncompletePartialDefinition, + InvalidPropertyDeclarationReturnsByRef, + InvalidPropertyDeclarationReturnsRefLikeType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + // This generator is intentionally also analyzing generated code, because Roslyn will interpret properties + // that have '[GeneratedCode]' on them as being generated (and the same will apply to all partial parts). + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + // Get the '[GeneratedCode]' symbol + if (context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Emit an error if the property is not a partial definition with no implementation... + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + // ...But only if it wasn't actually generated by the [ObservableProperty] generator. + bool isImplementationAllowed = + propertySymbol is { IsPartialDefinition: true, PartialImplementationPart: IPropertySymbol implementationPartSymbol } && + implementationPartSymbol.TryGetAttributeWithType(generatedCodeAttributeSymbol, out AttributeData? generatedCodeAttributeData) && + generatedCodeAttributeData.TryGetConstructorArgument(0, out string? toolName) && + toolName == DependencyPropertyGenerator.GeneratorName; + + // Emit the diagnostic only for cases that were not valid generator outputs + if (!isImplementationAllowed) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationIsNotIncompletePartialDefinition, + attributeData.GetLocation(), + propertySymbol)); + } + } + + // Emit an error if the property returns a value by ref + if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationReturnsByRef, + attributeData.GetLocation(), + propertySymbol)); + } + + // Emit an error if the property type is a ref struct + if (propertySymbol.Type.IsRefLikeType) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationReturnsRefLikeType, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs new file mode 100644 index 000000000..8b5d6b157 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs @@ -0,0 +1,102 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertySyntaxDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [InvalidPropertyDeclaration]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property isn't using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Check that the property has valid syntax + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidPropertyDeclaration(propertyNode)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclaration, + attributeData.GetLocation(), + propertySymbol)); + + return; + } + } + }, SymbolKind.Property); + }); + } + + /// + /// Checks whether a given property declaration has valid syntax. + /// + /// The input node to validate. + /// Whether is a valid property. + internal static bool IsValidPropertyDeclaration(SyntaxNode node) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) + { + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return false; + } + + // Static properties are not supported + if (property.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + return false; + } + + // The accessors must be a get and a set (with any accessibility) + if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || + accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) + { + return false; + } + + return true; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs new file mode 100644 index 000000000..200769544 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -0,0 +1,78 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when using [GeneratedDependencyProperty] without the right C# version. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnsupportedCSharpLanguageVersionAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + PropertyDeclarationRequiresCSharp13, + LocalCachingRequiresCSharpPreview + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // If we're using C# 'preview', we'll never emit any errors + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + bool isLocalCachingEnabled = attributeData.GetNamedArgument("IsLocalCacheEnabled", defaultValue: false); + + // Emit only up to one diagnostic, for whichever the highest required C# version would be + if (isLocalCachingEnabled && !context.Compilation.IsLanguageVersionPreview()) + { + context.ReportDiagnostic(Diagnostic.Create( + LocalCachingRequiresCSharpPreview, + attributeData.GetLocation(), + propertySymbol)); + } + else if (!isLocalCachingEnabled && !context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)) + { + context.ReportDiagnostic(Diagnostic.Create( + PropertyDeclarationRequiresCSharp13, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 000000000..213b3e8d7 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,156 @@ +// 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.GeneratedDependencyProperty.Diagnostics; + +/// +/// A container for all instances for errors reported by analyzers in this project. +/// +internal static class DiagnosticDescriptors +{ + /// + /// "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclaration = new( + id: "WCTDP0001", + title: "Invalid property declaration for [GeneratedDependencyProperty]", + messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must be instance (non static) partial properties, with a getter and a setter that is not init-only.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationIsNotIncompletePartialDefinition = new( + id: "WCTDP0002", + title: "Using [GeneratedDependencyProperty] on an invalid partial property (not incomplete partial definition)", + messageFormat: """The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [GeneratedDependencyProperty] is not a partial implementation part ([GeneratedDependencyProperty] must be used on partial property definitions with no implementation part).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsByRef = new( + id: "WCTDP0003", + title: "Using [GeneratedDependencyProperty] on a property that returns byref", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not return a ref value (only reference types and non byref-like types are supported).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsRefLikeType = new( + id: "WCTDP0004", + title: "Using [GeneratedDependencyProperty] on a property that returns byref-like", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not return a byref-like value (only reference types and non byref-like types are supported).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationContainingTypeIsNotDependencyObject = new( + id: "WCTDP0005", + title: "Using [GeneratedDependencyProperty] on a property with invalid containing type", + messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must be contained in a type that inherits from DependencyObject.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 13 or greater (add 13.0 to your .csproj/.props file). + /// + public static readonly DiagnosticDescriptor PropertyDeclarationRequiresCSharp13 = new( + id: "WCTDP0006", + title: "Using [GeneratedDependencyProperty] requires C# 13", + messageFormat: "The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 13 or greater (add 13.0 to your .csproj/.props file)", + category: typeof(UnsupportedCSharpLanguageVersionAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must be contained in a project using C# 13 or greater. Make sure to add 13.0 to your .csproj/.props file.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file). + /// + public static readonly DiagnosticDescriptor LocalCachingRequiresCSharpPreview = new( + id: "WCTDP0007", + title: "Using [GeneratedDependencyProperty] with 'IsLocalCachingEnabled' requires C# 'preview'", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and using the 'IsLocalCachingEnabled' option must be contained in a project using C# 'preview'. Make sure to add preview to your .csproj/.props file.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs'). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationWouldCauseConflicts = new( + id: "WCTDP0008", + title: "Conflicting property declaration for [GeneratedDependencyProperty]", + messageFormat: "The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs')", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not be declared in such a way that would cause generate members to cause conflicts. In particular, they cannot be named 'Property' and be of type either 'object' or 'DependencyPropertyChangedEventArgs'.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable). + /// + public static readonly DiagnosticDescriptor NonNullablePropertyDeclarationIsNotEnforced = new( + id: "WCTDP0009", + title: "Non-nullable dependency property is not guaranteed to not be null", + messageFormat: "The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Non-nullable properties annotated with [GeneratedDependencyProperty] should guarantee that their values will not be null upon exiting the constructor. This can be enforced by adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueNull = new( + id: "WCTDP0010", + title: "Invalid 'null' default value for [GeneratedDependencyProperty] use", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// + public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueType = new( + id: "WCTDP0011", + title: "Invalid default value type for [GeneratedDependencyProperty] use", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AccessibilityExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AccessibilityExtensions.cs new file mode 100644 index 000000000..413ecc2c7 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AccessibilityExtensions.cs @@ -0,0 +1,32 @@ +// 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.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AccessibilityExtensions +{ + /// + /// Gets the expression for a given value. + /// + /// The input value. + /// The expression for . + public static string GetExpression(this Accessibility accessibility) + { + return accessibility switch + { + Accessibility.Private => "private", + Accessibility.ProtectedAndInternal => "private protected", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.ProtectedOrInternal => "protected internal", + Accessibility.Public => "public", + _ => "" + }; + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AttributeDataExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AttributeDataExtensions.cs new file mode 100644 index 000000000..ef945b78b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -0,0 +1,117 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AttributeDataExtensions +{ + /// + /// Tries to get the location of the input instance. + /// + /// The input instance to get the location for. + /// The resulting location for , if a syntax reference is available. + public static Location? GetLocation(this AttributeData attributeData) + { + if (attributeData.ApplicationSyntaxReference is { } syntaxReference) + { + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + } + + return null; + } + + /// + /// Tries to get a constructor argument at a given index from the input instance. + /// + /// The type of constructor argument to retrieve. + /// The target instance to get the argument from. + /// The index of the argument to try to retrieve. + /// The resulting argument, if it was found. + /// Whether or not an argument of type at position was found. + public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result) + { + if (attributeData.ConstructorArguments.Length > index && + attributeData.ConstructorArguments[index].Value is T argument) + { + result = argument; + + return true; + } + + result = default; + + return false; + } + + /// + /// Tries to get a given named argument value from an instance, or a default value. + /// + /// The type of argument to check. + /// The target instance to check. + /// The name of the argument to check. + /// The default value to return if the argument is not found. + /// The argument value, or . + public static T? GetNamedArgument(this AttributeData attributeData, string name, T? defaultValue = default) + { + if (TryGetNamedArgument(attributeData, name, out T? value)) + { + return value; + } + + return defaultValue; + } + + /// + /// Tries to get a given named argument value from an instance, if present. + /// + /// The type of argument to check. + /// The target instance to check. + /// The name of the argument to check. + /// The resulting argument value, if present. + /// Whether or not contains an argument named with a valid value. + public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out T? value) + { + if (TryGetNamedArgument(attributeData, name, out TypedConstant constantValue)) + { + value = (T?)constantValue.Value; + + return true; + } + + value = default; + + return false; + } + + /// + /// Tries to get a given named argument value from an instance, if present. + /// + /// The target instance to check. + /// The name of the argument to check. + /// The resulting argument value, if present. + /// Whether or not contains an argument named with a valid value. + public static bool TryGetNamedArgument(this AttributeData attributeData, string name, out TypedConstant value) + { + foreach (KeyValuePair argument in attributeData.NamedArguments) + { + if (argument.Key == name) + { + value = argument.Value; + + return true; + } + } + + value = default; + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/CompilationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/CompilationExtensions.cs new file mode 100644 index 000000000..ff06af81c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/CompilationExtensions.cs @@ -0,0 +1,45 @@ +// 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.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class CompilationExtensions +{ + /// + /// Checks whether a given compilation (assumed to be for C#) is using at least a given language version. + /// + /// The to consider for analysis. + /// The minimum language version to check. + /// Whether is using at least the specified language version. + public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation, LanguageVersion languageVersion) + { + return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion; + } + + /// + /// Checks whether a given compilation (assumed to be for C#) is using the preview language version. + /// + /// The to consider for analysis. + /// Whether is using the preview language version. + public static bool IsLanguageVersionPreview(this Compilation compilation) + { + return ((CSharpCompilation)compilation).LanguageVersion == LanguageVersion.Preview; + } + + /// + /// Gets whether the current target is a WinRT application (ie. legacy UWP). + /// + /// The input instance to inspect. + /// Whether the current target is a WinRT application. + public static bool IsWindowsRuntimeApplication(this Compilation compilation) + { + return compilation.Options.OutputKind == OutputKind.WindowsRuntimeApplication; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ISymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ISymbolExtensions.cs new file mode 100644 index 000000000..c49e99c1c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ISymbolExtensions.cs @@ -0,0 +1,94 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ISymbolExtensions +{ + /// + /// Gets the fully qualified name for a given symbol (without nullability annotations). + /// + /// The input instance. + /// The fully qualified name for . + public static string GetFullyQualifiedName(this ISymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + /// + /// Gets the fully qualified name for a given symbol, including nullability annotations + /// + /// The input instance. + /// The fully qualified name for . + public static string GetFullyQualifiedNameWithNullabilityAnnotations(this ISymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); + } + + /// + /// Checks whether or not a given symbol has an attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// Whether or not has an attribute with the specified type. + public static bool HasAttributeWithAnyType(this ISymbol symbol, ImmutableArray typeSymbols) + { + return TryGetAttributeWithAnyType(symbol, typeSymbols, out _); + } + + /// + /// Tries to get an attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The resulting attribute, if it was found. + /// Whether or not has an attribute with the specified type. + public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + + return false; + } + + /// + /// Tries to get an attribute with any of the specified types. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The first attribute of a type matching any type in , if found. + /// Whether or not has an attribute with the specified type. + public static bool TryGetAttributeWithAnyType(this ISymbol symbol, ImmutableArray typeSymbols, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (typeSymbols.Contains(attribute.AttributeClass!, SymbolEqualityComparer.Default)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 000000000..df8431653 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,139 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ITypeSymbolExtensions +{ + /// + /// Checks whether or not a given type symbol has a specified fully qualified metadata name. + /// + /// The input instance to check. + /// The full name to check. + /// Whether has a full name equals to . + public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name) + { + using ImmutableArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.WrittenSpan.SequenceEqual(name.AsSpan()); + } + + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The instance to check for inheritance from. + /// Whether or not inherits from . + public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) + { + INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType; + + while (currentBaseTypeSymbol is not null) + { + if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol)) + { + return true; + } + + currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType; + } + + return false; + } + + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The full name of the type to check for inheritance. + /// Whether or not inherits from . + public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name) + { + INamedTypeSymbol? baseType = typeSymbol.BaseType; + + while (baseType is not null) + { + if (baseType.HasFullyQualifiedMetadataName(name)) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } + + /// + /// Gets the fully qualified metadata name for a given instance. + /// + /// The input instance. + /// The fully qualified metadata name for . + public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol) + { + using ImmutableArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.ToString(); + } + + /// + /// Appends the fully qualified metadata name for a given symbol to a target builder. + /// + /// The input instance. + /// The target instance. + public static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ref readonly ImmutableArrayBuilder builder) + { + static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (ie. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with no namespace just have their metadata name directly written + case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with a containing non-global namespace also append a leading '.' + case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: + BuildFrom(namespaceSymbol, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Nested types append a leading '+' + case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: + BuildFrom(typeSymbol, in builder); + builder.Add('+'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + BuildFrom(symbol, in builder); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IncrementalValueProviderExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IncrementalValueProviderExtensions.cs new file mode 100644 index 000000000..4dde6f3a7 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IncrementalValueProviderExtensions.cs @@ -0,0 +1,71 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for . +/// +internal static class IncrementalValuesProviderExtensions +{ + /// + /// Groups items in a given sequence by a specified key. + /// + /// The type of value that this source provides access to. + /// The type of grouped key elements. + /// The type of projected elements. + /// The type of resulting items. + /// The input instance. + /// The key selection . + /// The element selection . + /// The result selection . + /// An with the grouped results. + public static IncrementalValuesProvider GroupBy( + this IncrementalValuesProvider source, + Func keySelector, + Func elementSelector, + Func<(TKey Key, EquatableArray Values), TResult> resultSelector) + where TValues : IEquatable + where TKey : IEquatable + where TElement : IEquatable + where TResult : IEquatable + { + return source.Collect().SelectMany((item, token) => + { + Dictionary.Builder> map = []; + + foreach (TValues value in item) + { + TKey key = keySelector(value); + TElement element = elementSelector(value); + + if (!map.TryGetValue(key, out ImmutableArray.Builder builder)) + { + builder = ImmutableArray.CreateBuilder(); + + map.Add(key, builder); + } + + builder.Add(element); + } + + token.ThrowIfCancellationRequested(); + + using ImmutableArrayBuilder result = new(); + + foreach (KeyValuePair.Builder> entry in map) + { + result.Add(resultSelector((entry.Key, entry.Value.ToImmutable()))); + } + + return result.ToImmutable(); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IndentedTextWriterExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IndentedTextWriterExtensions.cs new file mode 100644 index 000000000..688db2077 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IndentedTextWriterExtensions.cs @@ -0,0 +1,104 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Helpers; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class IndentedTextWriterExtensions +{ + /// + /// Writes the following attributes into a target writer: + /// + /// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + /// [global::System.Diagnostics.DebuggerNonUserCode] + /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + /// + /// + /// The instance to write into. + /// The name of the generator. + /// Whether to use fully qualified type names or not. + /// Whether to also include the attribute for non-user code. + public static void WriteGeneratedAttributes( + this IndentedTextWriter writer, + string generatorName, + bool useFullyQualifiedTypeNames = true, + bool includeNonUserCodeAttributes = true) + { + // We can use this class to get the assembly, as all files for generators are just included + // via shared projects. As such, the assembly will be the same as the generator type itself. + Version assemblyVersion = typeof(IndentedTextWriterExtensions).Assembly.GetName().Version; + + if (useFullyQualifiedTypeNames) + { + writer.WriteLine($$"""[global::System.CodeDom.Compiler.GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); + + if (includeNonUserCodeAttributes) + { + writer.WriteLine($$"""[global::System.Diagnostics.DebuggerNonUserCode]"""); + writer.WriteLine($$"""[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"""); + } + } + else + { + writer.WriteLine($$"""[GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); + + if (includeNonUserCodeAttributes) + { + writer.WriteLine($$"""[DebuggerNonUserCode]"""); + writer.WriteLine($$"""[ExcludeFromCodeCoverage]"""); + } + } + } + + /// + /// Writes a series of members separated by one line between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteLineSeparatedMembers( + this IndentedTextWriter writer, + ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + if (i > 0) + { + writer.WriteLine(); + } + + callback(items[i], writer); + } + } + + /// + /// Writes a series of initialization expressions separated by a comma between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteInitializationExpressions( + this IndentedTextWriter writer, + ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + callback(items[i], writer); + + if (i < items.Length - 1) + { + writer.WriteLine(","); + } + } + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/SyntaxNodeExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/SyntaxNodeExtensions.cs new file mode 100644 index 000000000..73c835beb --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/SyntaxNodeExtensions.cs @@ -0,0 +1,76 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// A with some extension methods for C# syntax nodes. +/// +internal static partial class SyntaxNodeExtensions +{ + /// + /// Checks whether a given is a given type declaration with or potentially with any base types, using only syntax. + /// + /// The type of declaration to check for. + /// The input to check. + /// Whether is a given type declaration with or potentially with any base types. + public static bool IsTypeDeclarationWithOrPotentiallyWithBaseTypes(this SyntaxNode node) + where T : TypeDeclarationSyntax + { + // Immediately bail if the node is not a type declaration of the specified type + if (node is not T typeDeclaration) + { + return false; + } + + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + return typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } + + /// + public static TNode? FirstAncestor(this SyntaxNode node, Func? predicate = null, bool ascendOutOfTrivia = true) + where TNode : SyntaxNode + { + // Helper method ported from 'SyntaxNode' + static SyntaxNode? GetParent(SyntaxNode node, bool ascendOutOfTrivia) + { + SyntaxNode? parent = node.Parent; + + if (parent is null && ascendOutOfTrivia) + { + if (node is IStructuredTriviaSyntax structuredTrivia) + { + parent = structuredTrivia.ParentTrivia.Token.Parent; + } + } + + return parent; + } + + // Traverse all parents and find the first one of the target type + for (SyntaxNode? parentNode = GetParent(node, ascendOutOfTrivia); + parentNode is not null; + parentNode = GetParent(parentNode, ascendOutOfTrivia)) + { + if (parentNode is TNode candidateNode && predicate?.Invoke(candidateNode) != false) + { + return candidateNode; + } + } + + return null; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/EquatableArray{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/EquatableArray{T}.cs new file mode 100644 index 000000000..4a1d3605a --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/EquatableArray{T}.cs @@ -0,0 +1,216 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// Extensions for . +/// +internal static class EquatableArray +{ + /// + /// Creates an instance from a given . + /// + /// The type of items in the input array. + /// The input instance. + /// An instance from a given . + public static EquatableArray AsEquatableArray(this ImmutableArray array) + where T : IEquatable + { + return new(array); + } +} + +/// +/// An imutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +/// The input to wrap. +internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? array = ImmutableCollectionsMarshal.AsArray(array); + + /// + /// Gets a reference to an item at a specified position within the array. + /// + /// The index of the item to retrieve a reference to. + /// A reference to an item at a specified position within the array. + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref AsImmutableArray().ItemRef(index); + } + + /// + /// Gets a value indicating whether the current array is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsEmpty; + } + + /// + /// Gets a value indicating whether the current array is default or empty. + /// + public bool IsDefaultOrEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsDefaultOrEmpty; + } + + /// + /// Gets the length of the current array. + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().Length; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override unsafe int GetHashCode() + { + if (this.array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + if (typeof(T) == typeof(byte)) + { + ReadOnlySpan span = array; + ref T r0 = ref MemoryMarshal.GetReference(span); + ref byte r1 = ref Unsafe.As(ref r0); + + fixed (byte* p = &r1) + { + ReadOnlySpan bytes = new(p, span.Length); + + hashCode.AddBytes(bytes); + } + } + else + { + foreach (T item in array) + { + hashCode.Add(item); + } + } + + return hashCode.ToHashCode(); + } + + /// + /// Gets an instance from the current . + /// + /// The from the current . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return ImmutableCollectionsMarshal.AsImmutableArray(this.array); + } + + /// + /// Creates an instance from a given . + /// + /// The input instance. + /// An instance from a given . + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + /// + /// Copies the contents of this instance. to a mutable array. + /// + /// The newly instantiated array. + public T[] ToArray() + { + return [.. AsImmutableArray()]; + } + + /// + /// Gets an value to traverse items in the current array. + /// + /// An value to traverse items in the current array. + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator EquatableArray(ImmutableArray array) => FromImmutableArray(array); + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator ImmutableArray(EquatableArray array) => array.AsImmutableArray(); + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/HashCode.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/HashCode.cs new file mode 100644 index 000000000..8d9135cc7 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/HashCode.cs @@ -0,0 +1,503 @@ +// 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, IDE0009, IDE1006, IDE0048, CA1065 + +namespace System; + +/// +/// A polyfill type that mirrors some methods from on .7. +/// +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; + + /// + /// Initializes the default seed. + /// + /// A random seed. + private static unsafe uint GenerateGlobalSeed() + { + byte[] bytes = new byte[4]; + + RandomNumberGenerator.Create().GetBytes(bytes); + + return BitConverter.ToUInt32(bytes, 0); + } + + /// + /// Combines a value into a hash code. + /// + /// The type of the value to combine into the hash code. + /// The value to combine into the hash code. + /// The hash code that represents the value. + public static int Combine(T1 value) + { + uint hc1 = (uint)(value?.GetHashCode() ?? 0); + uint hash = MixEmptyState(); + + hash += 4; + hash = QueueRound(hash, hc1); + hash = MixFinal(hash); + + return (int)hash; + } + + /// + /// Combines two values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Combines three values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Combines four values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Combines five values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Combines six values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Combines seven values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The type of the seventh value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The seventh value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Combines eight values into a hash code. + /// + /// The type of the first value to combine into the hash code. + /// The type of the second value to combine into the hash code. + /// The type of the third value to combine into the hash code. + /// The type of the fourth value to combine into the hash code. + /// The type of the fifth value to combine into the hash code. + /// The type of the sixth value to combine into the hash code. + /// The type of the seventh value to combine into the hash code. + /// The type of the eighth value to combine into the hash code. + /// The first value to combine into the hash code. + /// The second value to combine into the hash code. + /// The third value to combine into the hash code. + /// The fourth value to combine into the hash code. + /// The fifth value to combine into the hash code. + /// The sixth value to combine into the hash code. + /// The seventh value to combine into the hash code. + /// The eighth value to combine into the hash code. + /// The hash code that represents the values. + public static int Combine(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; + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + /// The instance to use. + public void Add(T value, IEqualityComparer? comparer) + { + Add(value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + + /// + /// Adds a span of bytes to the hash code. + /// + /// The span. + public void AddBytes(ReadOnlySpan 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(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); + } + } + + /// + /// Gets the resulting hashcode from the current instance. + /// + /// The resulting hashcode from the current instance. + public readonly 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; + } + + /// + [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(); + } + + /// + [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(); + } + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ImmutableArrayBuilder{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ImmutableArrayBuilder{T}.cs new file mode 100644 index 000000000..8238f6b1f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ImmutableArrayBuilder{T}.cs @@ -0,0 +1,365 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +/// The type of items to create sequences for. +internal struct ImmutableArrayBuilder : IDisposable +{ + /// + /// The shared instance to share objects. + /// + private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); + + /// + /// The rented instance to use. + /// + private Writer? writer; + + /// + /// Creates a new object. + /// + public ImmutableArrayBuilder() + { + this.writer = SharedObjectPool.Allocate(); + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public readonly ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.writer!.WrittenSpan; + } + + /// + /// Gets the number of elements currently written in the current instance. + /// + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.writer!.Count; + } + + /// + /// Advances the current writer and gets a to the requested memory area. + /// + /// The requested size to advance by. + /// A to the requested memory area. + /// + /// No other data should be written to the builder while the returned + /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. + /// + public readonly Span Advance(int requestedSize) + { + return this.writer!.Advance(requestedSize); + } + + /// + public readonly void Add(T item) + { + this.writer!.Add(item); + } + + /// + /// Adds the specified items to the end of the array. + /// + /// The items to add at the end of the array. + public readonly void AddRange(ReadOnlySpan items) + { + this.writer!.AddRange(items); + } + + /// + public readonly void Clear() + { + this.writer!.Clear(); + } + + /// + /// Inserts an item to the builder at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The object to insert into the current instance. + public readonly void Insert(int index, T item) + { + this.writer!.Insert(index, item); + } + + /// + /// Gets an instance for the current builder. + /// + /// An instance for the current builder. + /// + /// The builder should not be mutated while an enumerator is in use. + /// + public readonly IEnumerable AsEnumerable() + { + return this.writer!; + } + + /// + public readonly ImmutableArray ToImmutable() + { + T[] array = this.writer!.WrittenSpan.ToArray(); + + return ImmutableCollectionsMarshal.AsImmutableArray(array); + } + + /// + public readonly T[] ToArray() + { + return this.writer!.WrittenSpan.ToArray(); + } + + /// + public override readonly string ToString() + { + return this.writer!.WrittenSpan.ToString(); + } + + /// + public void Dispose() + { + Writer? writer = this.writer; + + this.writer = null; + + if (writer is not null) + { + writer.Clear(); + + SharedObjectPool.Free(writer); + } + } + + /// + /// A class handling the actual buffer writing. + /// + private sealed class Writer : IList, IReadOnlyList + { + /// + /// The underlying array. + /// + private T[] array; + + /// + /// The starting offset within . + /// + private int index; + + /// + /// Creates a new instance with the specified parameters. + /// + public Writer() + { + if (typeof(T) == typeof(char)) + { + this.array = new T[1024]; + } + else + { + this.array = new T[8]; + } + + this.index = 0; + } + + /// + public int Count => this.index; + + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.array, 0, this.index); + } + + /// + bool ICollection.IsReadOnly => true; + + /// + T IReadOnlyList.this[int index] => WrittenSpan[index]; + + /// + T IList.this[int index] + { + get => WrittenSpan[index]; + set => throw new NotSupportedException(); + } + + /// + public Span Advance(int requestedSize) + { + EnsureCapacity(requestedSize); + + Span span = this.array.AsSpan(this.index, requestedSize); + + this.index += requestedSize; + + return span; + } + + /// + public void Add(T value) + { + EnsureCapacity(1); + + this.array[this.index++] = value; + } + + /// + public void AddRange(ReadOnlySpan items) + { + EnsureCapacity(items.Length); + + items.CopyTo(this.array.AsSpan(this.index)); + + this.index += items.Length; + } + + /// + public void Clear(ReadOnlySpan items) + { + this.index = 0; + } + + /// + public void Insert(int index, T item) + { + if (index < 0 || index > this.index) + { + ImmutableArrayBuilder.ThrowArgumentOutOfRangeExceptionForIndex(); + } + + EnsureCapacity(1); + + if (index < this.index) + { + Array.Copy(this.array, index, this.array, index + 1, this.index - index); + } + + this.array[index] = item; + this.index++; + } + + /// + /// Clears the items in the current writer. + /// + public void Clear() + { + if (typeof(T) != typeof(byte) && + typeof(T) != typeof(char) && + typeof(T) != typeof(int)) + { + this.array.AsSpan(0, this.index).Clear(); + } + + this.index = 0; + } + + /// + /// Ensures that has enough free space to contain a given number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int requestedSize) + { + if (requestedSize > this.array.Length - this.index) + { + ResizeBuffer(requestedSize); + } + } + + /// + /// Resizes to ensure it can fit the specified number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int sizeHint) + { + int minimumSize = this.index + sizeHint; + int requestedSize = Math.Max(this.array.Length * 2, minimumSize); + + T[] newArray = new T[requestedSize]; + + Array.Copy(this.array, newArray, this.index); + + this.array = newArray; + } + + /// + int IList.IndexOf(T item) + { + return Array.IndexOf(this.array, item, 0, this.index); + } + + /// + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } + + /// + bool ICollection.Contains(T item) + { + return Array.IndexOf(this.array, item, 0, this.index) >= 0; + } + + /// + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(this.array, 0, array, arrayIndex, this.index); + } + + /// + bool ICollection.Remove(T item) + { + throw new NotSupportedException(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + T?[] array = this.array!; + int length = this.index; + + for (int i = 0; i < length; i++) + { + yield return array[i]!; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + } +} + +/// +/// Private helpers for the type. +/// +file static class ImmutableArrayBuilder +{ + /// + /// Throws an for "index". + /// + public static void ThrowArgumentOutOfRangeExceptionForIndex() + { + throw new ArgumentOutOfRangeException("index"); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/IndentedTextWriter.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/IndentedTextWriter.cs new file mode 100644 index 000000000..9bfc74b65 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/IndentedTextWriter.cs @@ -0,0 +1,515 @@ +// 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.ComponentModel; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +#pragma warning disable IDE0290 + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +internal sealed class IndentedTextWriter : IDisposable +{ + /// + /// The default indentation (4 spaces). + /// + private const string DefaultIndentation = " "; + + /// + /// The default new line ('\n'). + /// + private const char DefaultNewLine = '\n'; + + /// + /// The instance that text will be written to. + /// + private ImmutableArrayBuilder builder; + + /// + /// The current indentation level. + /// + private int currentIndentationLevel; + + /// + /// The current indentation, as text. + /// + private string currentIndentation = ""; + + /// + /// The cached array of available indentations, as text. + /// + private string[] availableIndentations; + + /// + /// Creates a new object. + /// + public IndentedTextWriter() + { + this.builder = new ImmutableArrayBuilder(); + this.currentIndentationLevel = 0; + this.currentIndentation = ""; + this.availableIndentations = new string[4]; + this.availableIndentations[0] = ""; + + for (int i = 1, n = this.availableIndentations.Length; i < n; i++) + { + this.availableIndentations[i] = this.availableIndentations[i - 1] + DefaultIndentation; + } + } + + /// + /// Advances the current writer and gets a to the requested memory area. + /// + /// The requested size to advance by. + /// A to the requested memory area. + /// + /// No other data should be written to the writer while the returned + /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. + /// + public Span Advance(int requestedSize) + { + // Add the leading whitespace if needed (same as WriteRawText below) + if (this.builder.Count == 0 || this.builder.WrittenSpan[^1] == DefaultNewLine) + { + this.builder.AddRange(this.currentIndentation.AsSpan()); + } + + return this.builder.Advance(requestedSize); + } + + /// + /// Increases the current indentation level. + /// + public void IncreaseIndent() + { + this.currentIndentationLevel++; + + if (this.currentIndentationLevel == this.availableIndentations.Length) + { + Array.Resize(ref this.availableIndentations, this.availableIndentations.Length * 2); + } + + // Set both the current indentation and the current position in the indentations + // array to the expected indentation for the incremented level (ie. one level more). + this.currentIndentation = this.availableIndentations[this.currentIndentationLevel] + ??= this.availableIndentations[this.currentIndentationLevel - 1] + DefaultIndentation; + } + + /// + /// Decreases the current indentation level. + /// + public void DecreaseIndent() + { + this.currentIndentationLevel--; + this.currentIndentation = this.availableIndentations[this.currentIndentationLevel]; + } + + /// + /// Writes a block to the underlying buffer. + /// + /// A value to close the open block with. + public Block WriteBlock() + { + WriteLine("{"); + IncreaseIndent(); + + return new(this); + } + + /// + /// Writes content to the underlying buffer. + /// + /// The content to write. + /// Whether the input content is multiline. + public void Write(string content, bool isMultiline = false) + { + Write(content.AsSpan(), isMultiline); + } + + /// + /// Writes content to the underlying buffer. + /// + /// The content to write. + /// Whether the input content is multiline. + public void Write(ReadOnlySpan content, bool isMultiline = false) + { + if (isMultiline) + { + while (content.Length > 0) + { + int newLineIndex = content.IndexOf(DefaultNewLine); + + if (newLineIndex < 0) + { + // There are no new lines left, so the content can be written as a single line + WriteRawText(content); + + break; + } + else + { + ReadOnlySpan line = content[..newLineIndex]; + + // Write the current line (if it's empty, we can skip writing the text entirely). + // This ensures that raw multiline string literals with blank lines don't have + // extra whitespace at the start of those lines, which would otherwise happen. + WriteIf(!line.IsEmpty, line); + WriteLine(); + + // Move past the new line character (the result could be an empty span) + content = content[(newLineIndex + 1)..]; + } + } + } + else + { + WriteRawText(content); + } + } + + /// + /// Writes content to the underlying buffer. + /// + /// The interpolated string handler with content to write. + public void Write([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) + { + _ = this; + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + Write(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteIf(bool condition, ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + } + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The interpolated string handler with content to write. + public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) + { + _ = this; + } + + /// + /// Writes a line to the underlying buffer. + /// + /// Indicates whether to skip adding the line if there already is one. + public void WriteLine(bool skipIfPresent = false) + { + if (skipIfPresent && this.builder.WrittenSpan is [.., '\n', '\n']) + { + return; + } + + this.builder.Add(DefaultNewLine); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The content to write. + /// Whether the input content is multiline. + public void WriteLine(string content, bool isMultiline = false) + { + WriteLine(content.AsSpan(), isMultiline); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The content to write. + /// Whether the input content is multiline. + public void WriteLine(ReadOnlySpan content, bool isMultiline = false) + { + Write(content, isMultiline); + WriteLine(); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The interpolated string handler with content to write. + public void WriteLine([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) + { + WriteLine(); + } + + /// + /// Writes a line to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// Indicates whether to skip adding the line if there already is one. + public void WriteLineIf(bool condition, bool skipIfPresent = false) + { + if (condition) + { + WriteLine(skipIfPresent); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteLineIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + WriteLine(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteLineIf(bool condition, ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + WriteLine(); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The interpolated string handler with content to write. + public void WriteLineIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) + { + if (condition) + { + WriteLine(); + } + } + + /// + public override string ToString() + { + return this.builder.WrittenSpan.Trim().ToString(); + } + + /// + public void Dispose() + { + this.builder.Dispose(); + } + + /// + /// Writes raw text to the underlying buffer, adding leading indentation if needed. + /// + /// The raw text to write. + private void WriteRawText(ReadOnlySpan content) + { + if (this.builder.Count == 0 || this.builder.WrittenSpan[^1] == DefaultNewLine) + { + this.builder.AddRange(this.currentIndentation.AsSpan()); + } + + this.builder.AddRange(content); + } + + /// + /// A delegate representing a callback to write data into an instance. + /// + /// The type of data to use. + /// The input data to use to write into . + /// The instance to write into. + public delegate void Callback(T value, IndentedTextWriter writer); + + /// + /// Represents an indented block that needs to be closed. + /// + /// The input instance to wrap. + public struct Block(IndentedTextWriter writer) : IDisposable + { + /// + /// The instance to write to. + /// + private IndentedTextWriter? writer = writer; + + /// + public void Dispose() + { + IndentedTextWriter? writer = this.writer; + + this.writer = null; + + if (writer is not null) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + } + + /// + /// Provides a handler used by the language compiler to append interpolated strings into instances. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public readonly ref struct WriteInterpolatedStringHandler + { + /// The associated to which to append. + private readonly IndentedTextWriter writer; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated to which to append. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer) + { + this.writer = writer; + } + + /// Writes the specified string to the handler. + /// The string to write. + public void AppendLiteral(string value) + { + this.writer.Write(value); + } + + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(string? value) + { + AppendFormatted(value); + } + + /// Writes the specified character span to the handler. + /// The span to write. + public void AppendFormatted(ReadOnlySpan value) + { + this.writer.Write(value); + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The type of the value to write. + public void AppendFormatted(T value) + { + if (value is not null) + { + this.writer.Write(value.ToString()); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// The type of the value to write. + public void AppendFormatted(T value, string? format) + { + if (value is IFormattable) + { + this.writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); + } + else if (value is not null) + { + this.writer.Write(value.ToString()); + } + } + } + + /// + /// Provides a handler used by the language compiler to conditionally append interpolated strings into instances. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public readonly ref struct WriteIfInterpolatedStringHandler + { + /// The associated to use. + private readonly WriteInterpolatedStringHandler handler; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated to which to append. + /// The condition to use to decide whether or not to write content. + /// A value indicating whether formatting should proceed. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend) + { + if (condition) + { + this.handler = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer); + + shouldAppend = true; + } + else + { + this.handler = default; + + shouldAppend = false; + } + } + + /// + public void AppendLiteral(string value) + { + this.handler.AppendLiteral(value); + } + + /// + public void AppendFormatted(string? value) + { + this.handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(ReadOnlySpan value) + { + this.handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(T value) + { + this.handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(T value, string? format) + { + this.handler.AppendFormatted(value, format); + } + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ObjectPool{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ObjectPool{T}.cs new file mode 100644 index 000000000..723d0a73f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ObjectPool{T}.cs @@ -0,0 +1,154 @@ +// 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. + +// Ported from Roslyn, see: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +#pragma warning disable RS1035 + +namespace CommunityToolkit.GeneratedDependencyProperty.Helpers; + +/// +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose +/// is that limited number of frequently used objects can be kept in the pool for further recycling. +/// +/// +/// Notes: +/// +/// +/// It is not the goal to keep all returned objects. Pool is not meant for storage. If there +/// is no space in the pool, extra returned objects will be dropped. +/// +/// +/// It is implied that if object was obtained from a pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is ok, but +/// reduces usefulness of pooling. Just new up your own. +/// +/// +/// +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: if there is no intent for reusing the object, do not use pool - just use "new". +/// +/// +/// The type of objects to pool. +/// The input factory to produce items. +/// +/// The factory is stored for the lifetime of the pool. We will call this only when pool needs to +/// expand. compared to "new T()", Func gives more flexibility to implementers and faster than "new T()". +/// +/// The pool size to use. +internal sealed class ObjectPool(Func factory, int size) + where T : class +{ + /// + /// The array of cached items. + /// + private readonly Element[] items = new Element[size - 1]; + + /// + /// Storage for the pool objects. The first item is stored in a dedicated field + /// because we expect to be able to satisfy most requests from it. + /// + private T? firstItem; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The input factory to produce items. + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + /// + /// Produces a instance. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + T? item = this.firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref this.firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + /// + /// Returns a given instance to the pool. + /// + /// The instance to return. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Free(T obj) + { + if (this.firstItem is null) + { + this.firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + /// + /// Allocates a new item. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.NoInlining)] + private T AllocateSlow() + { + foreach (ref Element element in this.items.AsSpan()) + { + T? instance = element.Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + } + + return factory(); + } + + /// + /// Frees a given item. + /// + /// The item to return to the pool. + [MethodImpl(MethodImplOptions.NoInlining)] + private void FreeSlow(T obj) + { + foreach (ref Element element in this.items.AsSpan()) + { + if (element.Value is null) + { + element.Value = obj; + + break; + } + } + } + + /// + /// A container for a produced item (using a wrapper to avoid covariance checks). + /// + private struct Element + { + /// + /// The value held at the current element. + /// + internal T? Value; + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/DependencyPropertyInfo.cs new file mode 100644 index 000000000..3952dd95d --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/DependencyPropertyInfo.cs @@ -0,0 +1,40 @@ +// 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.GeneratedDependencyProperty.Models; + +/// +/// A model representing a generated dependency property. +/// +/// The hierarchy info for the containing type. +/// The property name. +/// The accessibility of the property, if available. +/// The accessibility of the accessor, if available. +/// The accessibility of the accessor, if available. +/// The type name for the generated property (without nullability annotations). +/// The type name for the generated property, including nullability annotations. +/// The default value to set the generated property to. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. +/// Whether or not the generated property should be marked as required. +/// Indicates whether local caching should be used for the property value. +/// Indicates whether the WinRT-based property changed callback is implemented. +/// Indicates whether the WinRT-based shared property changed callback is implemented. +/// Indicates whether the current target is .NET 8 or greater. +internal sealed record DependencyPropertyInfo( + HierarchyInfo Hierarchy, + string PropertyName, + Accessibility DeclaredAccessibility, + Accessibility GetterAccessibility, + Accessibility SetterAccessibility, + string TypeName, + string TypeNameWithNullabilityAnnotations, + TypedConstantInfo DefaultValue, + bool IsReferenceTypeOrUnconstraindTypeParameter, + bool IsRequired, + bool IsLocalCachingEnabled, + bool IsPropertyChangedCallbackImplemented, + bool IsSharedPropertyChangedCallbackImplemented, + bool IsNet8OrGreater); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/HierarchyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/HierarchyInfo.cs new file mode 100644 index 000000000..67393f1e9 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/HierarchyInfo.cs @@ -0,0 +1,140 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model describing the hierarchy info for a specific type. +/// +/// The fully qualified metadata name for the current type. +/// Gets the namespace for the current type. +/// Gets the sequence of type definitions containing the current type. +internal sealed partial record HierarchyInfo(string FullyQualifiedMetadataName, string Namespace, EquatableArray Hierarchy) +{ + /// + /// Creates a new instance from a given . + /// + /// The input instance to gather info for. + /// A instance describing . + public static HierarchyInfo From(INamedTypeSymbol typeSymbol) + { + using ImmutableArrayBuilder hierarchy = new(); + + for (INamedTypeSymbol? parent = typeSymbol; + parent is not null; + parent = parent.ContainingType) + { + hierarchy.Add(new TypeInfo( + parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.IsRecord)); + } + + return new( + typeSymbol.GetFullyQualifiedMetadataName(), + typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + hierarchy.ToImmutable()); + } + + /// + /// Writes syntax for the current hierarchy into a target writer. + /// + /// The type of state to pass to callbacks. + /// The input state to pass to callbacks. + /// The target instance to write text to. + /// A list of base types to add to the generated type, if any. + /// The callbacks to use to write members into the declared type. + public void WriteSyntax( + T state, + IndentedTextWriter writer, + ReadOnlySpan baseTypes, + ReadOnlySpan> memberCallbacks) + { + // Write the generated file header + writer.WriteLine("// "); + writer.WriteLine("#pragma warning disable"); + writer.WriteLine("#nullable enable"); + writer.WriteLine(); + + // Declare the namespace, if needed + if (Namespace.Length > 0) + { + writer.WriteLine($"namespace {Namespace}"); + writer.WriteLine("{"); + writer.IncreaseIndent(); + } + + // Declare all the opening types until the inner-most one + for (int i = Hierarchy.Length - 1; i >= 0; i--) + { + writer.WriteLine($$"""/// """); + writer.Write($$"""partial {{Hierarchy[i].GetTypeKeyword()}} {{Hierarchy[i].QualifiedName}}"""); + + // Add any base types, if needed + if (i == 0 && !baseTypes.IsEmpty) + { + writer.Write(" : "); + writer.WriteInitializationExpressions(baseTypes, static (item, writer) => writer.Write(item)); + writer.WriteLine(); + } + else + { + writer.WriteLine(); + } + + writer.WriteLine($$"""{"""); + writer.IncreaseIndent(); + } + + // Generate all nested members + writer.WriteLineSeparatedMembers(memberCallbacks, (callback, writer) => callback(state, writer)); + + // Close all scopes and reduce the indentation + for (int i = 0; i < Hierarchy.Length; i++) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + + // Close the namespace scope as well, if needed + if (Namespace.Length > 0) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + + /// + /// Gets the fully qualified type name for the current instance. + /// + /// The fully qualified type name for the current instance. + public string GetFullyQualifiedTypeName() + { + using ImmutableArrayBuilder fullyQualifiedTypeName = new(); + + fullyQualifiedTypeName.AddRange("global::".AsSpan()); + + if (Namespace.Length > 0) + { + fullyQualifiedTypeName.AddRange(Namespace.AsSpan()); + fullyQualifiedTypeName.Add('.'); + } + + fullyQualifiedTypeName.AddRange(Hierarchy[^1].QualifiedName.AsSpan()); + + for (int i = Hierarchy.Length - 2; i >= 0; i--) + { + fullyQualifiedTypeName.Add('.'); + fullyQualifiedTypeName.AddRange(Hierarchy[i].QualifiedName.AsSpan()); + } + + return fullyQualifiedTypeName.ToString(); + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypeInfo.cs new file mode 100644 index 000000000..a1b5e7e02 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypeInfo.cs @@ -0,0 +1,32 @@ +// 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.GeneratedDependencyProperty.Models; + +/// +/// A model describing a type info in a type hierarchy. +/// +/// The qualified name for the type. +/// The type of the type in the hierarchy. +/// Whether the type is a record type. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) +{ + /// + /// Gets the keyword for the current type kind. + /// + /// The keyword for the current type kind. + public string GetTypeKeyword() + { + return Kind switch + { + TypeKind.Struct when IsRecord => "record struct", + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + TypeKind.Class when IsRecord => "record", + _ => "class" + }; + } +} \ No newline at end of file diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.Factory.cs new file mode 100644 index 000000000..bb030884c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.Factory.cs @@ -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.GeneratedDependencyProperty.Models; + +/// +partial record TypedConstantInfo +{ + /// + /// Creates a new instance from a given value. + /// + /// The input value. + /// A instance representing . + /// Thrown if the input argument is not valid. + public static TypedConstantInfo Create(TypedConstant arg) + { + if (arg.IsNull) + { + return new Null(); + } + + if (arg.Kind == TypedConstantKind.Array) + { + string elementTypeName = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + ImmutableArray items = arg.Values.Select(Create).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(b), + char c => new Primitive.Of(c), + double d => new Primitive.Of(d), + float f => new Primitive.Of(f), + int i => new Primitive.Of(i), + long l => new Primitive.Of(l), + sbyte sb => new Primitive.Of(sb), + short sh => new Primitive.Of(sh), + uint ui => new Primitive.Of(ui), + ulong ul => new Primitive.Of(ul), + ushort ush => new Primitive.Of(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"), + }; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.cs new file mode 100644 index 000000000..d06ef9618 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.cs @@ -0,0 +1,187 @@ +// 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.Globalization; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing a typed constant item. +/// +/// This model is fully serializable and comparable. +internal abstract partial record TypedConstantInfo +{ + /// + /// A type representing an array. + /// + /// The type name for array elements. + /// The sequence of contained elements. + public sealed record Array(string ElementTypeName, EquatableArray Items) : TypedConstantInfo + { + /// + public override string ToString() + { + ArrayCreationExpressionSyntax arrayCreationExpressionSyntax = + ArrayCreationExpression( + ArrayType(IdentifierName(ElementTypeName)) + .AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression())))) + .WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression) + .AddExpressions(Items.Select(static c => ParseExpression(c.ToString())).ToArray())); + + return arrayCreationExpressionSyntax.NormalizeWhitespace(eol: "\n").ToFullString(); + } + } + + /// + /// A type representing a primitive value. + /// + public abstract record Primitive : TypedConstantInfo + { + /// + /// A type representing a value. + /// + /// The input value. + public sealed record String(string Value) : TypedConstantInfo + { + /// + public override string ToString() + { + return '"' + Value + '"'; + } + } + + /// + /// A type representing a value. + /// + /// The input value. + public sealed record Boolean(bool Value) : TypedConstantInfo + { + /// + public override string ToString() + { + return Value ? "true" : "false"; + } + } + + /// + /// A type representing a generic primitive value. + /// + /// The primitive type. + /// The input primitive value. + public sealed record Of(T Value) : TypedConstantInfo + where T : unmanaged, IEquatable + { + /// + public override string ToString() + { + LiteralExpressionSyntax expressionSyntax = LiteralExpression(SyntaxKind.NumericLiteralExpression, Value switch + { + byte b => Literal(b), + char c => Literal(c), + + // For doubles, we need to manually format it and always add the trailing "D" suffix. + // This ensures that the correct type is produced if the expression was assigned to + // an object (eg. the literal was used in an attribute object parameter/property). + double d => Literal(d.ToString("R", CultureInfo.InvariantCulture) + "D", d), + + // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed + 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") + }); + + return expressionSyntax.NormalizeWhitespace(eol: "\n").ToFullString(); + } + } + } + + /// + /// A type representing a type. + /// + /// The input type name. + public sealed record Type(string TypeName) : TypedConstantInfo + { + /// + public override string ToString() + { + return $"typeof({TypeName})"; + } + } + + /// + /// A type representing an enum value. + /// + /// The enum type name. + /// The boxed enum value. + public sealed record Enum(string TypeName, object Value) : TypedConstantInfo + { + /// + public override string ToString() + { + // We let Roslyn parse the value expression, so that it can automatically handle both positive and negative values. This + // is needed because negative values have a different syntax tree (UnaryMinusExpression holding the numeric expression). + ExpressionSyntax valueExpression = ParseExpression(Value.ToString()); + + // If the value is negative, we have to put parentheses around them (to avoid CS0075 errors) + if (valueExpression is PrefixUnaryExpressionSyntax unaryExpression && unaryExpression.IsKind(SyntaxKind.UnaryMinusExpression)) + { + valueExpression = ParenthesizedExpression(valueExpression); + } + + // Now we can safely return the cast expression for the target enum type (with optional parentheses if needed) + return $"({TypeName}){valueExpression.NormalizeWhitespace(eol: "\n").ToFullString()}"; + } + } + + /// + /// A type representing a value. + /// + public sealed record Null : TypedConstantInfo + { + /// + public override string ToString() + { + return "null"; + } + } + + /// + /// A type representing default value for a specific type. + /// + /// The input type name. + public sealed record Default(string TypeName) : TypedConstantInfo + { + /// + public override string ToString() + { + return $"default({TypeName})"; + } + } + + /// + /// A type representing the special unset value. + /// + public sealed record UnsetValue : TypedConstantInfo + { + /// + public override string ToString() + { + return $"global::{WellKnownTypeNames.DependencyProperty}.UnsetValue"; + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj new file mode 100644 index 000000000..ca2d08512 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj @@ -0,0 +1,41 @@ + + + net8.0-windows10.0.17763.0 + true + false + + + $(NoWarn);NU1903 + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs new file mode 100644 index 000000000..10478a2bc --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using CommunityToolkit.WinUI; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to test. +internal sealed class CSharpAnalyzerTest : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + private CSharpAnalyzerTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } + + /// + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, params DiagnosticResult[] expected) + { + CSharpAnalyzerTest test = new(languageVersion) { TestCode = source }; + + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + + test.ExpectedDiagnostics.AddRange(expected); + + return test.RunAsync(CancellationToken.None); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs new file mode 100644 index 000000000..03d746354 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -0,0 +1,220 @@ +// 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.IO; +using System.Linq; +using Basic.Reference.Assemblies; +using CommunityToolkit.WinUI; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A helper type to run source generator tests. +/// +/// The type of generator to test. +internal static class CSharpGeneratorTest + where TGenerator : IIncrementalGenerator, new() +{ + /// + /// Verifies the resulting diagnostics from a source generator. + /// + /// The input source to process. + /// The expected diagnostics ids to be generated. + public static void VerifyDiagnostics(string source, params string[] diagnosticsIds) + { + RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics); + + Dictionary diagnosticMap = diagnostics.DistinctBy(diagnostic => diagnostic.Id).ToDictionary(diagnostic => diagnostic.Id); + + // Check that the diagnostics match + Assert.IsTrue(diagnosticMap.Keys.ToHashSet().SetEquals(diagnosticsIds), $"Diagnostics didn't match. {string.Join(", ", diagnosticMap.Values)}"); + + // If the compilation was supposed to succeed, ensure that no further errors were generated + if (diagnosticsIds.Length == 0) + { + // Compute diagnostics for the final compiled output (just include errors) + List outputCompilationDiagnostics = compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList(); + + Assert.IsTrue(outputCompilationDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", outputCompilationDiagnostics)}"); + } + } + + /// + /// Verifies the resulting sources produced by a source generator. + /// + /// The input source to process. + /// The expected source to be generated. + /// The language version to use to run the test. + public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); + + // Ensure that no diagnostics were generated + CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); + + // Update the assembly version using the version from the assembly of the input generators. + // This allows the tests to not need updates whenever the version of the MVVM Toolkit changes. + string expectedText = result.Source.Replace("", $"\"{typeof(TGenerator).Assembly.GetName().Version}\""); + string actualText = compilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == result.Filename).ToString(); + + Assert.AreEqual(expectedText, actualText); + } + + /// + /// Verifies the incremental generator steps for a given source generator. + /// + /// The input source to process. + /// The updated source to process. + /// The reason for the first "Execute" step. + /// The reason for the "Diagnostics" step. + /// The reason for the "Output" step. + /// The reason for the output step for the diagnostics. + /// The reason for the final output source. + /// The language version to use to run the test. + public static void VerifyIncrementalSteps( + string source, + string updatedSource, + IncrementalStepRunReason executeReason, + IncrementalStepRunReason? diagnosticsReason, + IncrementalStepRunReason outputReason, + IncrementalStepRunReason? diagnosticsSourceReason, + IncrementalStepRunReason sourceReason, + LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + Compilation compilation = CreateCompilation(source, languageVersion); + + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [new TGenerator().AsSourceGenerator()], + driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true)); + + // Run the generator on the initial sources + driver = driver.RunGenerators(compilation); + + // Update the compilation by replacing the source + compilation = compilation.ReplaceSyntaxTree( + compilation.SyntaxTrees.First(), + CSharpSyntaxTree.ParseText(updatedSource, CSharpParseOptions.Default.WithLanguageVersion(languageVersion))); + + // Run the generators again on the updated source + driver = driver.RunGenerators(compilation); + + GeneratorRunResult result = driver.GetRunResult().Results.Single(); + + // Get the generated sources and validate them. We have two possible cases: if no diagnostics + // are produced, then just the output source node is triggered. Otherwise, we'll also have one + // output node which is used to emit the gathered diagnostics from the initial transform step. + if (diagnosticsSourceReason is not null) + { + Assert.AreEqual( + expected: 2, + actual: + result.TrackedOutputSteps + .SelectMany(outputStep => outputStep.Value) + .SelectMany(output => output.Outputs) + .Count()); + + // The "Diagnostics" name has one more parent compared to "Output", because it also + // has one extra Where(...) call on the node (used to filter out empty diagnostics). + Assert.AreEqual( + expected: diagnosticsSourceReason, + actual: + result.TrackedOutputSteps + .Single().Value + .Single(run => run.Inputs[0].Source.Inputs[0].Source.Name == "Diagnostics") + .Outputs.Single().Reason); + + Assert.AreEqual( + expected: sourceReason, + actual: + result.TrackedOutputSteps + .Single().Value + .Single(run => run.Inputs[0].Source.Name == "Output") + .Outputs.Single().Reason); + } + else + { + (object Value, IncrementalStepRunReason Reason)[] sourceOuputs = + result.TrackedOutputSteps + .SelectMany(outputStep => outputStep.Value) + .SelectMany(output => output.Outputs) + .ToArray(); + + Assert.AreEqual(1, sourceOuputs.Length); + Assert.AreEqual(sourceReason, sourceOuputs[0].Reason); + } + + Assert.AreEqual(executeReason, result.TrackedSteps["Execute"].Single().Outputs[0].Reason); + Assert.AreEqual(outputReason, result.TrackedSteps["Output"].Single().Outputs[0].Reason); + + // Check the diagnostics reason, which might not be present + if (diagnosticsReason is not null) + { + Assert.AreEqual(diagnosticsReason, result.TrackedSteps["Diagnostics"].Single().Outputs[0].Reason); + } + else + { + Assert.IsFalse(result.TrackedSteps.ContainsKey("Diagnostics")); + } + } + + /// + /// Creates a compilation from a given source. + /// + /// The input source to process. + /// The language version to use to run the test. + /// The resulting object. + private static CSharpCompilation CreateCompilation(string source, LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + // Get all assembly references for the .NET TFM and ComputeSharp + IEnumerable metadataReferences = + [ + .. Net80.References.All, + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + ]; + + // Parse the source text + SyntaxTree sourceTree = CSharpSyntaxTree.ParseText( + source, + CSharpParseOptions.Default.WithLanguageVersion(languageVersion)); + + // Create the original compilation + return CSharpCompilation.Create( + "original", + [sourceTree], + metadataReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + } + + /// + /// Runs a generator and gathers the output results. + /// + /// The input source to process. + /// + /// + /// The language version to use to run the test. + private static void RunGenerator( + string source, + out Compilation compilation, + out ImmutableArray diagnostics, + LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + Compilation originalCompilation = CreateCompilation(source, languageVersion); + + // Create the generator driver with the D2D shader generator + GeneratorDriver driver = CSharpGeneratorDriver.Create(new TGenerator()).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); + + // Run all source generators on the input source code + _ = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out compilation, out diagnostics); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_Analyzers.cs new file mode 100644 index 000000000..35164ff21 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_Analyzers.cs @@ -0,0 +1,746 @@ +// 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.Threading.Tasks; +using CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_Analyzers +{ + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_ValidAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NotPartial_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0001:GeneratedDependencyProperty|}] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NoSetter_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0001:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_NoGetter_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0001:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_InitOnlySetter_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0001:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { get; init; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySyntaxDeclarationAnalyzer_Static_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0001:GeneratedDependencyProperty|}] + public static partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ValidAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnUnannotatedPartialPropertyWithImplementation_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? Name { get; set; } + + public partial string? Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnImplementedProperty_GeneratedByToolkit_DoesNotWarn() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string Name { get; set; } + + [GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnImplementedProperty_GeneratedByAnotherGenerator_Warns() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0002:GeneratedDependencyProperty|}] + public partial string Name { get; set; } + + [GeneratedCode("Some.Other.Generator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_OnImplementedProperty_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0002:GeneratedDependencyProperty|}] + public partial string Name { get; set; } + + public partial string Name + { + get => field; + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsRef_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0003:GeneratedDependencyProperty|}] + public partial ref int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsRefReadOnly_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0003:GeneratedDependencyProperty|}] + public partial ref readonly int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsByRefLike_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0004:GeneratedDependencyProperty|}] + public partial Span {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl + { + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_ValidType1_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_ValidType2_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_InvalidType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + + namespace MyApp; + + public partial class MyControl + { + [{|WCTDP0005:GeneratedDependencyProperty|}] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp10); + } + + [TestMethod] + [DataRow(LanguageVersion.CSharp13)] + [DataRow(LanguageVersion.Preview)] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_ValidLanguageVersion_DoesNotWarn(LanguageVersion languageVersion) + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, languageVersion); + } + + [TestMethod] + [DataRow(LanguageVersion.CSharp10)] + [DataRow(LanguageVersion.CSharp12)] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_RequiresCSharp13_Warns(LanguageVersion languageVersion) + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0006:GeneratedDependencyProperty|}] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, languageVersion); + } + + [TestMethod] + [DataRow(LanguageVersion.CSharp10)] + [DataRow(LanguageVersion.CSharp13)] + public async Task UnsupportedCSharpLanguageVersionAnalyzer_CSharp10_RequiresPreview_Warns(LanguageVersion languageVersion) + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0007:GeneratedDependencyProperty(IsLocalCacheEnabled = true)|}] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, languageVersion); + } + + [TestMethod] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_ValidProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object")] + [DataRow("DependencyPropertyChangedEventArgs")] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_InvalidPropertyType_ValidName_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object")] + [DataRow("DependencyPropertyChangedEventArgs")] + public async Task InvalidPropertyConflictingDeclarationAnalyzer_InvalidPropertyType_NamedProperty_Warns(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0008:GeneratedDependencyProperty|}] + public partial {{propertyType}} {|CS9248:Property|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public class MyControl : Control + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int")] + [DataRow("int?")] + [DataRow("string?")] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NullableOrNotApplicableType_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithMaybeNullAttribute_DoesNotWarn() + { + string source = $$""" + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [MaybeNull] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Required_DoesNotWarn() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public required partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_NullableDisabled_DoesNotWarn() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public required partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValue_DoesNotWarn() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "Bob")] + public required partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Warns() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0009:GeneratedDependencyProperty|}] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValue_Warns() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0009:GeneratedDependencyProperty(DefaultValue = null)|}] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs new file mode 100644 index 000000000..66777d619 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs @@ -0,0 +1,48 @@ +// 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.IO; +using System.Reflection; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +partial class Test_DependencyPropertyGenerator +{ + [TestMethod] + [DataRow("GeneratedDependencyProperty")] + [DataRow("GeneratedDependencyPropertyAttribute")] + public void SingleProperty_String_WithNoCaching_PostInitializationSources(string typeName) + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? Name { get; set; } + } + """; + + string fileName = $"{typeName}.g.cs"; + string sourceText; + + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)) + using (StreamReader reader = new(stream)) + { + sourceText = reader.ReadToEnd(); + } + + string updatedSourceText = sourceText + .Replace("", "CommunityToolkit.WinUI.DependencyPropertyGenerator") + .Replace("", typeof(DependencyPropertyGenerator).Assembly.GetName().Version.ToString()); + + CSharpGeneratorTest.VerifySources(source, (fileName, updatedSourceText), languageVersion: LanguageVersion.CSharp13); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.cs new file mode 100644 index 000000000..76587fa83 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.cs @@ -0,0 +1,1962 @@ +// 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.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public partial class Test_DependencyPropertyGenerator +{ + [TestMethod] + public void SingleProperty_Int32_WithLocalCache() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + set + { + OnNumberSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + int __oldValue = field; + + OnNumberChanging(value); + OnNumberChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + OnNumberChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int newValue); + + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithLocalCache_WithCallback() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + set + { + OnNumberSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + int __oldValue = field; + + OnNumberChanging(value); + OnNumberChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + OnNumberChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int newValue); + + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file static class PropertyChangedCallbacks + { + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + static void Invoke(object d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + OnNumberPropertyChanged(__this, e); + } + + return new(Invoke); + } + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithLocalCache_WithDefaultValue() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true, DefaultValue = 42)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(42)); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + set + { + OnNumberSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + int __oldValue = field; + + OnNumberChanging(value); + OnNumberChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + OnNumberChanged(__oldValue, value); + } = 42; + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int newValue); + + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_UnsetValue() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = GeneratedDependencyProperty.UnsetValue)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(global::Windows.UI.Xaml.DependencyProperty.UnsetValue)); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithCallback() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file static class PropertyChangedCallbacks + { + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + static void Invoke(object d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + OnNumberPropertyChanged(__this, e); + } + + return new(Invoke); + } + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValue() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(42)); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValue_WithCallback() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(42, global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file static class PropertyChangedCallbacks + { + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + static void Invoke(object d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + OnNumberPropertyChanged(__this, e); + } + + return new(Invoke); + } + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithSharedCallback() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file static class PropertyChangedCallbacks + { + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + static void Invoke(object d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + OnPropertyChanged(__this, e); + } + + return new(Invoke); + } + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + private static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_Int32_WithNoCaching_WithBothCallbacks() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file static class PropertyChangedCallbacks + { + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback Number() + { + static void Invoke(object d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + OnNumberPropertyChanged(__this, e); + OnPropertyChanged(__this, e); + } + + return new(Invoke); + } + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] + private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + private static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithLocalCache() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(IsLocalCacheEnabled = true)] + public partial string? Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get => field; + set + { + OnNameSet(ref value); + + if (global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + return; + } + + string? __oldValue = field; + + OnNameChanging(value); + OnNameChanging(__oldValue, value); + + field = value; + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + OnNameChanged(__oldValue, value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging(string? newValue); + + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string? newValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// The previous property value that has been replaced. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching_IsRequired() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public required partial string Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public required partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_CorrectSpacing() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } +} diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj new file mode 100644 index 000000000..25312f926 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj @@ -0,0 +1,10 @@ + + + net8.0-windows10.0.17763.0 + enable + preview + true + false + $(DefineConstants);WINDOWS_UWP + + diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.targets new file mode 100644 index 000000000..3a93502bc --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.targets @@ -0,0 +1,70 @@ + + + + + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + + + + + + + + + false + true + + + + + + + + + + + + + + + + + + @(CommunityToolkitGeneratedDependencyPropertyCurrentCompilerAssemblyIdentity->'%(Version)') + + + true + + + + + + + diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs new file mode 100644 index 000000000..2516abc63 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs @@ -0,0 +1,24 @@ +// 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. + +#if WINDOWS_UWP +using DependencyProperty = Windows.UI.Xaml.DependencyProperty; +#else +using DependencyProperty = Microsoft.UI.Xaml.DependencyProperty; +#endif + +namespace CommunityToolkit.WinUI; + +/// +/// Provides constant values that can be used as default values for . +/// +public sealed class GeneratedDependencyProperty +{ + /// + /// + /// This constant is only meant to be used in assignments to (because + /// cannot be used in that context, as it is not a constant, but rather a static field). Using this constant in other scenarios is undefined behavior. + /// + public const object UnsetValue = null!; +} diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs new file mode 100644 index 000000000..388fd15b3 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +#if WINDOWS_UWP +using DependencyObject = Windows.UI.Xaml.DependencyObject; +using DependencyProperty = Windows.UI.Xaml.DependencyProperty; +using PropertyMetadata = Windows.UI.Xaml.PropertyMetadata; +#else +using DependencyObject = Microsoft.UI.Xaml.DependencyObject; +using DependencyProperty = Microsoft.UI.Xaml.DependencyProperty; +using PropertyMetadata = Microsoft.UI.Xaml.PropertyMetadata; +#endif + +namespace CommunityToolkit.WinUI; + +/// +/// An attribute that indicates that a given partial property should generate a backing . +/// In order to use this attribute, the containing type has to inherit from . +/// +/// This attribute can be used as follows: +/// +/// partial class MyClass : DependencyObject +/// { +/// [GeneratedDependencyProperty] +/// public partial string? Name { get; set; } +/// } +/// +/// +/// +/// +/// +/// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# 13 (or 'preview') must be used. +/// +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public sealed class GeneratedDependencyPropertyAttribute : Attribute +{ + /// + /// Gets a value indicating the default value to set for the property. + /// + /// + /// + /// If not set, the default value will be , for all property types. If there is no callback + /// registered for the generated property, will not be set at all. + /// + /// + /// To set the default value to , use . + /// + /// + public object? DefaultValue { get; init; } = null; + + /// + /// Gets a value indicating whether or not property values should be cached locally, to improve performance. + /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. + /// + /// + /// Local caching is disabled by default. It should be disabled in scenarios where the values of the dependency + /// properties might also be set outside of the partial property implementation, meaning caching would be invalid. + /// + public bool IsLocalCacheEnabled { get; init; } = false; +} From 794d0c313d7979348d9e45968bd8aba6e2606ff7 Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 17:23:32 -0600 Subject: [PATCH 003/200] Cleanup and merge ported csproj, rename files --- .../samples/Dependencies.props | 4 -- ...Toolkit.DependencyPropertyGenerator.csproj | 54 +++++++++++++++++++ ...olkit.DependencyPropertyGenerator.targets} | 0 ...Toolkit.GeneratedDependencyProperty.csproj | 10 ---- ...ontrols.DependencyPropertyGenerator.csproj | 15 ------ 5 files changed, 54 insertions(+), 29 deletions(-) create mode 100644 components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj rename components/DependencyPropertyGenerator/src/{CommunityToolkit.GeneratedDependencyProperty.targets => CommunityToolkit.DependencyPropertyGenerator.targets} (100%) delete mode 100644 components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj delete mode 100644 components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj diff --git a/components/DependencyPropertyGenerator/samples/Dependencies.props b/components/DependencyPropertyGenerator/samples/Dependencies.props index e622e1df4..0b0c230ab 100644 --- a/components/DependencyPropertyGenerator/samples/Dependencies.props +++ b/components/DependencyPropertyGenerator/samples/Dependencies.props @@ -11,21 +11,17 @@ - - - - diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj new file mode 100644 index 000000000..ee8af5f60 --- /dev/null +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj @@ -0,0 +1,54 @@ + + + + + DependencyPropertyGenerator + This package contains DependencyPropertyGenerator. + + + CommunityToolkit.DependencyPropertyGeneratorRns + false + + + + + + + + + + + + + System.Diagnostics.CodeAnalysis.NotNullAttribute; + System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; + System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; + System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; + System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; + System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.targets similarity index 100% rename from components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.targets rename to components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.targets diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj deleted file mode 100644 index 25312f926..000000000 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.GeneratedDependencyProperty.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - net8.0-windows10.0.17763.0 - enable - preview - true - false - $(DefineConstants);WINDOWS_UWP - - diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj deleted file mode 100644 index 84f505cc8..000000000 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.Controls.DependencyPropertyGenerator.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - - DependencyPropertyGenerator - This package contains DependencyPropertyGenerator. - - - CommunityToolkit.WinUI.Controls.DependencyPropertyGeneratorRns - false - - - - - From bd3f0c55a2d92db6782b4619d107e9dd5e131be5 Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 17:30:58 -0600 Subject: [PATCH 004/200] Fix default namespace and package name for DependencyPropertyGenerator component --- ...tyToolkit.WinUI.DependencyPropertyGenerator.csproj} | 10 +++++----- ...yToolkit.WinUI.DependencyPropertyGenerator.targets} | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename components/DependencyPropertyGenerator/src/{CommunityToolkit.DependencyPropertyGenerator.csproj => CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj} (78%) rename components/DependencyPropertyGenerator/src/{CommunityToolkit.DependencyPropertyGenerator.targets => CommunityToolkit.WinUI.DependencyPropertyGenerator.targets} (100%) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj similarity index 78% rename from components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj rename to components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index ee8af5f60..291f538ab 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -6,7 +6,7 @@ This package contains DependencyPropertyGenerator. - CommunityToolkit.DependencyPropertyGeneratorRns + CommunityToolkit.WinUI.DependencyPropertyGeneratorRns false @@ -38,10 +38,10 @@ - - - - + + + + diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets similarity index 100% rename from components/DependencyPropertyGenerator/src/CommunityToolkit.DependencyPropertyGenerator.targets rename to components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets From 28b5c8377ad4d62f68353a4b7666b22fce6225b6 Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 17:49:22 -0600 Subject: [PATCH 005/200] Disable globalusings in DependencyPropertyGenerator --- ...Toolkit.WinUI.DependencyPropertyGenerator.csproj | 13 ++----------- tooling | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index 291f538ab..18a5c1315 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -8,6 +8,7 @@ CommunityToolkit.WinUI.DependencyPropertyGeneratorRns false + false @@ -20,12 +21,7 @@ - System.Diagnostics.CodeAnalysis.NotNullAttribute; - System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; - System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; - System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; - System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; - System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; + System.Runtime.CompilerServices.IsExternalInit; @@ -46,9 +42,4 @@ - - - - - diff --git a/tooling b/tooling index 2ec08a780..66d28745e 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 2ec08a780daaecadd9705a2ab0e4cdde4c6168e2 +Subproject commit 66d28745edaf5eec07a25135f7eb8da54b43deb5 From 8e7694f65f16e2b3e82af8ad3103c6e81b8ffc61 Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 18:04:21 -0600 Subject: [PATCH 006/200] Move files, fix folder name --- .../AnalyzerReleases.Shipped.md | 0 .../AnalyzerReleases.Unshipped.md | 0 ...Toolkit.DependencyPropertyGenerator.SourceGenerators.csproj} | 0 .../Constants/WellKnownTrackingNames.cs | 0 .../Constants/WellKnownTypeNames.cs | 0 .../DependencyPropertyGenerator.Execute.cs | 0 .../DependencyPropertyGenerator.cs | 0 .../Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs | 0 .../InvalidPropertyContainingTypeDeclarationAnalyzer.cs | 0 .../Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs | 2 +- .../Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs | 0 .../Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs | 0 .../Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs | 0 .../Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs | 0 .../Diagnostics/DiagnosticDescriptors.cs | 0 .../Extensions/AccessibilityExtensions.cs | 0 .../Extensions/AttributeDataExtensions.cs | 0 .../Extensions/CompilationExtensions.cs | 0 .../Extensions/ISymbolExtensions.cs | 0 .../Extensions/ITypeSymbolExtensions.cs | 0 .../Extensions/IncrementalValueProviderExtensions.cs | 0 .../Extensions/IndentedTextWriterExtensions.cs | 0 .../Extensions/SyntaxNodeExtensions.cs | 0 .../Helpers/EquatableArray{T}.cs | 0 .../Helpers/HashCode.cs | 0 .../Helpers/ImmutableArrayBuilder{T}.cs | 0 .../Helpers/IndentedTextWriter.cs | 0 .../Helpers/ObjectPool{T}.cs | 0 .../Models/DependencyPropertyInfo.cs | 0 .../Models/HierarchyInfo.cs | 0 .../Models/TypeInfo.cs | 0 .../Models/TypedConstantInfo.Factory.cs | 0 .../Models/TypedConstantInfo.cs | 0 .../CommunityToolkit.DependencyPropertyGenerator.Tests.csproj} | 0 .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 0 .../Helpers/CSharpGeneratorTest{TGenerator}.cs | 0 .../Test_Analyzers.cs | 0 .../Test_DependencyPropertyGenerator.PostInitialization.cs | 0 .../Test_DependencyPropertyGenerator.cs | 0 39 files changed, 1 insertion(+), 1 deletion(-) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/AnalyzerReleases.Shipped.md (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/AnalyzerReleases.Unshipped.md (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj} (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Constants/WellKnownTrackingNames.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Constants/WellKnownTypeNames.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/DependencyPropertyGenerator.Execute.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/DependencyPropertyGenerator.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs (98%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Diagnostics/DiagnosticDescriptors.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/AccessibilityExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/AttributeDataExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/CompilationExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/ISymbolExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/ITypeSymbolExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/IncrementalValueProviderExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/IndentedTextWriterExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Extensions/SyntaxNodeExtensions.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Helpers/EquatableArray{T}.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Helpers/HashCode.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Helpers/ImmutableArrayBuilder{T}.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Helpers/IndentedTextWriter.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Helpers/ObjectPool{T}.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Models/DependencyPropertyInfo.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Models/HierarchyInfo.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Models/TypeInfo.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Models/TypedConstantInfo.Factory.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/Models/TypedConstantInfo.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj => CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj} (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.Tests => CommunityToolkit.DependencyPropertyGenerator.Tests}/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.Tests => CommunityToolkit.DependencyPropertyGenerator.Tests}/Helpers/CSharpGeneratorTest{TGenerator}.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.Tests => CommunityToolkit.DependencyPropertyGenerator.Tests}/Test_Analyzers.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.Tests => CommunityToolkit.DependencyPropertyGenerator.Tests}/Test_DependencyPropertyGenerator.PostInitialization.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.Tests => CommunityToolkit.DependencyPropertyGenerator.Tests}/Test_DependencyPropertyGenerator.cs (100%) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Shipped.md rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Unshipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/AnalyzerReleases.Unshipped.md rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator.csproj rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTrackingNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTrackingNames.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTypeNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Constants/WellKnownTypeNames.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.Execute.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/DependencyPropertyGenerator.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs similarity index 98% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs index 457c6a353..92e801dd1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs @@ -80,7 +80,7 @@ public override void Initialize(AnalysisContext context) { SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); - if (!IsValidPropertyDeclaration(propertyNode)) + // if (!IsValidPropertyDeclaration(propertyNode)) { context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDeclaration, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AccessibilityExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AccessibilityExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AttributeDataExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/AttributeDataExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/CompilationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/CompilationExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ISymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ISymbolExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/ITypeSymbolExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IncrementalValueProviderExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalValueProviderExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IncrementalValueProviderExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalValueProviderExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IndentedTextWriterExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/IndentedTextWriterExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/SyntaxNodeExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxNodeExtensions.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Extensions/SyntaxNodeExtensions.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxNodeExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/EquatableArray{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/EquatableArray{T}.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/EquatableArray{T}.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/EquatableArray{T}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/HashCode.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/HashCode.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ImmutableArrayBuilder{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ImmutableArrayBuilder{T}.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/IndentedTextWriter.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/IndentedTextWriter.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ObjectPool{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Helpers/ObjectPool{T}.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/DependencyPropertyInfo.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/HierarchyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/HierarchyInfo.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypeInfo.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.Factory.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/Models/TypedConstantInfo.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/CommunityToolkit.GeneratedDependencyProperty.Tests.csproj rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_Analyzers.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.Tests/Test_DependencyPropertyGenerator.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs From 54f0445b7889aa8b7d4496ea7391f75385ea2ddf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 16:08:31 -0800 Subject: [PATCH 007/200] Bring back embedded resources --- ...yPropertyGenerator.SourceGenerators.csproj | 6 +- ...t.DependencyPropertyGenerator.Tests.csproj | 8 +- .../GeneratedDependencyProperty.cs | 36 +++++++++ .../GeneratedDependencyPropertyAttribute.cs | 75 +++++++++++++++++++ 4 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyProperty.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index b64446802..21f4f3395 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -23,8 +23,8 @@ - - - + + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index ca2d08512..34b79836f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -30,12 +30,12 @@ diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyProperty.cs new file mode 100644 index 000000000..5364e9c10 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyProperty.cs @@ -0,0 +1,36 @@ +// +#pragma warning disable +#nullable enable + +// 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. + +#if GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_MODE + +namespace CommunityToolkit.WinUI +{ +#if GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + using DependencyProperty = global::Windows.UI.Xaml.DependencyProperty; +#else + using DependencyProperty = global::Microsoft.UI.Xaml.DependencyProperty; +#endif + + /// + /// Provides constant values that can be used as default values for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("", "")] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class GeneratedDependencyProperty + { + /// + /// + /// This constant is only meant to be used in assignments to (because + /// cannot be used in that context, as it is not a constant, but rather a static field). Using this constant in other scenarios is undefined behavior. + /// + public const object UnsetValue = null!; + } +} + +#endif diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs new file mode 100644 index 000000000..752240f7e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +#nullable enable + +// 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. + +#if GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_MODE + +namespace CommunityToolkit.WinUI +{ +#if GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + using DependencyObject = global::Windows.UI.Xaml.DependencyObject; + using DependencyProperty = global::Windows.UI.Xaml.DependencyProperty; + using PropertyMetadata = global::Windows.UI.Xaml.PropertyMetadata; +#else + using DependencyObject = global::Microsoft.UI.Xaml.DependencyObject; + using DependencyProperty = global::Microsoft.UI.Xaml.DependencyProperty; + using PropertyMetadata = global::Microsoft.UI.Xaml.PropertyMetadata; +#endif + + /// + /// An attribute that indicates that a given partial property should generate a backing . + /// In order to use this attribute, the containing type has to inherit from . + /// + /// This attribute can be used as follows: + /// + /// partial class MyClass : DependencyObject + /// { + /// [GeneratedDependencyProperty] + /// public partial string? Name { get; set; } + /// } + /// + /// + /// + /// + /// + /// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# 13 (or 'preview') must be used. + /// + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [global::System.CodeDom.Compiler.GeneratedCode("", "")] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.Diagnostics.Conditional("GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_PRESERVE_ATTRIBUTES")] + internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attribute + { + /// + /// Gets a value indicating the default value to set for the property. + /// + /// + /// + /// If not set, the default value will be , for all property types. If there is no callback + /// registered for the generated property, will not be set at all. + /// + /// + /// To set the default value to , use . + /// + /// + public object? DefaultValue { get; init; } = null; + + /// + /// Gets a value indicating whether or not property values should be cached locally, to improve performance. + /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. + /// + /// + /// Local caching is disabled by default. It should be disabled in scenarios where the values of the dependency + /// properties might also be set outside of the partial property implementation, meaning caching would be invalid. + /// + public bool IsLocalCacheEnabled { get; init; } = false; + } +} + +#endif From 9aec6010c224a9682b8205d4eee9dd05b3311b1c Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 18:27:04 -0600 Subject: [PATCH 008/200] Fix folder name typo --- .../EmbeddedResources/GeneratedDependencyProperty.cs | 0 .../EmbeddedResources/GeneratedDependencyPropertyAttribute.cs | 0 tooling | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/EmbeddedResources/GeneratedDependencyProperty.cs (100%) rename components/DependencyPropertyGenerator/{CommunityToolkit.GeneratedDependencyProperty.SourceGenerator => CommunityToolkit.DependencyPropertyGenerator.SourceGenerators}/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs (100%) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyProperty.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs similarity index 100% rename from components/DependencyPropertyGenerator/CommunityToolkit.GeneratedDependencyProperty.SourceGenerator/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs rename to components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs diff --git a/tooling b/tooling index 66d28745e..c5473d4ab 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 66d28745edaf5eec07a25135f7eb8da54b43deb5 +Subproject commit c5473d4abd1eb162ba9d6512439438a61e0b6252 From f472ab0117926e1a99d8c2c4884ac393adb6ef0d Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 2 Dec 2024 20:04:17 -0600 Subject: [PATCH 009/200] Fix project references and update embedded resource paths in DependencyPropertyGenerator tests --- ...ommunityToolkit.DependencyPropertyGenerator.Tests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index 34b79836f..e36f52807 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -23,18 +23,18 @@ - + From a559c58e75e5ea27caae8470687496073c21185d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 18:11:32 -0800 Subject: [PATCH 010/200] Fix wrong paths in test project --- ...munityToolkit.DependencyPropertyGenerator.Tests.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index e36f52807..d30603364 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -23,18 +23,18 @@ - - + + From bf62d4d84a1d100a367f30c9af082b650c71de3e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 18:14:39 -0800 Subject: [PATCH 011/200] Fix more wrong paths --- ...ommunityToolkit.DependencyPropertyGenerator.Tests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index d30603364..7828f4b89 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -23,18 +23,18 @@ - + From eb854c9bc7d45da5feaf3fa30c8e9d54ad6ed8ae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 20:49:16 -0800 Subject: [PATCH 012/200] Fix some warnings in the test project --- .../Test_DependencyPropertyGenerator.PostInitialization.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs index 66777d619..1baa9750d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.PostInitialization.cs @@ -33,7 +33,7 @@ public partial class MyControl : DependencyObject string fileName = $"{typeName}.g.cs"; string sourceText; - using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)) + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(fileName)!) using (StreamReader reader = new(stream)) { sourceText = reader.ReadToEnd(); @@ -41,7 +41,7 @@ public partial class MyControl : DependencyObject string updatedSourceText = sourceText .Replace("", "CommunityToolkit.WinUI.DependencyPropertyGenerator") - .Replace("", typeof(DependencyPropertyGenerator).Assembly.GetName().Version.ToString()); + .Replace("", typeof(DependencyPropertyGenerator).Assembly.GetName().Version!.ToString()); CSharpGeneratorTest.VerifySources(source, (fileName, updatedSourceText), languageVersion: LanguageVersion.CSharp13); } From df9307921cfa565d02e386858577e6ce15f27469 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 20:53:50 -0800 Subject: [PATCH 013/200] Create .gitattributes --- components/DependencyPropertyGenerator/.gitattributes | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 components/DependencyPropertyGenerator/.gitattributes diff --git a/components/DependencyPropertyGenerator/.gitattributes b/components/DependencyPropertyGenerator/.gitattributes new file mode 100644 index 000000000..64d6ecc10 --- /dev/null +++ b/components/DependencyPropertyGenerator/.gitattributes @@ -0,0 +1,10 @@ +# All file types: +# - Treat as text +# - Normalize to LF line endings +* text=auto eol=lf + +# Explicit settings for well known types +*.cs text eol=lf +*.csproj text eol=lf +*.projitems text eol=lf +*.shprroj text eol=lf \ No newline at end of file From 03f041ee7e12a42e664cb3a9c83c807ced808b77 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 20:56:42 -0800 Subject: [PATCH 014/200] Fix newlines to LF --- .../AnalyzerReleases.Unshipped.md | 2 +- .../Extensions/AccessibilityExtensions.cs | 2 +- .../Extensions/IndentedTextWriterExtensions.cs | 2 +- .../Helpers/HashCode.cs | 2 +- .../Helpers/IndentedTextWriter.cs | 2 +- .../Helpers/ObjectPool{T}.cs | 2 +- .../Models/HierarchyInfo.cs | 2 +- .../Models/TypeInfo.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md index 6a6dd08d1..17d4678ce 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -1,2 +1,2 @@ -; Unshipped analyzer release +; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs index 413ecc2c7..f7154e446 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs @@ -29,4 +29,4 @@ public static string GetExpression(this Accessibility accessibility) _ => "" }; } -} \ No newline at end of file +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs index 688db2077..01a9a5ec2 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IndentedTextWriterExtensions.cs @@ -101,4 +101,4 @@ public static void WriteInitializationExpressions( } } } -} \ No newline at end of file +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs index 8d9135cc7..9e3728b56 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/HashCode.cs @@ -500,4 +500,4 @@ private static uint RotateLeft(uint value, int offset) { return (value << offset) | (value >> (32 - offset)); } -} \ No newline at end of file +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs index 9bfc74b65..6d88ecb97 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs @@ -512,4 +512,4 @@ public void AppendFormatted(T value, string? format) this.handler.AppendFormatted(value, format); } } -} \ No newline at end of file +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs index 723d0a73f..44b103abc 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/ObjectPool{T}.cs @@ -151,4 +151,4 @@ private struct Element /// internal T? Value; } -} \ No newline at end of file +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs index 67393f1e9..5bb928710 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/HierarchyInfo.cs @@ -137,4 +137,4 @@ public string GetFullyQualifiedTypeName() return fullyQualifiedTypeName.ToString(); } -} \ No newline at end of file +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs index a1b5e7e02..daa0b27cd 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypeInfo.cs @@ -29,4 +29,4 @@ public string GetTypeKeyword() _ => "class" }; } -} \ No newline at end of file +} From d503756f5f90b17298706b01be0e795324002dc6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 20:58:29 -0800 Subject: [PATCH 015/200] Use 'WellKnownTypeNames' in all analyzers --- .../Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs | 2 +- .../InvalidPropertyContainingTypeDeclarationAnalyzer.cs | 2 +- .../Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs | 3 ++- .../Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs | 3 ++- .../Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs | 3 ++- .../Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs | 3 ++- .../Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs | 3 ++- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs index 1206c2fef..0c204d0c5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs @@ -29,7 +29,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); // Get the 'DependencyPropertyChangedEventArgs' symbol if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs) is not { } dependencyPropertyChangedEventArgsSymbol) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs index b99a9f5f0..af90b2470 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs @@ -29,7 +29,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); // Get the 'DependencyObject' symbol if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject) is not { } dependencyObjectSymbol) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs index 92e801dd1..062d84b60 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -36,7 +37,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); context.RegisterSymbolAction(context => { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs index a7d99b6ff..649ed5373 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -28,7 +29,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); // Attempt to also get the '[MaybeNull]' symbols (there might be multiples, due to polyfills) ImmutableArray maybeNullAttributeSymbol = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs index c82c02df0..60a26e2b6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -35,7 +36,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); // Get the '[GeneratedCode]' symbol if (context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs index 8b5d6b157..29df7b907 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -30,7 +31,7 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); context.RegisterSymbolAction(context => { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs index 200769544..c05b15809 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -39,7 +40,7 @@ public override void Initialize(AnalysisContext context) } // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName("CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"); + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); context.RegisterSymbolAction(context => { From 8846a706441920a44a035c219d94ec41cc504ac0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 23:15:09 -0800 Subject: [PATCH 016/200] Remove global usings --- ...olkit.DependencyPropertyGenerator.SourceGenerators.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index 21f4f3395..17f887803 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -27,4 +27,9 @@ + + + + + From 51d44bd95bee9e5c180b5990f22f474f23666c12 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 23:15:43 -0800 Subject: [PATCH 017/200] Finish 'InvalidPropertyDefaultValueTypeAnalyzer' --- .../Constants/WellKnownTypeNames.cs | 5 + .../DependencyPropertyGenerator.Execute.cs | 25 +++- ...InvalidPropertyDefaultValueTypeAnalyzer.cs | 128 +++++++++++++++++ ...nvalidPropertyDefaultValueTypeAttribute.cs | 129 ------------------ 4 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs delete mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs index cc2808537..60194c1db 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs @@ -14,6 +14,11 @@ internal static class WellKnownTypeNames /// public const string GeneratedDependencyPropertyAttribute = "CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"; + /// + /// The fully qualified type name for the GeneratedDependencyProperty type. + /// + public const string GeneratedDependencyProperty = "CommunityToolkit.WinUI.GeneratedDependencyProperty"; + /// /// The fully qualified type name for the XAML namespace. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index aa5164dba..f14961453 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; namespace CommunityToolkit.GeneratedDependencyProperty; @@ -227,10 +228,28 @@ public static TypedConstantInfo GetDefaultValue( return TypedConstantInfo.Create(defaultValue); } - // Handle 'UnsetValue' as well - if (InvalidPropertyDefaultValueTypeAttribute.IsDependencyPropertyUnsetValue(attributeData, semanticModel, token)) + // If we do have a default value, we also want to check whether it's the special 'UnsetValue' placeholder. + // To do so, we get the application syntax, find the argument, then get the operation and inspect it. + if (attributeData.ApplicationSyntaxReference?.GetSyntax(token) is AttributeSyntax attributeSyntax) { - return UnsetValueInfo; + foreach (AttributeArgumentSyntax attributeArgumentSyntax in attributeSyntax.ArgumentList?.Arguments ?? []) + { + // Let's see whether the current argument is the one that set the 'DefaultValue' property + if (attributeArgumentSyntax.NameEquals?.Name.Identifier.Text is "DefaultValue") + { + IOperation? operation = semanticModel.GetOperation(attributeArgumentSyntax.Expression, token); + + // Double check that it's a constant field reference (it could also be a literal of some kind, etc.) + if (operation is IFieldReferenceOperation { Field: { Name: "UnsetValue" } fieldSymbol }) + { + // Last step: we want to validate that the reference is actually to the special placeholder + if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName(WellKnownTypeNames.GeneratedDependencyProperty)) + { + return UnsetValueInfo; + } + } + } + } } // Otherwise, the value has been explicitly set to 'null', so let's respect that diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs new file mode 100644 index 000000000..485819482 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -0,0 +1,128 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used with an invalid default value type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyDefaultValueTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDefaultValueNull, + InvalidPropertyDefaultValueType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterOperationAction(context => + { + // We only care about attributes on properties + if (context.ContainingSymbol is not IPropertySymbol propertySymbol) + { + return; + } + + // Make sure the attribute operation is valid, and that we can get the attribute type symbol + if (context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation { Type: INamedTypeSymbol attributeTypeSymbol } objectOperation }) + { + return; + } + + // Filter out all attributes of other types + if (!generatedDependencyPropertyAttributeSymbols.Contains(attributeTypeSymbol, SymbolEqualityComparer.Default)) + { + return; + } + + // Also get the actual attribute data for '[GeneratedDependencyProperty]' (this should always succeed at this point) + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Get the default value, if present (if it's not set, nothing to do) + if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + return; + } + + bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + bool isNullableType = !propertySymbol.Type.IsValueType || isNullableValueType; + + // If the value is 'null', handle all possible cases: + // - Special placeholder for 'UnsetValue' + // - Explicit 'null' value + if (defaultValue.IsNull) + { + // Go through all named arguments of the attribute to look for 'UnsetValue' + foreach (IOperation argumentOperation in objectOperation.Initializer?.Initializers ?? []) + { + // We found its assignment: check if it's the 'UnsetValue' placeholder + if (argumentOperation is ISimpleAssignmentOperation { Value: IFieldReferenceOperation { Field: { Name: "UnsetValue" } fieldSymbol } }) + { + // Validate that the reference is actually to the special placeholder + if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName(WellKnownTypeNames.GeneratedDependencyProperty)) + { + return; + } + + // If it's not a match, we can just stop iterating: we know for sure the value is something else explicitly set + break; + } + } + + // Warn if the value is not nullable + if (!isNullableType) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDefaultValueNull, + attributeData.GetLocation(), + propertySymbol, + propertySymbol.Type)); + } + } + else + { + // Get the target type with a special case for 'Nullable' + ITypeSymbol propertyTypeSymbol = isNullableValueType + ? ((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0] + : propertySymbol.Type; + + // Warn if the type of the default value is not compatible + if (!SymbolEqualityComparer.Default.Equals(propertyTypeSymbol, defaultValue.Type)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDefaultValueType, + attributeData.GetLocation(), + propertySymbol, + propertySymbol.Type, + defaultValue.Value, + defaultValue.Type)); + } + } + }, OperationKind.Attribute); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs deleted file mode 100644 index 062d84b60..000000000 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAttribute.cs +++ /dev/null @@ -1,129 +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.Immutable; -using System.Threading; -using CommunityToolkit.GeneratedDependencyProperty.Constants; -using CommunityToolkit.GeneratedDependencyProperty.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Operations; -using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; - -namespace CommunityToolkit.GeneratedDependencyProperty; - -/// -/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used with an invalid default value type. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class InvalidPropertyDefaultValueTypeAttribute : DiagnosticAnalyzer -{ - /// - public override ImmutableArray SupportedDiagnostics { get; } = - [ - InvalidPropertyDefaultValueNull, - InvalidPropertyDefaultValueType - ]; - - /// - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(static context => - { - // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); - - context.RegisterSymbolAction(context => - { - // We're intentionally only looking for properties here - if (context.Symbol is not IPropertySymbol propertySymbol) - { - return; - } - - // If the property isn't using '[GeneratedDependencyProperty]', there's nothing to do - if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) - { - return; - } - - // Get the default value, if present (if it's not set, nothing to do) - if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) - { - return; - } - - // Skip 'UnsetValue', that's special - - bool isNullableValueType = propertySymbol.Type is { IsValueType: true } and not INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; - bool isNullableType = !propertySymbol.Type.IsValueType || isNullableValueType; - - // Check for invalid 'null' default values - if (defaultValue.IsNull && !isNullableType) - { - context.ReportDiagnostic(Diagnostic.Create( - InvalidPropertyDefaultValueNull, - attributeData.GetLocation(), - propertySymbol, - propertySymbol.Type)); - - return; - } - - - foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) - { - SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); - - // if (!IsValidPropertyDeclaration(propertyNode)) - { - context.ReportDiagnostic(Diagnostic.Create( - InvalidPropertyDeclaration, - attributeData.GetLocation(), - propertySymbol)); - - return; - } - } - }, SymbolKind.Property); - }); - } - - internal static bool IsDependencyPropertyUnsetValue( - AttributeData attributeData, - SemanticModel semanticModel, - CancellationToken token) - { - // If we do have a default value, we also want to check whether it's the special 'UnsetValue' placeholder. - // To do so, we get the application syntax, find the argument, then get the operation and inspect it. - if (attributeData.ApplicationSyntaxReference?.GetSyntax(token) is AttributeSyntax attributeSyntax) - { - foreach (AttributeArgumentSyntax attributeArgumentSyntax in attributeSyntax.ArgumentList?.Arguments ?? []) - { - // Let's see whether the current argument is the one that set the 'DefaultValue' property - if (attributeArgumentSyntax.NameEquals?.Name.Identifier.Text is "DefaultValue") - { - IOperation? operation = semanticModel.GetOperation(attributeArgumentSyntax.Expression, token); - - // Double check that it's a constant field reference (it could also be a literal of some kind, etc.) - if (operation is IFieldReferenceOperation { Field: { Name: "UnsetValue" } fieldSymbol }) - { - // Last step: we want to validate that the reference is actually to the special placeholder - if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName("CommunityToolkit.WinUI.GeneratedDependencyProperty")) - { - return true; - } - } - } - } - } - - return false; - } -} From 6b2d0cba6bf23b2d4218bd3150c5be82bcb7a250 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 23:15:49 -0800 Subject: [PATCH 018/200] Add unit tests --- .../Test_Analyzers.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 35164ff21..deaa19b7b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -743,4 +743,161 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoDefaultValue_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string?")] + [DataRow("int")] + [DataRow("int?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_UnsetValue_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = GeneratedDependencyProperty.UnsetValue)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string")] + [DataRow("int?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NullValue_Nullable_DoesNotWarn(string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "\"test\"")] + [DataRow("int", "42")] + [DataRow("double", "3.14")] + [DataRow("int?", "42")] + [DataRow("double?", "3.14")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_CompatibleType_DoesNotWarn(string propertyType, string defaultValueType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValueType}})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_NullValue_NonNullable_Warns() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0010:GeneratedDependencyProperty(DefaultValue = null)|}] + public partial int {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "42")] + [DataRow("string", "3.14")] + [DataRow("int", "\"test\"")] + [DataRow("int?", "\"test\"")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_IncompatibleType_Warns(string propertyType, string defaultValueType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0011:GeneratedDependencyProperty(DefaultValue = {{defaultValueType}})|}] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From 6c1ceb736f058817c9d9f22f6abe2e03503c9b44 Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 11:52:24 -0600 Subject: [PATCH 019/200] Consolidate GlobalUsings imports around tooling --- Directory.Build.targets | 1 - Windows.Toolkit.Common.props | 4 ---- .../AppServices.SourceGenerators.Tests.csproj | 4 ---- .../CommunityToolkit.AppServices.SourceGenerators.csproj | 5 ----- .../AppServices/src/CommunityToolkit.AppServices.csproj | 6 +----- ...lkit.DependencyPropertyGenerator.SourceGenerators.csproj | 5 ----- .../samples/DependencyPropertyGeneratorCustomSample.xaml.cs | 4 ++-- ...t.Extensions.DependencyInjection.SourceGenerators.csproj | 5 ----- .../CommunityToolkit.Extensions.DependencyInjection.csproj | 5 ----- tooling | 2 +- 10 files changed, 4 insertions(+), 37 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 22dfa3191..10a703183 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -22,7 +22,6 @@ - \ No newline at end of file diff --git a/Windows.Toolkit.Common.props b/Windows.Toolkit.Common.props index 83f9ae093..773d2effe 100644 --- a/Windows.Toolkit.Common.props +++ b/Windows.Toolkit.Common.props @@ -29,8 +29,4 @@ true true - - - - diff --git a/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj b/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj index 46b9111b5..3fceba82d 100644 --- a/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj +++ b/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj @@ -16,8 +16,4 @@ - - - - diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj index 64d80c88a..78f3c3ccc 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj @@ -22,9 +22,4 @@ - - - - - diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index dcab9109a..42a5c0199 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -7,6 +7,7 @@ CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) false + false @@ -57,9 +58,4 @@ - - - - - diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index 17f887803..21f4f3395 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -27,9 +27,4 @@ - - - - - diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs index 1b07afdd3..78ac1454e 100644 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs @@ -2,7 +2,7 @@ // 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.WinUI.Controls; +using CommunityToolkit.WinUI; namespace DependencyPropertyGeneratorExperiment.Samples; @@ -12,7 +12,7 @@ namespace DependencyPropertyGeneratorExperiment.Samples; [ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] [ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] -[ToolkitSample(id: nameof(DependencyPropertyGeneratorCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(DependencyPropertyGenerator)} custom control.")] +[ToolkitSample(id: nameof(DependencyPropertyGeneratorCustomSample), "Custom control", description: $"A sample for showing how to create and use the DependencyPropertyGenerator.")] public sealed partial class DependencyPropertyGeneratorCustomSample : Page { public DependencyPropertyGeneratorCustomSample() diff --git a/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj b/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj index e80e6cf87..23e3243a3 100644 --- a/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj +++ b/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj @@ -22,9 +22,4 @@ - - - - - \ No newline at end of file diff --git a/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj b/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj index c339fb59f..4aa44f5af 100644 --- a/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj +++ b/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj @@ -30,9 +30,4 @@ - - - - - diff --git a/tooling b/tooling index c5473d4ab..05fbc3eac 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit c5473d4abd1eb162ba9d6512439438a61e0b6252 +Subproject commit 05fbc3eacff1ae0f1ef4865ff841bcefe9f8899f From d9bec9403f8cea21b9f87a3d67416e7d1968e172 Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 12:15:37 -0600 Subject: [PATCH 020/200] Fixed namespace errors when running under Uno --- .../src/GeneratedDependencyProperty.cs | 2 +- .../src/GeneratedDependencyPropertyAttribute.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs index 2516abc63..e5dbcc3a5 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if WINDOWS_UWP +#if WINDOWS_UWP || HAS_UNO using DependencyProperty = Windows.UI.Xaml.DependencyProperty; #else using DependencyProperty = Microsoft.UI.Xaml.DependencyProperty; diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs index 388fd15b3..d7b016cfe 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -#if WINDOWS_UWP +#if WINDOWS_UWP || HAS_UNO using DependencyObject = Windows.UI.Xaml.DependencyObject; using DependencyProperty = Windows.UI.Xaml.DependencyProperty; using PropertyMetadata = Windows.UI.Xaml.PropertyMetadata; From 291f7fa93f58d69c0f19379894c42dba4259ec48 Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 12:15:47 -0600 Subject: [PATCH 021/200] Update tooling submodule --- tooling | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling b/tooling index 05fbc3eac..7e6206391 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 05fbc3eacff1ae0f1ef4865ff841bcefe9f8899f +Subproject commit 7e620639169a85e9430504563f00426ab39c85d2 From 9637c3a2bdb68f6621fc9e0ab00ecb4f6e2cce88 Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 14:08:19 -0600 Subject: [PATCH 022/200] Remove LangVersion setting from DependencyPropertyGenerator project --- ...tyToolkit.DependencyPropertyGenerator.SourceGenerators.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index 21f4f3395..32c4c1233 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -1,7 +1,6 @@ netstandard2.0 - 13.0 enable true true From 9f0809d50b274c8cd0a2059f2420bc11f472d1fc Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 14:14:23 -0600 Subject: [PATCH 023/200] Ran XAML styler --- .../samples/DependencyPropertyGeneratorCustomSample.xaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml index 0d45a88ec..75d2cb3a7 100644 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml @@ -3,6 +3,4 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:CommunityToolkit.WinUI.Controls" - xmlns:local="using:DependencyPropertyGeneratorExperiment.Samples"> - - + xmlns:local="using:DependencyPropertyGeneratorExperiment.Samples" /> From 8436bde6d046b9fa580857f6bb02bf7d3ab17c0c Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 14:17:13 -0600 Subject: [PATCH 024/200] Remove empty samples --- .../samples/DependencyPropertyGenerator.md | 6 ---- ...pendencyPropertyGeneratorCustomSample.xaml | 6 ---- ...dencyPropertyGeneratorCustomSample.xaml.cs | 30 ------------------- ...ependencyInjection.SourceGenerators.csproj | 25 ---------------- 4 files changed, 67 deletions(-) delete mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml delete mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs delete mode 100644 components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md index 35e7939c9..351101d5c 100644 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md +++ b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md @@ -24,9 +24,3 @@ icon: assets/icon.png TODO: Fill in information about this experiment and how to get started here... -## Custom Control - -You can inherit from an existing component as well, like `Panel`, this example shows a control without a -XAML Style that will be more light-weight to consume by an app developer: - -> [!Sample DependencyPropertyGeneratorCustomSample] diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml deleted file mode 100644 index 75d2cb3a7..000000000 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs b/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs deleted file mode 100644 index 78ac1454e..000000000 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGeneratorCustomSample.xaml.cs +++ /dev/null @@ -1,30 +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 CommunityToolkit.WinUI; - -namespace DependencyPropertyGeneratorExperiment.Samples; - -/// -/// An example sample page of a custom control inheriting from Panel. -/// -[ToolkitSampleTextOption("TitleText", "This is a title", Title = "Input the text")] -[ToolkitSampleMultiChoiceOption("LayoutOrientation", "Horizontal", "Vertical", Title = "Orientation")] - -[ToolkitSample(id: nameof(DependencyPropertyGeneratorCustomSample), "Custom control", description: $"A sample for showing how to create and use the DependencyPropertyGenerator.")] -public sealed partial class DependencyPropertyGeneratorCustomSample : Page -{ - public DependencyPropertyGeneratorCustomSample() - { - this.InitializeComponent(); - } - - // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 - public static Orientation ConvertStringToOrientation(string orientation) => orientation switch - { - "Vertical" => Orientation.Vertical, - "Horizontal" => Orientation.Horizontal, - _ => throw new System.NotImplementedException(), - }; -} diff --git a/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj b/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj deleted file mode 100644 index 23e3243a3..000000000 --- a/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - netstandard2.0 - false - true - - - $(NoWarn);CS8500 - - - - - - - - - - - - \ No newline at end of file From 1babcec58e3ab263970986e7ade5a180ba2906cb Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 14:27:58 -0600 Subject: [PATCH 025/200] Remove empty component doc to fix CI error --- .../samples/DependencyPropertyGenerator.md | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md diff --git a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md b/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md deleted file mode 100644 index 351101d5c..000000000 --- a/components/DependencyPropertyGenerator/samples/DependencyPropertyGenerator.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: DependencyPropertyGenerator -author: githubaccount -description: TODO: Your experiment's description here -keywords: DependencyPropertyGenerator, Control, Layout -dev_langs: - - csharp -category: Controls -subcategory: Layout -discussion-id: 0 -issue-id: 0 -icon: assets/icon.png ---- - - - - - - - - - -# DependencyPropertyGenerator - -TODO: Fill in information about this experiment and how to get started here... - From c2351acfcc5c4775b5817b4a0a5ac31d906793f8 Mon Sep 17 00:00:00 2001 From: Arlo Date: Tue, 3 Dec 2024 14:47:29 -0600 Subject: [PATCH 026/200] Remove unused test files from DependencyPropertyGenerator project --- .../DependencyPropertyGenerator.Tests.projitems | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems index 3156f4a9f..f30608823 100644 --- a/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems +++ b/components/DependencyPropertyGenerator/tests/DependencyPropertyGenerator.Tests.projitems @@ -8,16 +8,4 @@ DependencyPropertyGeneratorExperiment.Tests - - - - ExampleDependencyPropertyGeneratorTestPage.xaml - - - - - Designer - MSBuild:Compile - - \ No newline at end of file From 471c16116b7a587ad2c420a0fb3a1340cad88302 Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 18:04:36 -0600 Subject: [PATCH 027/200] Update .NET version to 9.0 in Dockerfile, devcontainer.json, build.yml, and global.json --- .devcontainer/Dockerfile | 7 ++----- .devcontainer/devcontainer.json | 2 +- .github/workflows/build.yml | 4 +--- global.json | 2 +- tooling | 2 +- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f09593147..6935449fd 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,5 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.208.0/containers/dotnet/.devcontainer/base.Dockerfile - -# [Choice] .NET version: 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal, etc -ARG VARIANT="8.0-bullseye-slim" -FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} +# See https://github.com/devcontainers/images/tree/main/src/dotnet for image choices +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:9.0 # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d21671858..f616c78fc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "args": { // Update 'VARIANT' to pick a .NET Core version: 3.1, 5.0, 6.0 // Append -bullseye or -focal to pin to an OS version. - "VARIANT": "6.0", + "VARIANT": "9.0", // Options "NODE_VERSION": "lts/*" } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc9dec043..1cc92604e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,9 +17,7 @@ on: merge_group: env: - DOTNET_VERSION: ${{ '8.0.201' }} - DOTNET_INSTALL_DIR: dotnet-install - DOTNET_ROOT: dotnet-install + DOTNET_VERSION: ${{ '9.0.x' }} ENABLE_DIAGNOSTICS: false #COREHOST_TRACE: 1 MSBUILD_VERBOSITY: normal diff --git a/global.json b/global.json index 91187a7c4..27a187755 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.403", + "version": "9.0.101", "rollForward": "latestFeature" }, "msbuild-sdks": diff --git a/tooling b/tooling index 7e6206391..43e41c221 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 7e620639169a85e9430504563f00426ab39c85d2 +Subproject commit 43e41c2215b583ffcd54eb33954c79d069249b55 From 96578276d2af9734d052e4dbeb596ace8323d206 Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 18:22:04 -0600 Subject: [PATCH 028/200] Fixed compilation conditionals --- .../src/GeneratedDependencyProperty.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs index e5dbcc3a5..0db11876a 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyProperty.cs @@ -2,10 +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. -#if WINDOWS_UWP || HAS_UNO -using DependencyProperty = Windows.UI.Xaml.DependencyProperty; -#else +#if WINAPPSDK using DependencyProperty = Microsoft.UI.Xaml.DependencyProperty; +#else +using DependencyProperty = Windows.UI.Xaml.DependencyProperty; #endif namespace CommunityToolkit.WinUI; From 10499acfaac028a128a34877b32478080e7af8e6 Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 18:56:24 -0600 Subject: [PATCH 029/200] Temporarily limit WinUI and multitarget options in build matrix for Uno/Wasm compatibility --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1cc92604e..d3f758220 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,8 +60,8 @@ jobs: strategy: fail-fast: false # prevent one matrix pipeline from being cancelled if one fails, we want them all to run to completion. matrix: - winui: [2, 3] - multitarget: ['uwp', 'wasdk', 'wasm', 'wpf', 'linuxgtk', 'macos', 'ios', 'android'] + winui: [2] # Temporary until we can get Uno/Wasm working + multitarget: ['uwp'] # Temporary until we can get Uno/Wasm working exclude: # WinUI 2 not supported on wasdk - winui: 2 From da2b2c325243f0b84d5d6d8663d1e7cb2f102946 Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 19:05:26 -0600 Subject: [PATCH 030/200] Temporarily disable wasm-linux check, only build DependencyPropertyGenerator component --- .github/workflows/build.yml | 56 +++---------------------------------- 1 file changed, 4 insertions(+), 52 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3f758220..3054ae6ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -135,7 +135,7 @@ jobs: # Generate full solution with all projects (sample gallery heads, components, tests) - name: Generate solution with ${{ matrix.multitarget }} gallery, components and tests working-directory: ./ - run: powershell -version 5.1 -command "./tooling/GenerateAllSolution.ps1 -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop + run: powershell -version 5.1 -command "./tooling/GenerateAllSolution.ps1 -Components DependencyPropertyGenerator -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop # Build solution - name: MSBuild (With diagnostics) @@ -213,7 +213,7 @@ jobs: strategy: fail-fast: false # prevent one matrix pipeline from being cancelled if one fails, we want them all to run to completion. matrix: - winui: [2, 3] + winui: [2] env: VERSION_PROPERTY: ${{ github.ref == 'refs/heads/main' && format('build.{0}', github.run_number) || format('pull-{0}.{1}', github.event.number, github.run_number) }} @@ -269,7 +269,7 @@ jobs: # Build and pack component nupkg - name: Build and pack component packages - run: ./tooling/Build-Toolkit-Components.ps1 -MultiTargets all -ExcludeMultiTargets ${{ env.EXCLUDED_MULTITARGETS }} -WinUIMajorVersion ${{ matrix.winui }} -DateForVersion ${{ env.VERSION_DATE }} ${{ env.VERSION_PROPERTY != '' && format('-PreviewVersion "{0}"', env.VERSION_PROPERTY) || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-EnableBinlogs' || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-Verbose' || '' }} -BinlogOutput ./ -NupkgOutput ./ -Release + run: ./tooling/Build-Toolkit-Components.ps1 -Components DependencyPropertyGenerator -MultiTargets uwp -ExcludeMultiTargets ${{ env.EXCLUDED_MULTITARGETS }} -WinUIMajorVersion ${{ matrix.winui }} -DateForVersion ${{ env.VERSION_DATE }} ${{ env.VERSION_PROPERTY != '' && format('-PreviewVersion "{0}"', env.VERSION_PROPERTY) || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-EnableBinlogs' || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-Verbose' || '' }} -BinlogOutput ./ -NupkgOutput ./ -Release - name: Validate package names if: ${{ env.VERSION_PROPERTY != '' }} @@ -306,52 +306,4 @@ jobs: if: ${{ (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }} with: name: build-logs-winui${{ matrix.winui }} - path: ./*.*log - - wasm-linux: - runs-on: ubuntu-latest - env: - HEADS_DIRECTORY: tooling/ProjectHeads - - steps: - - name: Install .NET SDK v${{ env.DOTNET_VERSION }} - uses: actions/setup-dotnet@v4 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: .NET Info (if diagnostics) - if: ${{ env.ENABLE_DIAGNOSTICS == 'true' }} - run: dotnet --info - - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - name: Checkout Repository - uses: actions/checkout@v4 - with: - submodules: recursive - - # Restore Tools from Manifest list in the Repository - - name: Restore dotnet tools - run: dotnet tool restore - - - name: Generate solution - shell: pwsh - working-directory: ./ - run: ./tooling/GenerateAllSolution.ps1${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} - - - name: Install ninja for WASM native dependencies - run: sudo apt-get install ninja-build - - # Issue with Comment Links currently, see: https://github.com/mrlacey/CommentLinks/issues/38 - # See launch.json configuration file for analogous command we're emulating here to build LINK: ../../.vscode/launch.json:CommunityToolkit.App.Wasm.csproj - - name: dotnet build - working-directory: ./${{ env.HEADS_DIRECTORY }}/AllComponents/Wasm/ - run: dotnet build /r /bl /p:UnoSourceGeneratorUseGenerationHost=true /p:UnoSourceGeneratorUseGenerationController=false - - # TODO: Do we want to run tests here? Can we do that on linux easily? - - - name: Artifact - Diagnostic Logs - uses: actions/upload-artifact@v4 - if: ${{ (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }} - with: - name: linux-logs - path: ./**/*.*log + path: ./*.*log \ No newline at end of file From cf30a6452eeeb753c69a6f953c7712ee0ddb998c Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 19:24:50 -0600 Subject: [PATCH 031/200] Adjust CI to only build DependencyPropertyGenerator --- .github/workflows/build.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3054ae6ca..e60b97202 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -134,23 +134,24 @@ jobs: # Generate full solution with all projects (sample gallery heads, components, tests) - name: Generate solution with ${{ matrix.multitarget }} gallery, components and tests - working-directory: ./ - run: powershell -version 5.1 -command "./tooling/GenerateAllSolution.ps1 -Components DependencyPropertyGenerator -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop + working-directory: ./components/DependencyPropertyGenerator + run: powershell -version 5.1 -command "./GenerateSingleSampleHeads.ps1 -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop # Build solution - name: MSBuild (With diagnostics) if: ${{ env.ENABLE_DIAGNOSTICS == 'true' }} + working-directory: ./components/DependencyPropertyGenerator run: > msbuild.exe /restore /nowarn:MSB4011 /p:Configuration=Release /m ${{ env.ENABLE_DIAGNOSTICS == 'true' && '/bl' || '' }} /v:${{ env.MSBUILD_VERBOSITY }} - CommunityToolkit.AllComponents.sln - name: MSBuild if: ${{ env.ENABLE_DIAGNOSTICS == 'false' }} - run: msbuild.exe CommunityToolkit.AllComponents.sln /restore /nowarn:MSB4011 -p:Configuration=Release + working-directory: ./components/DependencyPropertyGenerator + run: msbuild.exe /restore /nowarn:MSB4011 -p:Configuration=Release # Run tests - name: Setup VSTest Path From ec018efc5411fae39495bb765a18a27c37903ddf Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 19:38:41 -0600 Subject: [PATCH 032/200] Enable WinUI 3, fix GenerateSingleSampleHeads script invocation --- .github/workflows/build.yml | 10 +++++----- tooling | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e60b97202..0c59212de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,8 +60,8 @@ jobs: strategy: fail-fast: false # prevent one matrix pipeline from being cancelled if one fails, we want them all to run to completion. matrix: - winui: [2] # Temporary until we can get Uno/Wasm working - multitarget: ['uwp'] # Temporary until we can get Uno/Wasm working + winui: [2, 3] # Temporary until we can get Uno/Wasm working + multitarget: ['uwp', 'wasdk'] # Temporary until we can get Uno/Wasm working exclude: # WinUI 2 not supported on wasdk - winui: 2 @@ -135,7 +135,7 @@ jobs: # Generate full solution with all projects (sample gallery heads, components, tests) - name: Generate solution with ${{ matrix.multitarget }} gallery, components and tests working-directory: ./components/DependencyPropertyGenerator - run: powershell -version 5.1 -command "./GenerateSingleSampleHeads.ps1 -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop + run: powershell -version 5.1 -command "../../tooling/ProjectHeads/GenerateSingleSampleHeads.ps1 -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop # Build solution - name: MSBuild (With diagnostics) @@ -214,7 +214,7 @@ jobs: strategy: fail-fast: false # prevent one matrix pipeline from being cancelled if one fails, we want them all to run to completion. matrix: - winui: [2] + winui: [2, 3] # Temporary until we can get Uno/Wasm working env: VERSION_PROPERTY: ${{ github.ref == 'refs/heads/main' && format('build.{0}', github.run_number) || format('pull-{0}.{1}', github.event.number, github.run_number) }} @@ -270,7 +270,7 @@ jobs: # Build and pack component nupkg - name: Build and pack component packages - run: ./tooling/Build-Toolkit-Components.ps1 -Components DependencyPropertyGenerator -MultiTargets uwp -ExcludeMultiTargets ${{ env.EXCLUDED_MULTITARGETS }} -WinUIMajorVersion ${{ matrix.winui }} -DateForVersion ${{ env.VERSION_DATE }} ${{ env.VERSION_PROPERTY != '' && format('-PreviewVersion "{0}"', env.VERSION_PROPERTY) || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-EnableBinlogs' || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-Verbose' || '' }} -BinlogOutput ./ -NupkgOutput ./ -Release + run: ./tooling/Build-Toolkit-Components.ps1 -Components DependencyPropertyGenerator -MultiTargets uwp,wasdk -ExcludeMultiTargets ${{ env.EXCLUDED_MULTITARGETS }} -WinUIMajorVersion ${{ matrix.winui }} -DateForVersion ${{ env.VERSION_DATE }} ${{ env.VERSION_PROPERTY != '' && format('-PreviewVersion "{0}"', env.VERSION_PROPERTY) || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-EnableBinlogs' || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-Verbose' || '' }} -BinlogOutput ./ -NupkgOutput ./ -Release - name: Validate package names if: ${{ env.VERSION_PROPERTY != '' }} diff --git a/tooling b/tooling index 43e41c221..4d331c6b2 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 43e41c2215b583ffcd54eb33954c79d069249b55 +Subproject commit 4d331c6b24e258afcd42d28ebf63bea781744541 From c937c4c21e0073d804a6364e91ee1e7af7aaf97a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Dec 2024 17:39:15 -0800 Subject: [PATCH 033/200] Switch generator to WinAppSDK for now --- ...tyToolkit.DependencyPropertyGenerator.SourceGenerators.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index 32c4c1233..6b470e010 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -4,7 +4,6 @@ enable true true - $(DefineConstants);WINDOWS_UWP $(NoWarn);IDE0130 From 9acc0ce14e80eaf59d1ce56ddc6aefa9e1f3e583 Mon Sep 17 00:00:00 2001 From: Arlo Date: Wed, 4 Dec 2024 19:53:25 -0600 Subject: [PATCH 034/200] Temp: Refactor build workflow to use Build-Toolkit-Components script instead of generating solution --- .github/workflows/build.yml | 38 +------------------------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c59212de..0679c7d2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -134,43 +134,7 @@ jobs: # Generate full solution with all projects (sample gallery heads, components, tests) - name: Generate solution with ${{ matrix.multitarget }} gallery, components and tests - working-directory: ./components/DependencyPropertyGenerator - run: powershell -version 5.1 -command "../../tooling/ProjectHeads/GenerateSingleSampleHeads.ps1 -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop - - # Build solution - - name: MSBuild (With diagnostics) - if: ${{ env.ENABLE_DIAGNOSTICS == 'true' }} - working-directory: ./components/DependencyPropertyGenerator - run: > - msbuild.exe /restore /nowarn:MSB4011 - /p:Configuration=Release - /m - ${{ env.ENABLE_DIAGNOSTICS == 'true' && '/bl' || '' }} - /v:${{ env.MSBUILD_VERBOSITY }} - - - name: MSBuild - if: ${{ env.ENABLE_DIAGNOSTICS == 'false' }} - working-directory: ./components/DependencyPropertyGenerator - run: msbuild.exe /restore /nowarn:MSB4011 -p:Configuration=Release - - # Run tests - - name: Setup VSTest Path - uses: darenm/setup-vstest@3a16d909a1f3bbc65b52f8270d475d905e7d3e44 - - - name: Install Testspace Module - uses: testspace-com/setup-testspace@v1 - with: - domain: ${{ github.repository_owner }} - - - name: Run component tests against ${{ matrix.multitarget }} - if: ${{ matrix.multitarget == 'uwp' || matrix.multitarget == 'wasdk' }} - id: test-platform - run: vstest.console.exe ./tooling/**/CommunityToolkit.Tests.${{ matrix.multitarget }}.build.appxrecipe /Framework:FrameworkUap10 /logger:"trx;LogFileName=${{ matrix.multitarget }}.trx" /Blame - - - name: Create test reports - run: | - testspace '[${{ matrix.multitarget }}]./TestResults/*.trx' - if: ${{ (matrix.multitarget == 'uwp' || matrix.multitarget == 'wasdk') && (steps.test-generator.conclusion == 'success' || steps.test-platform.conclusion == 'success') }} + run: powershell -version 5.1 -command "./tooling/Build-Toolkit-Components.ps1 -Components DependencyPropertyGenerator -Release -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop - name: Artifact - Diagnostic Logs uses: actions/upload-artifact@v4 From 05edd936c8b3c6ea660a82f74175ea9288d9939a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Dec 2024 18:34:44 -0800 Subject: [PATCH 035/200] Fix condition for local caching in generator --- .../DependencyPropertyGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index d82418f23..5ebe11b8a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -47,7 +47,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // This generator requires C# preview to be used (due to the use of the 'field' keyword). // The 'field' keyword is actually only used when local caching is enabled, so filter to that. - if (!isLocalCachingEnabled && !context.SemanticModel.Compilation.IsLanguageVersionPreview()) + if (isLocalCachingEnabled && !context.SemanticModel.Compilation.IsLanguageVersionPreview()) { return null; } From c7ab831cbdce705cb18a0362d9d57217b1a7d4d5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Dec 2024 19:53:23 -0800 Subject: [PATCH 036/200] Define 'DependencyPropertyGeneratorUseWindowsUIXaml' --- ...oolkit.WinUI.DependencyPropertyGenerator.targets | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 3a93502bc..0e057128c 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -1,7 +1,18 @@ + + + true + false + + + + + + + - + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML From aff5de987dde2467d27fd42c69c4098eecf1de9b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Dec 2024 21:12:48 -0800 Subject: [PATCH 037/200] Generalize .dll reference check --- ...it.WinUI.DependencyPropertyGenerator.targets | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 0e057128c..29f744307 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -16,27 +16,22 @@ $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML - + - + + false - true + true + true - + Date: Wed, 4 Dec 2024 21:16:58 -0800 Subject: [PATCH 038/200] Add 'ForAttributeWithMetadataNameAndOptions' --- ...eneratorInitializationContextExtensions.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs new file mode 100644 index 000000000..7c3169d03 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs @@ -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.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.GeneratedDependencyProperty.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; +} + +/// +/// Extension methods for . +/// +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)); + } +} From 061f74e46bb7c7b3c3c3cc86991791865c6985a8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Dec 2024 21:56:03 -0800 Subject: [PATCH 039/200] Add 'AnalyzerConfigOptionsExtensions' --- .../AnalyzerConfigOptionsExtensions.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs new file mode 100644 index 000000000..4737574d9 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs @@ -0,0 +1,46 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AnalyzerConfigOptionsExtensions +{ + /// + /// Gets the boolean value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The default value to return if the property is not found or cannot be parsed. + /// The value of the target MSBuild property. + public static bool GetMSBuildBooleanPropertyValue(this AnalyzerConfigOptions options, string propertyName, bool defaultValue = false) + { + if (options.TryGetMSBuildStringPropertyValue(propertyName, out string? propertyValue)) + { + if (bool.TryParse(propertyValue, out bool booleanPropertyValue)) + { + return booleanPropertyValue; + } + } + + return defaultValue; + } + + /// + /// Tries to get a value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The resulting property value. + /// Whether the property value was retrieved.. + public static bool TryGetMSBuildStringPropertyValue(this AnalyzerConfigOptions options, string propertyName, [NotNullWhen(true)] out string? propertyValue) + { + return options.TryGetValue($"build_property.{propertyName}", out propertyValue); + } +} From 6af9be8364414a0bf2876d2dc5ce64d6896ece90 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 4 Dec 2024 21:56:21 -0800 Subject: [PATCH 040/200] Enable XAML option for generators/analyzers --- .../Constants/WellKnownPropertyNames.cs | 16 +++++ .../Constants/WellKnownTypeNames.cs | 66 ++++++++++++++----- .../DependencyPropertyGenerator.Execute.cs | 48 +++++++------- .../DependencyPropertyGenerator.cs | 16 +++-- ...dPropertyConflictingDeclarationAnalyzer.cs | 5 +- ...opertyContainingTypeDeclarationAnalyzer.cs | 5 +- .../Models/DependencyPropertyInfo.cs | 4 +- .../Models/TypedConstantInfo.cs | 5 +- 8 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs new file mode 100644 index 000000000..7d88ca86a --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs @@ -0,0 +1,16 @@ +// 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.GeneratedDependencyProperty.Constants; + +/// +/// The well known names for properties used by source generators and analyzers. +/// +internal static class WellKnownPropertyNames +{ + /// + /// The MSBuild property to control the XAML mode. + /// + public const string DependencyPropertyGeneratorUseWindowsUIXaml = nameof(DependencyPropertyGeneratorUseWindowsUIXaml); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs index 60194c1db..d200ed859 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs @@ -15,38 +15,72 @@ internal static class WellKnownTypeNames public const string GeneratedDependencyPropertyAttribute = "CommunityToolkit.WinUI.GeneratedDependencyPropertyAttribute"; /// - /// The fully qualified type name for the GeneratedDependencyProperty type. + /// The fully qualified name for the GeneratedDependencyProperty type. /// public const string GeneratedDependencyProperty = "CommunityToolkit.WinUI.GeneratedDependencyProperty"; /// - /// The fully qualified type name for the XAML namespace. + /// The fully qualified name for the Windows.UI.Xaml namespace. /// - public const string XamlNamespace = -#if WINDOWS_UWP - "Windows.UI.Xaml"; + public const string WindowsUIXamlNamespace = "Windows.UI.Xaml"; -#else - "Microsoft.UI.Xaml"; -#endif + /// + /// The fully qualified name for the Microsoft.UI.Xaml namespace. + /// + public const string MicrosoftUIXamlNamespace = "Microsoft.UI.Xaml"; + + /// + /// Gets the fully qualified name for theXAML namespace. + /// + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + public static string XamlNamespace(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? WindowsUIXamlNamespace + : MicrosoftUIXamlNamespace; + } /// - /// The fully qualified type name for the DependencyObject type. + /// Gets the fully qualified type name for the DependencyObject type for a given XAML mode. /// - public const string DependencyObject = $"{XamlNamespace}.{nameof(DependencyObject)}"; + /// + public static string DependencyObject(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(DependencyObject)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(DependencyObject)}"; + } /// - /// The fully qualified type name for the DependencyProperty type. + /// Gets the fully qualified type name for the DependencyProperty type. /// - public const string DependencyProperty = $"{XamlNamespace}.{nameof(DependencyProperty)}"; + /// + public static string DependencyProperty(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(DependencyProperty)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(DependencyProperty)}"; + } /// - /// The fully qualified type name for the DependencyPropertyChangedEventArgs type. + /// Gets the fully qualified type name for the DependencyPropertyChangedEventArgs type. /// - public const string DependencyPropertyChangedEventArgs = $"{XamlNamespace}.{nameof(DependencyPropertyChangedEventArgs)}"; + /// + public static string DependencyPropertyChangedEventArgs(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(DependencyPropertyChangedEventArgs)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(DependencyPropertyChangedEventArgs)}"; + } /// - /// The fully qualified type name for the PropertyMetadata type. + /// Gets the fully qualified type name for the PropertyMetadata type. /// - public const string PropertyMetadata = $"{XamlNamespace}.{nameof(PropertyMetadata)}"; + /// + public static string PropertyMetadata(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(PropertyMetadata)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(PropertyMetadata)}"; + } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index f14961453..b94b1e752 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -31,11 +31,6 @@ private static partial class Execute /// private static readonly TypedConstantInfo.Null NullInfo = new(); - /// - /// Placeholder for the unset value of a given property type. - /// - private static readonly TypedConstantInfo.UnsetValue UnsetValueInfo = new(); - /// /// Generates the sources for the embedded types, for PrivateAssets="all" scenarios. /// @@ -109,8 +104,9 @@ public static bool IsCandidateSyntaxValid(SyntaxNode node, CancellationToken tok /// Checks whether an input symbol is a candidate property declaration for the generator. /// /// The input symbol to check. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// Whether is a candidate property declaration. - public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol) + public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol, bool useWindowsUIXaml) { // Ensure that the property declaration is a partial definition with no implementation if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) @@ -131,7 +127,7 @@ public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol) } // Ensure that the containing type derives from 'DependencyObject' - if (!typeSymbol.InheritsFromFullyQualifiedMetadataName(WellKnownTypeNames.DependencyObject)) + if (!typeSymbol.InheritsFromFullyQualifiedMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml))) { return false; } @@ -143,7 +139,7 @@ public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol) { bool propertyTypeWouldCauseConflicts = propertySymbol.Type.SpecialType == SpecialType.System_Object || - propertySymbol.Type.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs); + propertySymbol.Type.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml)); return !propertyTypeWouldCauseConflicts; } @@ -211,12 +207,14 @@ public static bool TryGetAccessibilityModifiers( /// The input that triggered the annotation. /// The input instance. /// The for the current compilation. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// The used to cancel the operation, if needed. /// The default value to use to initialize the generated property. public static TypedConstantInfo GetDefaultValue( AttributeData attributeData, IPropertySymbol propertySymbol, SemanticModel semanticModel, + bool useWindowsUIXaml, CancellationToken token) { // First, check whether the default value is explicitly set or not @@ -245,7 +243,7 @@ public static TypedConstantInfo GetDefaultValue( // Last step: we want to validate that the reference is actually to the special placeholder if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName(WellKnownTypeNames.GeneratedDependencyProperty)) { - return UnsetValueInfo; + return new TypedConstantInfo.UnsetValue(useWindowsUIXaml); } } } @@ -281,8 +279,9 @@ public static bool IsLocalCachingEnabled(AttributeData attributeData) /// Checks whether the generated code has to register the property changed callback with WinRT. /// /// The input instance to process. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// Whether the generated should register the property changed callback. - public static bool IsPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol) + public static bool IsPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol, bool useWindowsUIXaml) { // Check for any 'OnChanged' methods foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}PropertyChanged")) @@ -296,7 +295,7 @@ public static bool IsPropertyChangedCallbackImplemented(IPropertySymbol property // There might be other property changed callback methods when field caching is enabled, or in other scenarios. // Because the callback method existing adds overhead (since we have to register it with WinRT), we want to // avoid false positives. To do that, we check that the parameter type is exactly the one we need. - if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs)) + if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml))) { return true; } @@ -309,8 +308,9 @@ public static bool IsPropertyChangedCallbackImplemented(IPropertySymbol property /// Checks whether the generated code has to register the shared property changed callback with WinRT. /// /// The input instance to process. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// Whether the generated should register the shared property changed callback. - public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol) + public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol propertySymbol, bool useWindowsUIXaml) { // Check for any 'OnPropertyChanged' methods foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers("OnPropertyChanged")) @@ -322,7 +322,7 @@ public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol pr } // Also same actual check as above - if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs)) + if (argsType.HasFullyQualifiedMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml))) { return true; } @@ -385,36 +385,36 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { DefaultValue: TypedConstantInfo.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } - => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue})", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", // Codegen for legacy UWP { IsNet8OrGreater: false } => propertyInfo switch { { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } - => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } - => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e))", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e))", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } - => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, static (d, e) => {{ (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e); (({typeQualifiedName})d).OnPropertyChanged(e); }})", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => {{ (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e); (({typeQualifiedName})d).OnPropertyChanged(e); }})", _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), }, // Codegen for .NET 8 or greater { DefaultValue: TypedConstantInfo.Null } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) - => $"new global::{WellKnownTypeNames.PropertyMetadata}(null, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}(null, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) - => $"new global::{WellKnownTypeNames.PropertyMetadata}({defaultValue}, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), }; writer.WriteLine($$""" /// - /// The backing instance for . + /// The backing instance for . /// """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); writer.WriteLine($$""" - public static readonly global::{{WellKnownTypeNames.DependencyProperty}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty}}.Register( + public static readonly global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}}.Register( name: "{{propertyInfo.PropertyName}}", propertyType: typeof({{propertyInfo.TypeName}}), ownerType: typeof({{typeQualifiedName}}), @@ -650,7 +650,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) writer.WriteLine($""" /// Executes the logic for when has just changed. /// Event data that is issued by any event that tracks changes to the effective value of this property. - /// This method is invoked by the infrastructure, after the value of is changed. + /// This method is invoked by the infrastructure, after the value of is changed. """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); writer.WriteLine($"partial void On{propertyInfo.PropertyName}PropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs} e);"); @@ -661,7 +661,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) writer.WriteLine($""" /// Executes the logic for when any dependency property has just changed. /// Event data that is issued by any event that tracks changes to the effective value of this property. - /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); writer.WriteLine($"partial void OnPropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs} e);"); @@ -700,7 +700,7 @@ public static bool RequiresAdditionalTypes(EquatableArray propertyInfos, IndentedTextWriter writer) { writer.WriteLine("using global::System.Runtime.CompilerServices;"); - writer.WriteLine($"using global::{WellKnownTypeNames.XamlNamespace};"); + writer.WriteLine($"using global::{WellKnownTypeNames.XamlNamespace(propertyInfos[0].UseWindowsUIXaml)};"); writer.WriteLine(); writer.WriteLine($$""" /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index 5ebe11b8a..2e21759c1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -31,8 +31,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Get the info on all dependency properties to generate IncrementalValuesProvider propertyInfo = - context.SyntaxProvider - .ForAttributeWithMetadataName( + context.ForAttributeWithMetadataNameAndOptions( WellKnownTypeNames.GeneratedDependencyPropertyAttribute, Execute.IsCandidateSyntaxValid, static (context, token) => @@ -60,8 +59,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return null; } + // Get the XAML mode to use + bool useWindowsUIXaml = context.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + // Do an initial filtering on the symbol as well - if (!Execute.IsCandidateSymbolValid(propertySymbol)) + if (!Execute.IsCandidateSymbolValid(propertySymbol, useWindowsUIXaml)) { return null; } @@ -87,8 +89,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); bool isRequired = Execute.IsRequiredProperty(propertySymbol); - bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol); - bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol); + bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); + bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); bool isNet8OrGreater = !context.SemanticModel.Compilation.IsWindowsRuntimeApplication(); token.ThrowIfCancellationRequested(); @@ -103,6 +105,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.Attributes[0], propertySymbol, context.SemanticModel, + useWindowsUIXaml, token); // The 'UnsetValue' can only be used when local caching is disabled @@ -132,7 +135,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IsLocalCachingEnabled: isLocalCachingEnabled, IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, - IsNet8OrGreater: isNet8OrGreater); + IsNet8OrGreater: isNet8OrGreater, + UseWindowsUIXaml: useWindowsUIXaml); }) .WithTrackingName(WellKnownTrackingNames.Execute) .Where(static item => item is not null)!; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs index 0c204d0c5..5bf8bf9ac 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs @@ -28,11 +28,14 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); // Get the 'DependencyPropertyChangedEventArgs' symbol - if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs) is not { } dependencyPropertyChangedEventArgsSymbol) + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyPropertyChangedEventArgs(useWindowsUIXaml)) is not { } dependencyPropertyChangedEventArgsSymbol) { return; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs index af90b2470..10fe9967c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs @@ -28,11 +28,14 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); // Get the 'DependencyObject' symbol - if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject) is not { } dependencyObjectSymbol) + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml)) is not { } dependencyObjectSymbol) { return; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs index 3952dd95d..38a5af29d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -23,6 +23,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// Indicates whether the WinRT-based property changed callback is implemented. /// Indicates whether the WinRT-based shared property changed callback is implemented. /// Indicates whether the current target is .NET 8 or greater. +/// Whether to use the UWP XAML or WinUI 3 XAML namespaces. internal sealed record DependencyPropertyInfo( HierarchyInfo Hierarchy, string PropertyName, @@ -37,4 +38,5 @@ internal sealed record DependencyPropertyInfo( bool IsLocalCachingEnabled, bool IsPropertyChangedCallbackImplemented, bool IsSharedPropertyChangedCallbackImplemented, - bool IsNet8OrGreater); + bool IsNet8OrGreater, + bool UseWindowsUIXaml); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index d06ef9618..bbc5f5710 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -176,12 +176,13 @@ public override string ToString() /// /// A type representing the special unset value. /// - public sealed record UnsetValue : TypedConstantInfo + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + public sealed record UnsetValue(bool UseWindowsUIXaml) : TypedConstantInfo { /// public override string ToString() { - return $"global::{WellKnownTypeNames.DependencyProperty}.UnsetValue"; + return $"global::{WellKnownTypeNames.DependencyProperty(UseWindowsUIXaml)}.UnsetValue"; } } } From c100c0eb793751562f8f4ac4298c213031fcabae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 08:20:52 -0800 Subject: [PATCH 041/200] Enable Windows.UI.Xaml for legacy UWP --- .../CommunityToolkit.WinUI.DependencyPropertyGenerator.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 29f744307..7806f34d3 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -2,6 +2,7 @@ + true true false From 9439f37e2126df3467985ce4ef6132c4b8ab287f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 18:54:39 -0800 Subject: [PATCH 042/200] Add 'SyntaxKind' extensions --- .../Extensions/SyntaxKindExtensions.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs new file mode 100644 index 000000000..6b966edaf --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -0,0 +1,39 @@ +// 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.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// A with some extension methods for C# syntax kinds. +/// +internal static partial class SyntaxKindExtensions +{ + /// + /// Converts an of values to one of their underlying type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsUnderlyingType(this ImmutableArray array) + { + ushort[]? underlyingArray = (ushort[]?)(object?)Unsafe.As, SyntaxKind[]?>(ref array); + + return Unsafe.As>(ref underlyingArray); + } + + /// + /// Converts an of values to one of their real type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsSyntaxKindArray(this ImmutableArray array) + { + SyntaxKind[]? typedArray = (SyntaxKind[]?)(object?)Unsafe.As, ushort[]?>(ref array); + + return Unsafe.As>(ref typedArray); + } +} From eb41e2869d1e858cd6fd0b73400fd08720e101c3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 19:27:46 -0800 Subject: [PATCH 043/200] Add support for more modifiers, bug fixes --- .../DependencyPropertyGenerator.Execute.cs | 59 ++++++++++++++----- .../DependencyPropertyGenerator.cs | 9 ++- .../Extensions/AccessibilityExtensions.cs | 32 ---------- .../Models/DependencyPropertyInfo.cs | 5 +- 4 files changed, 55 insertions(+), 50 deletions(-) delete mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index b94b1e752..3501de65c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -3,6 +3,7 @@ // 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; @@ -147,6 +148,37 @@ public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol, bool u return true; } + /// + /// Gathers all allowed property modifiers that should be forwarded to the generated property. + /// + /// The input node. + /// The returned set of property modifiers, if any. + public static ImmutableArray GetPropertyModifiers(PropertyDeclarationSyntax node) + { + // We only allow a subset of all possible modifiers (aside from the accessibility modifiers) + ReadOnlySpan candidateKinds = + [ + SyntaxKind.NewKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, + SyntaxKind.RequiredKeyword + ]; + + using ImmutableArrayBuilder builder = new(); + + // Track all modifiers from the allowed set on the input property declaration + foreach (SyntaxKind kind in candidateKinds) + { + if (node.Modifiers.Any(kind)) + { + builder.Add(kind); + } + } + + return builder.ToImmutable(); + } + /// /// Tries to get the accessibility of the property and accessors, if possible. /// @@ -331,16 +363,6 @@ public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol pr return false; } - /// - /// Checks whether an input property is required. - /// - /// The input instance to process. - /// Whether is required. - public static bool IsRequiredProperty(IPropertySymbol propertySymbol) - { - return propertySymbol.IsRequired; - } - /// /// Writes all implementations of partial dependency property declarations. /// @@ -367,7 +389,7 @@ static string GetOldValueTypeNameAsNullable(DependencyPropertyInfo propertyInfo) // Helper to get the accessibility with a trailing space static string GetExpressionWithTrailingSpace(Accessibility accessibility) { - return accessibility.GetExpression() switch + return SyntaxFacts.GetText(accessibility) switch { { Length: > 0 } expression => expression + " ", _ => "" @@ -428,11 +450,20 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { string oldValueTypeNameAsNullable = GetOldValueTypeNameAsNullable(propertyInfo); + // Declare the property writer.WriteLine(skipIfPresent: true); writer.WriteLine("/// "); writer.WriteGeneratedAttributes(GeneratorName); writer.Write(GetExpressionWithTrailingSpace(propertyInfo.DeclaredAccessibility)); - writer.WriteIf(propertyInfo.IsRequired, "required "); + + // Add all gathered modifiers + foreach (SyntaxKind modifier in propertyInfo.PropertyModifiers.AsImmutableArray().AsSyntaxKindArray()) + { + writer.Write($"{SyntaxFacts.GetText(modifier)} "); + } + + // The 'partial' modifier always goes last, right before the property type and the property name. + // We will never have the 'partial' modifier in the set of property modifiers processed above. writer.WriteLine($"partial {propertyInfo.TypeNameWithNullabilityAnnotations} {propertyInfo.PropertyName}"); using (writer.WriteBlock()) @@ -653,7 +684,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) /// This method is invoked by the infrastructure, after the value of is changed. """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); - writer.WriteLine($"partial void On{propertyInfo.PropertyName}PropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs} e);"); + writer.WriteLine($"partial void On{propertyInfo.PropertyName}PropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs(propertyInfo.UseWindowsUIXaml)} e);"); } // OnPropertyChanged, for the shared property metadata callback @@ -664,7 +695,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); - writer.WriteLine($"partial void OnPropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs} e);"); + writer.WriteLine($"partial void OnPropertyChanged(global::{WellKnownTypeNames.DependencyPropertyChangedEventArgs(propertyInfos[0].UseWindowsUIXaml)} e);"); } /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index 2e21759c1..ff70652e7 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -2,6 +2,7 @@ // 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 CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using CommunityToolkit.GeneratedDependencyProperty.Helpers; @@ -70,6 +71,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); + // Get all additional modifiers for the property + ImmutableArray propertyModifiers = Execute.GetPropertyModifiers((PropertyDeclarationSyntax)context.TargetNode); + + token.ThrowIfCancellationRequested(); + // Get the accessibility values, if the property is valid if (!Execute.TryGetAccessibilityModifiers( node: (PropertyDeclarationSyntax)context.TargetNode, @@ -88,7 +94,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - bool isRequired = Execute.IsRequiredProperty(propertySymbol); bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); bool isNet8OrGreater = !context.SemanticModel.Compilation.IsWindowsRuntimeApplication(); @@ -124,6 +129,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return new DependencyPropertyInfo( Hierarchy: hierarchyInfo, PropertyName: propertySymbol.Name, + PropertyModifiers: propertyModifiers.AsUnderlyingType(), DeclaredAccessibility: declaredAccessibility, GetterAccessibility: getterAccessibility, SetterAccessibility: setterAccessibility, @@ -131,7 +137,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) TypeNameWithNullabilityAnnotations: typeNameWithNullabilityAnnotations, DefaultValue: defaultValue, IsReferenceTypeOrUnconstraindTypeParameter: isReferenceTypeOrUnconstraindTypeParameter, - IsRequired: isRequired, IsLocalCachingEnabled: isLocalCachingEnabled, IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs deleted file mode 100644 index f7154e446..000000000 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AccessibilityExtensions.cs +++ /dev/null @@ -1,32 +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 Microsoft.CodeAnalysis; - -namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; - -/// -/// Extension methods for the type. -/// -internal static class AccessibilityExtensions -{ - /// - /// Gets the expression for a given value. - /// - /// The input value. - /// The expression for . - public static string GetExpression(this Accessibility accessibility) - { - return accessibility switch - { - Accessibility.Private => "private", - Accessibility.ProtectedAndInternal => "private protected", - Accessibility.Protected => "protected", - Accessibility.Internal => "internal", - Accessibility.ProtectedOrInternal => "protected internal", - Accessibility.Public => "public", - _ => "" - }; - } -} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs index 38a5af29d..363ea6516 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -2,6 +2,7 @@ // 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.GeneratedDependencyProperty.Helpers; using Microsoft.CodeAnalysis; namespace CommunityToolkit.GeneratedDependencyProperty.Models; @@ -11,6 +12,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// /// The hierarchy info for the containing type. /// The property name. +/// The list of additional modifiers for the property (they are values). /// The accessibility of the property, if available. /// The accessibility of the accessor, if available. /// The accessibility of the accessor, if available. @@ -18,7 +20,6 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// The type name for the generated property, including nullability annotations. /// The default value to set the generated property to. /// Indicates whether the property is of a reference type or an unconstrained type parameter. -/// Whether or not the generated property should be marked as required. /// Indicates whether local caching should be used for the property value. /// Indicates whether the WinRT-based property changed callback is implemented. /// Indicates whether the WinRT-based shared property changed callback is implemented. @@ -27,6 +28,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; internal sealed record DependencyPropertyInfo( HierarchyInfo Hierarchy, string PropertyName, + EquatableArray PropertyModifiers, Accessibility DeclaredAccessibility, Accessibility GetterAccessibility, Accessibility SetterAccessibility, @@ -34,7 +36,6 @@ internal sealed record DependencyPropertyInfo( string TypeNameWithNullabilityAnnotations, TypedConstantInfo DefaultValue, bool IsReferenceTypeOrUnconstraindTypeParameter, - bool IsRequired, bool IsLocalCachingEnabled, bool IsPropertyChangedCallbackImplemented, bool IsSharedPropertyChangedCallbackImplemented, From 1b40cafb8aa98c8ac1922b5af0bc8392061581d9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 19:32:27 -0800 Subject: [PATCH 044/200] Add unit tests for more modifiers --- .../Test_DependencyPropertyGenerator.cs | 362 +++++++++++++++++- 1 file changed, 361 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 76587fa83..8cdace498 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -1648,7 +1648,7 @@ public partial string? Name } [TestMethod] - public void SingleProperty_String_WithNoCaching_IsRequired() + public void SingleProperty_String_WithNoCaching_Required() { const string source = """ using Windows.UI.Xaml; @@ -1763,6 +1763,366 @@ public required partial string Name CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + [TestMethod] + public void SingleProperty_String_WithNoCaching_New() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public class BaseControl : DependencyObject + { + public new string Name { get; set; } + } + + public partial class MyControl : BaseControl + { + [GeneratedDependencyProperty] + public new partial string Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public new partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_String_WithNoCaching_Virtual() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public virtual partial string Name { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public virtual partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + [DataRow("override")] + [DataRow("sealed override")] + public void SingleProperty_String_WithNoCaching_Override(string modifiers) + { + string source = $$""" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public class BaseControl : DependencyObject + { + public virtual string Name { get; set; } + } + + public partial class MyControl : BaseControl + { + [GeneratedDependencyProperty] + public {{modifiers}} partial string Name { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public {{modifiers}} partial string Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + [TestMethod] public void MultipleProperties_WithNoCaching_CorrectSpacing() { From e152803168b98321f24a38a79bfcdeee34a32014 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 19:50:59 -0800 Subject: [PATCH 045/200] Add diagnostic and test for pointer types --- .../AnalyzerReleases.Shipped.md | 1 + .../DependencyPropertyGenerator.Execute.cs | 11 +++++- ...nvalidPropertySymbolDeclarationAnalyzer.cs | 16 ++++++-- .../Diagnostics/DiagnosticDescriptors.cs | 16 ++++++++ .../Test_Analyzers.cs | 38 +++++++++++++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index a7615b210..d010b84f3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -18,3 +18,4 @@ WCTDP0008 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0009 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0010 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0011 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0012 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 3501de65c..9034585ed 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -115,8 +115,15 @@ public static bool IsCandidateSymbolValid(IPropertySymbol propertySymbol, bool u return false; } - // Also ignore all properties that have an invalid declaration - if (propertySymbol.IsStatic || propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly || propertySymbol.Type.IsRefLikeType) + // Also ignore all properties returning a byref-like value. We don't need to also + // check for ref values here, as that's already validated by the syntax filter. + if (propertySymbol.Type.IsRefLikeType) + { + return false; + } + + // Pointer types are never allowed + if (propertySymbol.Type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer) { return false; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs index 60a26e2b6..9284c6593 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySymbolDeclarationAnalyzer.cs @@ -22,7 +22,8 @@ public sealed class InvalidPropertySymbolDeclarationAnalyzer : DiagnosticAnalyze [ InvalidPropertyDeclarationIsNotIncompletePartialDefinition, InvalidPropertyDeclarationReturnsByRef, - InvalidPropertyDeclarationReturnsRefLikeType + InvalidPropertyDeclarationReturnsRefLikeType, + InvalidPropertyDeclarationReturnsPointerType ]; /// @@ -86,15 +87,22 @@ public override void Initialize(AnalysisContext context) attributeData.GetLocation(), propertySymbol)); } - - // Emit an error if the property type is a ref struct - if (propertySymbol.Type.IsRefLikeType) + else if (propertySymbol.Type.IsRefLikeType) { + // Emit an error if the property type is a ref struct context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDeclarationReturnsRefLikeType, attributeData.GetLocation(), propertySymbol)); } + else if (propertySymbol.Type.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer) + { + // Emit a diagnostic if the type is a pointer type + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationReturnsPointerType, + attributeData.GetLocation(), + propertySymbol)); + } }, SymbolKind.Property); }); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 213b3e8d7..7d3b61cb1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -153,4 +153,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a pointer type. + /// + /// Format: "The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". + /// + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsPointerType = new( + id: "WCTDP0012", + title: "Using [GeneratedDependencyProperty] on a property that returns pointer type", + messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a pointer value ([GeneratedDependencyProperty] must be used on properties returning a non pointer value)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] must not return a pointer value (only reference types and non byref-like types are supported).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index deaa19b7b..3a9c8e330 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -339,6 +339,44 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsPointerType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public unsafe partial class MyControl : Control + { + [{|WCTDP0012:GeneratedDependencyProperty|}] + public partial int* {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertySymbolDeclarationAnalyzer_ReturnsFunctionPointerType_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public unsafe partial class MyControl : Control + { + [{|WCTDP0012:GeneratedDependencyProperty|}] + public partial delegate* unmanaged[Stdcall] {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyContainingTypeDeclarationAnalyzer_NoAttribute_DoesNotWarn() { From 95590f06d14911bb6857ebecd02f6ef323b2c393 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 10:19:39 -0800 Subject: [PATCH 046/200] Add 'DefaultValueCallback' property --- .../GeneratedDependencyPropertyAttribute.cs | 13 +++++++++++++ .../src/GeneratedDependencyPropertyAttribute.cs | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs index 752240f7e..c94eb6652 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -57,9 +57,22 @@ internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attr /// /// To set the default value to , use . /// + /// + /// Using this property is mutually exclusive with . + /// /// public object? DefaultValue { get; init; } = null; + /// + /// Gets or sets the name of the method that will be invoked to produce the default value of the + /// property, for each instance of the containing type. The referenced method needs to return either + /// an , or a value of exactly the property type, and it needs to be parameterless. + /// + /// + /// Using this property is mutually exclusive with . + /// + public string? DefaultValueCallback { get; init; } = null; + /// /// Gets a value indicating whether or not property values should be cached locally, to improve performance. /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs index d7b016cfe..1510b9f33 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -48,9 +48,22 @@ public sealed class GeneratedDependencyPropertyAttribute : Attribute /// /// To set the default value to , use . /// + /// + /// Using this property is mutually exclusive with . + /// /// public object? DefaultValue { get; init; } = null; + /// + /// Gets or sets the name of the method that will be invoked to produce the default value of the + /// property, for each instance of the containing type. The referenced method needs to return either + /// an , or a value of exactly the property type, and it needs to be parameterless. + /// + /// + /// Using this property is mutually exclusive with . + /// + public string? DefaultValueCallback { get; init; } = null; + /// /// Gets a value indicating whether or not property values should be cached locally, to improve performance. /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. From 22ab8688a02688d8812d9f6176386dad59e46703 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 11:28:35 -0800 Subject: [PATCH 047/200] Add initial support for default value callbacks --- .../Constants/WellKnownTypeNames.cs | 11 +++ .../DependencyPropertyGenerator.Execute.cs | 68 +++++++++++++--- .../DependencyPropertyGenerator.cs | 4 +- .../Models/DependencyPropertyDefaultValue.cs | 77 +++++++++++++++++++ .../Models/DependencyPropertyInfo.cs | 2 +- .../Models/TypedConstantInfo.cs | 52 +++---------- 6 files changed, 161 insertions(+), 53 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs index d200ed859..da021cad5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTypeNames.cs @@ -83,4 +83,15 @@ public static string PropertyMetadata(bool useWindowsUIXaml) ? $"{WindowsUIXamlNamespace}.{nameof(PropertyMetadata)}" : $"{MicrosoftUIXamlNamespace}.{nameof(PropertyMetadata)}"; } + + /// + /// Gets the fully qualified type name for the CreateDefaultValueCallback type. + /// + /// + public static string CreateDefaultValueCallback(bool useWindowsUIXaml) + { + return useWindowsUIXaml + ? $"{WindowsUIXamlNamespace}.{nameof(CreateDefaultValueCallback)}" + : $"{MicrosoftUIXamlNamespace}.{nameof(CreateDefaultValueCallback)}"; + } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 9034585ed..6dbbf15ff 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -30,7 +30,7 @@ private static partial class Execute /// /// Placeholder for . /// - private static readonly TypedConstantInfo.Null NullInfo = new(); + private static readonly DependencyPropertyDefaultValue.Null NullInfo = new(); /// /// Generates the sources for the embedded types, for PrivateAssets="all" scenarios. @@ -249,20 +249,64 @@ public static bool TryGetAccessibilityModifiers( /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// The used to cancel the operation, if needed. /// The default value to use to initialize the generated property. - public static TypedConstantInfo GetDefaultValue( + public static DependencyPropertyDefaultValue GetDefaultValue( AttributeData attributeData, IPropertySymbol propertySymbol, SemanticModel semanticModel, bool useWindowsUIXaml, CancellationToken token) { - // First, check whether the default value is explicitly set or not + // First, check if we have a callback + if (attributeData.TryGetNamedArgument("DefaultValueCallback", out TypedConstant defaultValueCallback)) + { + // This must be a valid 'string' value + if (defaultValueCallback is { Type.SpecialType: SpecialType.System_String, Value: string { Length: > 0 } methodName }) + { + ImmutableArray memberSymbols = propertySymbol.ContainingType!.GetMembers(methodName); + + foreach (ISymbol member in memberSymbols) + { + // We need methods which are static and with no parameters (and that is not explicitly implemented) + if (member is not IMethodSymbol { IsStatic: true, Parameters: [], ExplicitInterfaceImplementations: [] } methodSymbol) + { + continue; + } + + // Match the exact method name too + if (methodSymbol.Name != methodName) + { + continue; + } + + bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + + // We have a candidate, now we need to match the return type. Also handle + // for nullable value types where we're returning the raw value directly. + if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || + (isNullableValueType && SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) || + (!isNullableValueType && SymbolEqualityComparer.Default.Equals(propertySymbol.Type, methodSymbol.ReturnType))) + { + return new DependencyPropertyDefaultValue.Callback(methodName); + } + + // The method name cannot possibly be valid, we can stop here + break; + } + } + + // Invalid callback, the analyzer will emit an error + return NullInfo; + } + + token.ThrowIfCancellationRequested(); + + // Next, check whether the default value is explicitly set or not if (attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) { // If the explicit value is anything other than 'null', we can return it directly if (!defaultValue.IsNull) { - return TypedConstantInfo.Create(defaultValue); + return new DependencyPropertyDefaultValue.Constant(TypedConstantInfo.Create(defaultValue)); } // If we do have a default value, we also want to check whether it's the special 'UnsetValue' placeholder. @@ -282,7 +326,7 @@ public static TypedConstantInfo GetDefaultValue( // Last step: we want to validate that the reference is actually to the special placeholder if (fieldSymbol.ContainingType!.HasFullyQualifiedMetadataName(WellKnownTypeNames.GeneratedDependencyProperty)) { - return new TypedConstantInfo.UnsetValue(useWindowsUIXaml); + return new DependencyPropertyDefaultValue.UnsetValue(useWindowsUIXaml); } } } @@ -293,11 +337,13 @@ public static TypedConstantInfo GetDefaultValue( return NullInfo; } + token.ThrowIfCancellationRequested(); + // In all other cases, we'll automatically use the default value of the type in question. // First we need to special case non nullable values, as for those we need 'default'. if (propertySymbol.Type is { IsValueType: true } and not INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }) { - return new TypedConstantInfo.Default(propertySymbol.Type.GetFullyQualifiedName()); + return new DependencyPropertyDefaultValue.Default(propertySymbol.Type.GetFullyQualifiedName()); } // For all other ones, we can just use the 'null' placeholder again @@ -411,8 +457,10 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) string typeMetadata = propertyInfo switch { // Shared codegen - { DefaultValue: TypedConstantInfo.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } - => "null", + { DefaultValue: DependencyPropertyDefaultValue.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => "null", + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}{methodName})", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", @@ -429,7 +477,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) }, // Codegen for .NET 8 or greater - { DefaultValue: TypedConstantInfo.Null } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) + { DefaultValue: DependencyPropertyDefaultValue.Null } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}(null, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", @@ -512,7 +560,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) """, isMultiline: true); // If the default value is not what the default field value would be, add an initializer - if (propertyInfo.DefaultValue is not (TypedConstantInfo.Null or TypedConstantInfo.Default)) + if (propertyInfo.DefaultValue is not (DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default or DependencyPropertyDefaultValue.Callback)) { writer.Write($" = {propertyInfo.DefaultValue};"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index ff70652e7..fb9edc8b3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -106,7 +106,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) bool isReferenceTypeOrUnconstraindTypeParameter = !propertySymbol.Type.IsValueType; // Also get the default value (this might be slightly expensive, so do it towards the end) - TypedConstantInfo defaultValue = Execute.GetDefaultValue( + DependencyPropertyDefaultValue defaultValue = Execute.GetDefaultValue( context.Attributes[0], propertySymbol, context.SemanticModel, @@ -114,7 +114,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token); // The 'UnsetValue' can only be used when local caching is disabled - if (defaultValue is TypedConstantInfo.UnsetValue && isLocalCachingEnabled) + if (defaultValue is DependencyPropertyDefaultValue.UnsetValue && isLocalCachingEnabled) { return null; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs new file mode 100644 index 000000000..b494f4c5c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -0,0 +1,77 @@ +// 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.GeneratedDependencyProperty.Constants; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing a default value for a dependency property. +/// +internal abstract partial record DependencyPropertyDefaultValue +{ + /// + /// A type representing a value. + /// + public sealed record Null : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return "null"; + } + } + + /// + /// A type representing default value for a specific type. + /// + /// The input type name. + public sealed record Default(string TypeName) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return $"default({TypeName})"; + } + } + + /// + /// A type representing the special unset value. + /// + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + public sealed record UnsetValue(bool UseWindowsUIXaml) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return $"global::{WellKnownTypeNames.DependencyProperty(UseWindowsUIXaml)}.UnsetValue"; + } + } + + /// + /// A type representing a constant value. + /// + /// The constant value. + public sealed record Constant(TypedConstantInfo Value) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return Value.ToString(); + } + } + + /// + /// A type representing a callback. + /// + /// The name of the callback method to invoke. + public sealed record Callback(string MethodName) : DependencyPropertyDefaultValue + { + /// + public override string ToString() + { + return MethodName; + } + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs index 363ea6516..428d9bf3d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -34,7 +34,7 @@ internal sealed record DependencyPropertyInfo( Accessibility SetterAccessibility, string TypeName, string TypeNameWithNullabilityAnnotations, - TypedConstantInfo DefaultValue, + DependencyPropertyDefaultValue DefaultValue, bool IsReferenceTypeOrUnconstraindTypeParameter, bool IsLocalCachingEnabled, bool IsPropertyChangedCallbackImplemented, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index bbc5f5710..48accd4e1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -5,7 +5,6 @@ using System; using System.Globalization; using System.Linq; -using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Helpers; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -17,9 +16,20 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// /// A model representing a typed constant item. /// -/// This model is fully serializable and comparable. internal abstract partial record TypedConstantInfo { + /// + /// A type representing a value. + /// + public sealed record Null : TypedConstantInfo + { + /// + public override string ToString() + { + return "null"; + } + } + /// /// A type representing an array. /// @@ -147,42 +157,4 @@ public override string ToString() return $"({TypeName}){valueExpression.NormalizeWhitespace(eol: "\n").ToFullString()}"; } } - - /// - /// A type representing a value. - /// - public sealed record Null : TypedConstantInfo - { - /// - public override string ToString() - { - return "null"; - } - } - - /// - /// A type representing default value for a specific type. - /// - /// The input type name. - public sealed record Default(string TypeName) : TypedConstantInfo - { - /// - public override string ToString() - { - return $"default({TypeName})"; - } - } - - /// - /// A type representing the special unset value. - /// - /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. - public sealed record UnsetValue(bool UseWindowsUIXaml) : TypedConstantInfo - { - /// - public override string ToString() - { - return $"global::{WellKnownTypeNames.DependencyProperty(UseWindowsUIXaml)}.UnsetValue"; - } - } } From c0536991578200b71507e53a2f14108c1f5b7a2b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 21:44:53 -0800 Subject: [PATCH 048/200] Fix value callbacks, add unit tests --- .../DependencyPropertyGenerator.Execute.cs | 19 +- .../Test_DependencyPropertyGenerator.cs | 367 ++++++++++++++++++ 2 files changed, 380 insertions(+), 6 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 6dbbf15ff..e8e3d5d5c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -278,13 +278,20 @@ public static DependencyPropertyDefaultValue GetDefaultValue( continue; } + // We have a candidate, now we need to match the return type. First, + // we just check whether the return is 'object', or an exact match. + if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || + SymbolEqualityComparer.Default.Equals(propertySymbol.Type, methodSymbol.ReturnType)) + { + return new DependencyPropertyDefaultValue.Callback(methodName); + } + bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; - // We have a candidate, now we need to match the return type. Also handle - // for nullable value types where we're returning the raw value directly. - if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || - (isNullableValueType && SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) || - (!isNullableValueType && SymbolEqualityComparer.Default.Equals(propertySymbol.Type, methodSymbol.ReturnType))) + // Otherwise, try to see if the return is the type argument of a nullable value type + if (isNullableValueType && + methodSymbol.ReturnType.TypeKind is TypeKind.Struct && + SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) { return new DependencyPropertyDefaultValue.Callback(methodName); } @@ -460,7 +467,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { DefaultValue: DependencyPropertyDefaultValue.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}{methodName})", + => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}))", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 8cdace498..6e6645d77 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -2319,4 +2319,371 @@ public partial string? LastName CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + + [TestMethod] + [DataRow("int")] + [DataRow("object")] + [DataRow("object?")] + public void SingleProperty_Int32_WithNoCaching_WithDefaultValueCallback(string returnType) + { + string source = $$""" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateNumber))] + public partial int Number { get; set; } + + private static {{returnType}} CreateNumber() => 42; + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int __unboxedValue = (int)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int")] + [DataRow("int?")] + [DataRow("object")] + [DataRow("object?")] + public void SingleProperty_NullableOfInt32_WithNoCaching_WithDefaultValueCallback(string returnType) + { + string source = $$""" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateNumber))] + public partial int? Number { get; set; } + + private static {{returnType}} CreateNumber() => 42; + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(int?), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + int? __unboxedValue = (int?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref int? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref int? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(int? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string")] + [DataRow("string?")] + [DataRow("object")] + [DataRow("object?")] + public void SingleProperty_String_WithNoCaching_WithDefaultValueCallback(string returnType) + { + string source = $$""" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(CreateName))] + public partial string? Name { get; set; } + + private static {{returnType}} CreateName() => "Bob"; + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateName))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } } From b0e178e5dcc72bfd67c2343f9afa473d8ef0ca53 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 22:01:54 -0800 Subject: [PATCH 049/200] Add '[DisallowNull]' to 'DefaultValueCallback' --- .../GeneratedDependencyPropertyAttribute.cs | 3 +++ .../src/GeneratedDependencyPropertyAttribute.cs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs index c94eb6652..bcffcfa02 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -71,6 +71,9 @@ internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attr /// /// Using this property is mutually exclusive with . /// +#if NET8_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DisallowNull] +#endif public string? DefaultValueCallback { get; init; } = null; /// diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs index 1510b9f33..ffc3dca67 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +#if NET8_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif #if WINDOWS_UWP || HAS_UNO using DependencyObject = Windows.UI.Xaml.DependencyObject; using DependencyProperty = Windows.UI.Xaml.DependencyProperty; @@ -62,6 +65,9 @@ public sealed class GeneratedDependencyPropertyAttribute : Attribute /// /// Using this property is mutually exclusive with . /// +#if NET8_0_OR_GREATER + [DisallowNull] +#endif public string? DefaultValueCallback { get; init; } = null; /// From b4bf0a32aa7c23f3f7c10457fc33a3636e1e2f22 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Dec 2024 10:32:43 -0800 Subject: [PATCH 050/200] Add 'InvalidPropertyDefaultValueCallbackTypeAnalyzer' --- .../AnalyzerReleases.Shipped.md | 3 + .../DependencyPropertyGenerator.Execute.cs | 36 +--- ...ropertyDefaultValueCallbackTypeAnalyzer.cs | 159 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 44 ++++- 4 files changed, 206 insertions(+), 36 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index d010b84f3..ce5ace20f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -19,3 +19,6 @@ WCTDP0009 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0010 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0011 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0012 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0013 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0014 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0015 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index e8e3d5d5c..dc64367b6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -262,42 +262,14 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // This must be a valid 'string' value if (defaultValueCallback is { Type.SpecialType: SpecialType.System_String, Value: string { Length: > 0 } methodName }) { - ImmutableArray memberSymbols = propertySymbol.ContainingType!.GetMembers(methodName); - - foreach (ISymbol member in memberSymbols) + // Check that we can find a potential candidate callback method + if (InvalidPropertyDefaultValueCallbackTypeAnalyzer.TryFindDefaultValueCallbackMethod(propertySymbol, methodName, out IMethodSymbol? methodSymbol)) { - // We need methods which are static and with no parameters (and that is not explicitly implemented) - if (member is not IMethodSymbol { IsStatic: true, Parameters: [], ExplicitInterfaceImplementations: [] } methodSymbol) - { - continue; - } - - // Match the exact method name too - if (methodSymbol.Name != methodName) - { - continue; - } - - // We have a candidate, now we need to match the return type. First, - // we just check whether the return is 'object', or an exact match. - if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || - SymbolEqualityComparer.Default.Equals(propertySymbol.Type, methodSymbol.ReturnType)) + // Validate the method has a valid signature as well + if (InvalidPropertyDefaultValueCallbackTypeAnalyzer.IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) { return new DependencyPropertyDefaultValue.Callback(methodName); } - - bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; - - // Otherwise, try to see if the return is the type argument of a nullable value type - if (isNullableValueType && - methodSymbol.ReturnType.TypeKind is TypeKind.Struct && - SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) - { - return new DependencyPropertyDefaultValue.Callback(methodName); - } - - // The method name cannot possibly be valid, we can stop here - break; } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs new file mode 100644 index 000000000..e2577b301 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -0,0 +1,159 @@ +// 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.Diagnostics.CodeAnalysis; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error whenever [GeneratedDependencyProperty] is used with an invalid default value callback argument. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyDefaultValueCallbackTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidPropertyDeclarationDefaultValueCallbackMixed, + InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound, + InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + // Validate that we do have a property + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // If 'DefaultValueCallback' is not set, there's nothing to do + if (!attributeData.TryGetNamedArgument("DefaultValueCallback", out string? defaultValueCallback)) + { + return; + } + + // Emit a diagnostic if 'DefaultValue' is also being set + if (attributeData.TryGetNamedArgument("DefaultValue", out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackMixed, + attributeData.GetLocation(), + propertySymbol)); + } + + // If 'DefaultValueCallback' is 'null', ignore it (Roslyn will already warn here) + if (defaultValueCallback is null) + { + return; + } + + // Emit a diagnostic if we can't find a candidate method + if (!TryFindDefaultValueCallbackMethod(propertySymbol, defaultValueCallback, out IMethodSymbol? methodSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound, + attributeData.GetLocation(), + propertySymbol, + defaultValueCallback)); + } + else if (!IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) + { + // Emit a diagnostic if the candidate method is not valid + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod, + attributeData.GetLocation(), + propertySymbol, + defaultValueCallback)); + } + + }, SymbolKind.Property); + }); + } + + /// + /// Tries to find a candidate default value callback method for a given property. + /// + /// The currently being targeted by the analyzer. + /// The name of the default value callback method to look for. + /// The for the resulting default value callback candidate method, if found. + /// Whether could be found. + public static bool TryFindDefaultValueCallbackMethod(IPropertySymbol propertySymbol, string methodName, [NotNullWhen(true)] out IMethodSymbol? methodSymbol) + { + ImmutableArray memberSymbols = propertySymbol.ContainingType!.GetMembers(methodName); + + foreach (ISymbol member in memberSymbols) + { + // We need methods which are static and with no parameters (and that is not explicitly implemented) + if (member is not IMethodSymbol { IsStatic: true, Parameters: [], ExplicitInterfaceImplementations: [] } candidateSymbol) + { + continue; + } + + // Match the exact method name too + if (candidateSymbol.Name == methodName) + { + methodSymbol = candidateSymbol; + + return true; + } + } + + methodSymbol = null; + + return false; + } + + /// + /// Checks whether a given default value callback method is valid for a given property. + /// + /// The currently being targeted by the analyzer. + /// The for the candidate default value callback method to validate. + /// Whether is a valid default value callback method for . + public static bool IsDefaultValueCallbackValid(IPropertySymbol propertySymbol, IMethodSymbol methodSymbol) + { + // We have a candidate, now we need to match the return type. First, + // we just check whether the return is 'object', or an exact match. + if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || + SymbolEqualityComparer.Default.Equals(propertySymbol.Type, methodSymbol.ReturnType)) + { + return true; + } + + bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + + // Otherwise, try to see if the return is the type argument of a nullable value type + if (isNullableValueType && + methodSymbol.ReturnType.TypeKind is TypeKind.Struct && + SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) + { + return true; + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 7d3b61cb1..2c04ec645 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -155,10 +155,7 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// Gets a for when [ObservableProperty] is used on a property that returns a pointer type. - /// - /// Format: "The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". - /// + /// "The property '{0}' returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsPointerType = new( id: "WCTDP0012", @@ -169,4 +166,43 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must not return a pointer value (only reference types and non byref-like types are supported).", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackMixed = new( + id: "WCTDP0013", + title: "Using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback'", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] cannot use both 'DefaultValue' and 'DefaultValueCallback' at the same time.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound = new( + id: "WCTDP0014", + title: "Using [GeneratedDependencyProperty] with missing 'DefaultValueCallback' method", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of an accessible method in their same containing type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')". + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod = new( + id: "WCTDP0015", + title: "Using [GeneratedDependencyProperty] with invalid 'DefaultValueCallback' method", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of a method with a valid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object').", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } From ba00de96162ea2ce7cb0fefff2fbec7418176b22 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Dec 2024 12:30:55 -0800 Subject: [PATCH 051/200] Add unit tests for new analyzer --- ...ropertyDefaultValueCallbackTypeAnalyzer.cs | 10 +- .../Test_Analyzers.cs | 231 +++++++++++++++++- 2 files changed, 232 insertions(+), 9 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs index e2577b301..d12d5662c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -108,8 +108,8 @@ public static bool TryFindDefaultValueCallbackMethod(IPropertySymbol propertySym foreach (ISymbol member in memberSymbols) { - // We need methods which are static and with no parameters (and that is not explicitly implemented) - if (member is not IMethodSymbol { IsStatic: true, Parameters: [], ExplicitInterfaceImplementations: [] } candidateSymbol) + // Ignore all other member types + if (member is not IMethodSymbol candidateSymbol) { continue; } @@ -136,6 +136,12 @@ public static bool TryFindDefaultValueCallbackMethod(IPropertySymbol propertySym /// Whether is a valid default value callback method for . public static bool IsDefaultValueCallbackValid(IPropertySymbol propertySymbol, IMethodSymbol methodSymbol) { + // We need methods which are static and with no parameters (and that are not explicitly implemented) + if (methodSymbol is not { IsStatic: true, Parameters: [], ExplicitInterfaceImplementations: [] }) + { + return false; + } + // We have a candidate, now we need to match the return type. First, // we just check whether the return is 'object', or an exact match. if (methodSymbol.ReturnType.SpecialType is SpecialType.System_Object || diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 3a9c8e330..19657c763 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -659,7 +659,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithMaybeNullAttribute_DoesNotWarn() { - string source = $$""" + const string source = """ using System.Diagnostics.CodeAnalysis; using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -682,7 +682,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Required_DoesNotWarn() { - string source = $$""" + const string source = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -703,7 +703,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_NullableDisabled_DoesNotWarn() { - string source = $$""" + const string source = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -722,7 +722,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValue_DoesNotWarn() { - string source = $$""" + const string source = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -743,7 +743,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Warns() { - string source = $$""" + const string source = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -764,7 +764,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValue_Warns() { - string source = $$""" + const string source = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -896,7 +896,7 @@ public partial class MyControl : Control [TestMethod] public async Task InvalidPropertyDefaultValueTypeAnalyzer_NullValue_NonNullable_Warns() { - string source = $$""" + const string source = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml.Controls; @@ -938,4 +938,221 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NoDefaultValueCallback1_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NoDefaultValueCallback2_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "Bob")] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_NullDefaultValueCallback_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = null)] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "string")] + [DataRow("string", "string?")] + [DataRow("string", "object")] + [DataRow("string", "object?")] + [DataRow("string?", "string")] + [DataRow("string?", "string?")] + [DataRow("int", "int")] + [DataRow("int", "object")] + [DataRow("int", "object?")] + [DataRow("int?", "int")] + [DataRow("int?", "int?")] + [DataRow("int?", "object")] + [DataRow("int?", "object?")] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_ValidDefaultValueCallback_DoesNotWarn(string propertyType, string returnType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultValue))] + public partial {{propertyType}} {|CS9248:Value|} { get; set; } + + private static {{returnType}} GetDefaultValue() => default!; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_BothDefaultValuePropertiesSet_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0013:GeneratedDependencyProperty(DefaultValue = "Bob", DefaultValueCallback = nameof(GetDefaultName))|}] + public partial string? {|CS9248:Name|} { get; set; } + + private static string? GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_MethodNotFound_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0014:GeneratedDependencyProperty(DefaultValueCallback = "MissingMethod")|}] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_InvalidMethod_ExplicitlyImplemented_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control, IGetDefaultValue + { + [{|WCTDP0014:GeneratedDependencyProperty(DefaultValueCallback = "GetDefaultValue")|}] + public partial string? {|CS9248:Name|} { get; set; } + + static string? IGetDefaultValue.GetDefaultValue() => "Bob"; + } + + public interface IGetDefaultValue + { + static abstract string? GetDefaultValue(); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private string? GetDefaultName()")] + [DataRow("private static string? GetDefaultName(int x)")] + [DataRow("private static int GetDefaultName()")] + [DataRow("private static int GetDefaultName(int x)")] + public async Task InvalidPropertyDefaultValueCallbackTypeAnalyzer_InvalidMethod_Warns(string methodSignature) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0015:GeneratedDependencyProperty(DefaultValueCallback = "GetDefaultName")|}] + public partial string? {|CS9248:Name|} { get; set; } + + {{methodSignature}} => default!; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From 8f971c9ea5e75bf8c34a36cbf917bcea70f599bf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Dec 2024 12:40:32 -0800 Subject: [PATCH 052/200] Fix codegen for all default value callback cases --- .../DependencyPropertyGenerator.Execute.cs | 12 ++++++++++-- .../Test_DependencyPropertyGenerator.cs | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index dc64367b6..9555df548 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -439,13 +439,19 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { DefaultValue: DependencyPropertyDefaultValue.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}))", + => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}))", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", // Codegen for legacy UWP { IsNet8OrGreater: false } => propertyInfo switch { + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } + => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } + => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e))", + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } + => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), static (d, e) => {{ (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e); (({typeQualifiedName})d).OnPropertyChanged(e); }})", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } @@ -456,8 +462,10 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) }, // Codegen for .NET 8 or greater - { DefaultValue: DependencyPropertyDefaultValue.Null } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) + { DefaultValue: DependencyPropertyDefaultValue.Null } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}(null, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName) } + => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 6e6645d77..9dd3b4a05 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -2359,7 +2359,7 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -2481,7 +2481,7 @@ partial class MyControl name: "Number", propertyType: typeof(int?), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -2605,7 +2605,7 @@ partial class MyControl name: "Name", propertyType: typeof(string), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateName))); + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateName))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] From aa1267082c3079f604bd6decac1238290cfddf03 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Dec 2024 14:06:21 -0800 Subject: [PATCH 053/200] Add 'PropertyDeclarationWithPropertyNameSuffixAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...dPropertyConflictingDeclarationAnalyzer.cs | 6 +- ...opertyContainingTypeDeclarationAnalyzer.cs | 6 +- ...ropertyDefaultValueCallbackTypeAnalyzer.cs | 6 +- ...dPropertyNonNullableDeclarationAnalyzer.cs | 2 +- ...nvalidPropertySyntaxDeclarationAnalyzer.cs | 6 +- ...clarationWithPropertyNameSuffixAnalyzer.cs | 55 ++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 13 ++++ .../Test_Analyzers.cs | 65 +++++++++++++++++++ 9 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index ce5ace20f..34bb94f7d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -22,3 +22,4 @@ WCTDP0012 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0013 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0014 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0015 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0016 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs index 5bf8bf9ac..06158fb20 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyConflictingDeclarationAnalyzer.cs @@ -42,11 +42,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Validate that we do have a property - if (context.Symbol is not IPropertySymbol propertySymbol) - { - return; - } + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs index 10fe9967c..e06c981ef 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyContainingTypeDeclarationAnalyzer.cs @@ -42,11 +42,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Validate that we do have a property - if (context.Symbol is not IPropertySymbol propertySymbol) - { - return; - } + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs index d12d5662c..b15719f1f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -39,11 +39,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Validate that we do have a property - if (context.Symbol is not IPropertySymbol propertySymbol) - { - return; - } + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs index 649ed5373..4d729b2e0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs @@ -36,7 +36,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Validate that we do have a property, and that it is of some type that can be explicitly nullable. + // Validate that we have a property that is of some type that can be explicitly nullable. // We're intentionally ignoring 'Nullable' values here, since those are by defintiion nullable. // Additionally, we only care about properties that are explicitly marked as not nullable. // Lastly, we can skip required properties, since for those it's completely fine to be non-nullable. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs index 29df7b907..b26fa16ad 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertySyntaxDeclarationAnalyzer.cs @@ -35,11 +35,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // We're intentionally only looking for properties here - if (context.Symbol is not IPropertySymbol propertySymbol) - { - return; - } + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; // If the property isn't using '[GeneratedDependencyProperty]', there's nothing to do if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs new file mode 100644 index 000000000..6052a8beb --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs @@ -0,0 +1,55 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a diagnostic whenever [GeneratedDependencyProperty] is used on a property with the 'Property' suffix in its name. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PropertyDeclarationWithPropertyNameSuffixAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [PropertyDeclarationWithPropertySuffix]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // We only want to lookup the attribute if the property name actually ends with the 'Property' suffix + if (!propertySymbol.Name.EndsWith("Property")) + { + return; + } + + // Emit a diagnostic if the property is using '[GeneratedDependencyProperty]' + if (propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + context.ReportDiagnostic(Diagnostic.Create( + PropertyDeclarationWithPropertySuffix, + attributeData.GetLocation(), + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 2c04ec645..e254b1a25 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -205,4 +205,17 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of a method with a valid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object').", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)". + /// + public static readonly DiagnosticDescriptor PropertyDeclarationWithPropertySuffix = new( + id: "WCTDP0016", + title: "Using [GeneratedDependencyProperty] on a property with the 'Property' suffix", + messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] should not have the 'Property' suffix in their name, as it is redundant (the generated dependency properties will always add the 'Property' suffix to the name of their associated properties).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 19657c763..2317a547b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1155,4 +1155,69 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + [DataRow("Name")] + [DataRow("TestProperty")] + public async Task PropertyDeclarationWithPropertyNameSuffixAnalyzer_NoAttribute_DoesNotWarn(string propertyName) + { + string source = $$""" + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public partial string? {|CS9248:{{propertyName}}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("Name")] + [DataRow("PropertyGroup")] + public async Task PropertyDeclarationWithPropertyNameSuffixAnalyzer_ValidName_DoesNotWarn(string propertyName) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:{{propertyName}}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task PropertyDeclarationWithPropertyNameSuffixAnalyzer_InvalidName_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0016:GeneratedDependencyProperty|}] + public partial string? {|CS9248:TestProperty|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From e988aab8922a2e76e9913b50b554c15f08ea638d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Dec 2024 14:07:48 -0800 Subject: [PATCH 054/200] Fix two nullability warnings --- .../EmbeddedResources/GeneratedDependencyPropertyAttribute.cs | 2 +- .../src/GeneratedDependencyPropertyAttribute.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs index bcffcfa02..61577eea5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -74,7 +74,7 @@ internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attr #if NET8_0_OR_GREATER [global::System.Diagnostics.CodeAnalysis.DisallowNull] #endif - public string? DefaultValueCallback { get; init; } = null; + public string? DefaultValueCallback { get; init; } = null!; /// /// Gets a value indicating whether or not property values should be cached locally, to improve performance. diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs index ffc3dca67..368fe5fc1 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -68,7 +68,7 @@ public sealed class GeneratedDependencyPropertyAttribute : Attribute #if NET8_0_OR_GREATER [DisallowNull] #endif - public string? DefaultValueCallback { get; init; } = null; + public string? DefaultValueCallback { get; init; } = null!; /// /// Gets a value indicating whether or not property values should be cached locally, to improve performance. From 4a42758ce5296c7f7599cf59cde2fed048757e5c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Dec 2024 14:34:30 -0800 Subject: [PATCH 055/200] Improve formatting for property initialization --- .../DependencyPropertyGenerator.Execute.cs | 66 +++++++++++++++---- .../Test_DependencyPropertyGenerator.cs | 29 +++++--- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 9555df548..3da03d7d2 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -439,7 +439,10 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { DefaultValue: DependencyPropertyDefaultValue.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } - => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}))", + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName})) + """, { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", @@ -447,27 +450,63 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { IsNet8OrGreater: false } => propertyInfo switch { { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } - => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e)) + """, { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } - => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e))", + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e)) + """, { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } - => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), static (d, e) => {{ (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e); (({typeQualifiedName})d).OnPropertyChanged(e); }})", + => $$""" + global::{{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}}.Create( + createDefaultValueCallback: new {{WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}}({{methodName}}), + propertyChangedCallback: static (d, e) => { (({{typeQualifiedName}})d).On{{propertyInfo.PropertyName}}PropertyChanged(e); (({{typeQualifiedName}})d).OnPropertyChanged(e); }) + """, { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e))", + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: {defaultValue}, + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e)) + """, { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: true } - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e))", + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: {defaultValue}, + propertyChangedCallback: static (d, e) => (({typeQualifiedName})d).OnPropertyChanged(e)) + """, { DefaultValue: { } defaultValue, IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: true } - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, static (d, e) => {{ (({typeQualifiedName})d).On{propertyInfo.PropertyName}PropertyChanged(e); (({typeQualifiedName})d).OnPropertyChanged(e); }})", + => $$""" + new global::{{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}}( + defaultValue: {{defaultValue}}, + propertyChangedCallback: static (d, e) => { (({{typeQualifiedName}})d).On{{propertyInfo.PropertyName}}PropertyChanged(e); (({{typeQualifiedName}})d).OnPropertyChanged(e); }) + """, _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), }, // Codegen for .NET 8 or greater { DefaultValue: DependencyPropertyDefaultValue.Null } - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}(null, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: null, + propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) + """, { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName) } - => $"global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create(new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + => $""" + global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}.Create( + createDefaultValueCallback: new {WellKnownTypeNames.CreateDefaultValueCallback(propertyInfo.UseWindowsUIXaml)}({methodName}), + propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) + """, { DefaultValue: { } defaultValue } and ({ IsPropertyChangedCallbackImplemented: true } or { IsSharedPropertyChangedCallbackImplemented: true }) - => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue}, global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}())", + => $""" + new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}( + defaultValue: {defaultValue}, + propertyChangedCallback: global::{GeneratorName}.PropertyChangedCallbacks.{propertyInfo.PropertyName}()) + """, _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), }; @@ -477,13 +516,16 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) /// """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); - writer.WriteLine($$""" + writer.Write($$""" public static readonly global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}}.Register( name: "{{propertyInfo.PropertyName}}", propertyType: typeof({{propertyInfo.TypeName}}), ownerType: typeof({{typeQualifiedName}}), - typeMetadata: {{typeMetadata}}); + typeMetadata: """, isMultiline: true); + writer.IncreaseIndent(); + writer.WriteLine($"{typeMetadata});", isMultiline: true); + writer.DecreaseIndent(); writer.WriteLine(); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 9dd3b4a05..cfe303d5c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -175,7 +175,9 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -698,7 +700,9 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -969,7 +973,9 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(42, global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: 42, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -1124,7 +1130,9 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -1283,7 +1291,9 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int), global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: default(int), + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.Number())); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -2359,7 +2369,8 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -2481,7 +2492,8 @@ partial class MyControl name: "Number", propertyType: typeof(int?), ownerType: typeof(MyControl), - typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateNumber))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -2605,7 +2617,8 @@ partial class MyControl name: "Name", propertyType: typeof(string), ownerType: typeof(MyControl), - typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create(new Windows.UI.Xaml.CreateDefaultValueCallback(CreateName))); + typeMetadata: global::Windows.UI.Xaml.PropertyMetadata.Create( + createDefaultValueCallback: new Windows.UI.Xaml.CreateDefaultValueCallback(CreateName))); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] From d40bee5fb11498f3e8991318b5db4976943efc73 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Dec 2024 19:58:33 -0800 Subject: [PATCH 056/200] Bug fixes to embedded mode --- .../GeneratedDependencyProperty.cs | 2 +- .../GeneratedDependencyPropertyAttribute.cs | 2 +- ...t.WinUI.DependencyPropertyGenerator.targets | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs index 5364e9c10..e35abebf6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyProperty.cs @@ -6,7 +6,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_MODE +#if GENERATED_DEPENDENCY_PROPERTY_EMBEDDED_MODE namespace CommunityToolkit.WinUI { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs index 61577eea5..0e7c0fafe 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -6,7 +6,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if GENERATED_DEPENDENCY_PROPERTY_PRIVATE_ASSETS_ALL_MODE +#if GENERATED_DEPENDENCY_PROPERTY_ATTRIBUTE_EMBEDDED_MODE namespace CommunityToolkit.WinUI { diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 7806f34d3..933629788 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -7,21 +7,29 @@ false + + + false + false + + - - - $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + + + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_USE_WINDOWS_UI_XAML + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_ATTRIBUTE_EMBEDDED_MODE + $(DefineConstants);GENERATED_DEPENDENCY_PROPERTY_EMBEDDED_MODE + Condition="'$(EnableGeneratedDependencyPropertyAttributeEmbeddedMode)' == 'true' OR '$(EnableGeneratedDependencyPropertyEmbeddedMode)' == 'true'"> @@ -36,7 +44,7 @@ + Text="This project is referencing the '[GeneratedDependencyProperty]' .dll file, but it's also enabling the embedded mode for its public APIs. The embedded mode can only be used when the .dll file is not being referenced. Make sure to use 'PrivateAssets="all"' and 'ExcludeAssets="lib"' in the '<PackageReference>' element for the NuGet package." /> From b48e37ff4e6395df3f63d4f50dad1f2bc915786a Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 9 Dec 2024 11:17:48 -0600 Subject: [PATCH 057/200] Update components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs Co-authored-by: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> --- .../Extensions/ITypeSymbolExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index df8431653..eba208177 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -106,7 +106,7 @@ static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder builder.AddRange(symbol.MetadataName.AsSpan()); break; - // Other namespaces (ie. the one right before global) skip the leading '.' + // Other namespaces (i.e. the one right before global) skip the leading '.' case INamespaceSymbol { IsGlobalNamespace: false }: builder.AddRange(symbol.MetadataName.AsSpan()); break; From a943c7b418cc93805f772361c7e2cf340e1a285a Mon Sep 17 00:00:00 2001 From: Arlo Date: Mon, 9 Dec 2024 11:17:56 -0600 Subject: [PATCH 058/200] Update components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs Co-authored-by: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> --- .../Helpers/CSharpGeneratorTest{TGenerator}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index 03d746354..5390d1858 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -211,7 +211,7 @@ private static void RunGenerator( { Compilation originalCompilation = CreateCompilation(source, languageVersion); - // Create the generator driver with the D2D shader generator + // Create the generator driver with the specified generator GeneratorDriver driver = CSharpGeneratorDriver.Create(new TGenerator()).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); // Run all source generators on the input source code From 42d3211e414abad1d11511754c1355f0eff92236 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 09:59:26 -0800 Subject: [PATCH 059/200] Fix typos --- .../Extensions/CompilationExtensions.cs | 2 +- .../Helpers/IndentedTextWriter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs index ff06af81c..f3c256381 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/CompilationExtensions.cs @@ -34,7 +34,7 @@ public static bool IsLanguageVersionPreview(this Compilation compilation) } /// - /// Gets whether the current target is a WinRT application (ie. legacy UWP). + /// Gets whether the current target is a WinRT application (i.e. legacy UWP). /// /// The input instance to inspect. /// Whether the current target is a WinRT application. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs index 6d88ecb97..0883e84d1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs @@ -97,7 +97,7 @@ public void IncreaseIndent() } // Set both the current indentation and the current position in the indentations - // array to the expected indentation for the incremented level (ie. one level more). + // array to the expected indentation for the incremented level (i.e. one level more). this.currentIndentation = this.availableIndentations[this.currentIndentationLevel] ??= this.availableIndentations[this.currentIndentationLevel - 1] + DefaultIndentation; } From e481f480cc68bd29cbce561983582a5f37b2157b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 18:12:05 -0800 Subject: [PATCH 060/200] Add draft 'UseGeneratedDependencyPropertyOnManualPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...endencyPropertyOnManualPropertyAnalyzer.cs | 525 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 18 + .../Test_Analyzers.cs | 203 +++++++ 4 files changed, 747 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 34bb94f7d..e547e0ee3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -23,3 +23,4 @@ WCTDP0013 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0014 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0015 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0016 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | +WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs new file mode 100644 index 000000000..316edea2f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -0,0 +1,525 @@ +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a suggestion whenever [GeneratedDependencytProperty] should be used over a manual property. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + /// The number of pooled flags per stack (ie. how many properties we expect on average per type). + /// + private const int NumberOfPooledFlagsPerStack = 20; + + /// + /// Shared pool for instances for properties. + /// + [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")] + private static readonly ObjectPool> PropertyMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default)); + + /// + /// Shared pool for instances for fields, for dependency properties. + /// + [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")] + private static readonly ObjectPool> FieldMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default)); + + /// + /// Shared pool for -s of property flags, one per type being processed. + /// + private static readonly ObjectPool> PropertyFlagsStackPool = new(CreateFlagsStack); + + /// + /// Shared pool for -s of field flags, one per type being processed. + /// + private static readonly ObjectPool> FieldFlagsStackPool = new(CreateFlagsStack); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the 'DependencyObject' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml)) is not { } dependencyObjectSymbol) + { + return; + } + + // Get the symbol for the 'GetValue' method as well + if (dependencyObjectSymbol.GetMembers("GetValue") is not [IMethodSymbol { Parameters: [_], ReturnType.SpecialType: SpecialType.System_Object } getValueSymbol]) + { + return; + } + + // Get the symbol for the 'SetValue' method as well + if (dependencyObjectSymbol.GetMembers("SetValue") is not [IMethodSymbol { Parameters: [_, _], ReturnsVoid: true } setValueSymbol]) + { + return; + } + + // We also need the 'DependencyProperty' and 'PropertyMetadata' symbols + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol || + context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.PropertyMetadata(useWindowsUIXaml)) is not { } propertyMetadataSymbol) + { + return; + } + + // Next, we need to get the 'DependencyProperty.Register' symbol as well, to validate initializers. + // No need to validate this more, as there's only a single overload defined on this type. + if (dependencyPropertySymbol.GetMembers("Register") is not [IMethodSymbol dependencyPropertyRegisterSymbol]) + { + return; + } + + context.RegisterSymbolStartAction(context => + { + // We only care about types that could derive from 'DependencyObject' + if (context.Symbol is not INamedTypeSymbol { IsStatic: false, IsReferenceType: true, BaseType.SpecialType: not SpecialType.System_Object } typeSymbol) + { + return; + } + + // If the type does not derive from 'DependencyObject', ignore it + if (!typeSymbol.InheritsFromType(dependencyObjectSymbol)) + { + return; + } + + Dictionary propertyMap = PropertyMapPool.Allocate(); + Dictionary fieldMap = FieldMapPool.Allocate(); + Stack propertyFlagsStack = PropertyFlagsStackPool.Allocate(); + Stack fieldFlagsStack = FieldFlagsStackPool.Allocate(); + + // Crawl all members to discover properties that might be of interest + foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) + { + // First, look for properties that might be valid candidates for conversion + if (memberSymbol is IPropertySymbol + { + IsStatic: false, + IsPartialDefinition: false, + PartialDefinitionPart: null, + PartialImplementationPart: null, + ReturnsByRef: false, + ReturnsByRefReadonly: false, + Type.IsRefLikeType: false, + GetMethod: not null, + SetMethod.IsInitOnly: false + } propertySymbol) + { + // We can safely ignore properties that already have '[GeneratedDependencyProperty]' + if (propertySymbol.HasAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols)) + { + continue; + } + + // Take a new flags object from the stack or create a new one otherwise + PropertyFlags flags = propertyFlagsStack.Count > 0 + ? propertyFlagsStack.Pop() + : new(); + + // Track the property for later + propertyMap.Add(propertySymbol, flags); + } + else if (memberSymbol is IFieldSymbol + { + DeclaredAccessibility: Accessibility.Public, + IsStatic: true, + IsReadOnly: true, + IsFixedSizeBuffer: false, + IsRequired: false, + Type.IsReferenceType: true, + IsVolatile: false + } fieldSymbol) + { + // We only care about fields that are 'DependencyProperty' + if (!SymbolEqualityComparer.Default.Equals(dependencyPropertySymbol, fieldSymbol.Type)) + { + continue; + } + + // Same as above for the field flags + fieldMap.Add( + key: fieldSymbol, + value: fieldFlagsStack.Count > 0 ? fieldFlagsStack.Pop() : new FieldFlags()); + } + } + + // We want to process both accessors, where we specifically need both the syntax + // and their semantic model to verify what they're doing. We can use a code callback. + context.RegisterOperationBlockAction(context => + { + // Handle a 'get' accessor (for any property) + void HandleGetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFlags) + { + // We expect a top-level block operation, that immediately returns an expression + if (context.OperationBlocks is not [IBlockOperation { Operations: [IReturnOperation returnOperation] }]) + { + return; + } + + // Next, check whether we have an invocation operation. This is the case when the getter is just + // calling 'GetValue' and returning it directly, which only works when the property type allows + // direct conversion. Generally speaking this happens when properties are just of type 'object'. + if (returnOperation is not { ReturnedValue: IInvocationOperation invocationOperation }) + { + // Alternatively, we expect a conversion (a cast) + if (returnOperation is not { ReturnedValue: IConversionOperation { IsTryCast: false } conversionOperation }) + { + return; + } + + // Check the conversion is actually valid + if (!SymbolEqualityComparer.Default.Equals(propertySymbol.Type, conversionOperation.Type)) + { + return; + } + + // Try to extract the invocation from the conversion + if (conversionOperation.Operand is not IInvocationOperation operandInvocationOperation) + { + return; + } + + invocationOperation = operandInvocationOperation; + } + + // Now that we have the invocation, first filter the target method + if (invocationOperation.TargetMethod is not { Name: "GetValue", IsGenericMethod: false, IsStatic: false } methodSymbol) + { + return; + } + + // Next, make sure we're actually calling 'DependencyObject.GetValue' + if (!SymbolEqualityComparer.Default.Equals(methodSymbol, getValueSymbol)) + { + return; + } + + // Make sure we have one argument, which will be the dependency property + if (invocationOperation.Arguments is not [{ } dependencyPropertyArgument]) + { + return; + } + + // This argument should be a field reference (we'll fully validate it later) + if (dependencyPropertyArgument.Value is not IFieldReferenceOperation { Field: { } fieldSymbol }) + { + return; + } + + // The field must follow the expected naming pattern. We can check this just here in the getter. + // If this is valid, the whole property will be skipped anyway, so no need to do it twice. + if (fieldSymbol.Name != $"{propertySymbol.Name}Property") + { + return; + } + + // We can in the meantime at least verify that we do have the field in the set + if (!fieldMap.ContainsKey(fieldSymbol)) + { + return; + } + + // If the property is also valid, then the accessor is valid + propertyFlags.GetValueTargetField = fieldSymbol; + } + + // Handle a 'set' accessor (for any property) + void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFlags) + { + // We expect a top-level block operation, that immediately performs an invocation + if (context.OperationBlocks is not [IBlockOperation { Operations: [IExpressionStatementOperation { Operation: IInvocationOperation invocationOperation }] }]) + { + return; + } + + // Brief filtering of the target method + if (invocationOperation.TargetMethod is not { Name: "SetValue", IsGenericMethod: false, IsStatic: false } methodSymbol) + { + return; + } + + // First, check that we're calling 'DependencyObject.SetValue' + if (!SymbolEqualityComparer.Default.Equals(methodSymbol, setValueSymbol)) + { + return; + } + + // We matched the method, now let's validate the arguments + if (invocationOperation.Arguments is not [{ } dependencyPropertyArgument, { } valueArgument]) + { + return; + } + + // Like for the getter, the first argument should be a field reference... + if (dependencyPropertyArgument.Value is not IFieldReferenceOperation { Field: { } fieldSymbol }) + { + return; + } + + // ...and the field should be in the set (not fully guaranteed to be valid yet, but partially) + if (!fieldMap.ContainsKey(fieldSymbol)) + { + return; + } + + // The value is just the 'value' keyword + if (valueArgument.Value is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } }) + { + // If this is not the case, check whether the parameter reference was wrapped in a conversion (boxing) + if (valueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) + { + return; + } + + // Check for 'value' again + if (conversionOperation.Operand is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } }) + { + return; + } + } + + // The 'set' accessor is valid if the field is valid, like above + propertyFlags.SetValueTargetField = fieldSymbol; + } + + // Only look for method symbols, for property accessors + if (context.OwningSymbol is not IMethodSymbol { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet, AssociatedSymbol: IPropertySymbol propertySymbol }) + { + return; + } + + // If so, check that we are actually processing one of the properties we care about + if (!propertyMap.TryGetValue(propertySymbol, out PropertyFlags? propertyFlags)) + { + return; + } + + // Handle the 'get' and 'set' logic + if (SymbolEqualityComparer.Default.Equals(propertySymbol.GetMethod, context.OwningSymbol)) + { + HandleGetAccessor(propertySymbol, propertyFlags); + } + else if (SymbolEqualityComparer.Default.Equals(propertySymbol.SetMethod, context.OwningSymbol)) + { + HandleSetAccessor(propertySymbol, propertyFlags); + } + }); + + // Same as above, but targeting field initializers (we can't just inspect field symbols) + context.RegisterOperationAction(context => + { + // Only look for field symbols, which we should always get here, and an invocation in the initializer block (the 'DependencyProperty.Register' call) + if (context.Operation is not IFieldInitializerOperation { InitializedFields: [{ } fieldSymbol], Value: IInvocationOperation invocationOperation }) + { + return; + } + + // Check that the field is one of the ones we expect to encounter + if (!fieldMap.TryGetValue(fieldSymbol, out FieldFlags? fieldFlags)) + { + return; + } + + // Validate that we are calling 'DependencyProperty.Register' + if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, dependencyPropertyRegisterSymbol)) + { + return; + } + + // Next, make sure we have the arguments we expect + if (invocationOperation.Arguments is not [{ } nameArgument, { } propertyTypeArgument, { } ownerTypeArgument, { } propertyMetadataArgument]) + { + return; + } + + // We cannot validate the property name from here yet, but let's check it's a constant, and save it for later + if (nameArgument.Value.ConstantValue is not { HasValue: true, Value: string propertyName }) + { + return; + } + + // Extract the property type, we can validate it later + if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) + { + return; + } + + // Extract the owning type, we can validate it right now + if (ownerTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } owningTypeSymbol }) + { + return; + } + + // The owning type always has to be exactly the same as the containing type + if (!SymbolEqualityComparer.Default.Equals(owningTypeSymbol, typeSymbol)) + { + return; + } + + // For now, just check that the metadata is 'null' + if (propertyMetadataArgument.Value.ConstantValue is not { HasValue: true, Value: null }) + { + return; + } + + fieldFlags.PropertyName = propertyName; + fieldFlags.PropertyType = propertyTypeSymbol; + fieldFlags.IsInitializerValid = true; + }, OperationKind.FieldInitializer); + + // Finally, we can consume this information when we finish processing the symbol + context.RegisterSymbolEndAction(context => + { + // Emit a diagnostic for each property that was a valid match + foreach (KeyValuePair pair in propertyMap) + { + // Make sure we have target fields for each accessor. This also implies the accessors themselves are valid. + if (pair.Value is not { GetValueTargetField: { } getValueFieldSymbol, SetValueTargetField: { } setValueFieldSymbol }) + { + continue; + } + + // The two fields must of course be the same + if (!SymbolEqualityComparer.Default.Equals(getValueFieldSymbol, setValueFieldSymbol)) + { + continue; + } + + // Next, check that the field are present in the mapping (otherwise for sure they're not valid) + if (!fieldMap.TryGetValue(getValueFieldSymbol, out FieldFlags? fieldFlags)) + { + continue; + } + + // Validate the combination of accessors and target field, and warn if that's the case + if (fieldFlags.PropertyName == pair.Key.Name && + SymbolEqualityComparer.Default.Equals(fieldFlags.PropertyType, pair.Key.Type) && + fieldFlags.IsInitializerValid) + { + context.ReportDiagnostic(Diagnostic.Create( + UseGeneratedDependencyPropertyForManualProperty, + pair.Key.Locations.FirstOrDefault(), + pair.Key)); + } + } + + // Before clearing the dictionary, move back all values to the stack + foreach (PropertyFlags propertyFlags in propertyMap.Values) + { + // Make sure the flags is cleared before returning it + propertyFlags.GetValueTargetField = null; + propertyFlags.SetValueTargetField = null; + + propertyFlagsStack.Push(propertyFlags); + } + + // Same for the field flags + foreach (FieldFlags fieldFlags in fieldMap.Values) + { + fieldFlags.PropertyName = null; + fieldFlags.PropertyType = null; + fieldFlags.IsInitializerValid = false; + + fieldFlagsStack.Push(fieldFlags); + } + + // We are now done processing the symbol, we can return the dictionary. + // Note that we must clear it before doing so to avoid leaks and issues. + propertyMap.Clear(); + + PropertyMapPool.Free(propertyMap); + + // Also do the same for the stacks, except we don't need to clean them (since it roots no compilation objects) + PropertyFlagsStackPool.Free(propertyFlagsStack); + FieldFlagsStackPool.Free(fieldFlagsStack); + }); + }, SymbolKind.NamedType); + }); + } + + /// + /// Produces a new instance to pool. + /// + /// The type of flags objects to create. + /// The resulting instance to use. + private static Stack CreateFlagsStack() + where T : class, new() + { + static IEnumerable EnumerateFlags() + { + for (int i = 0; i < NumberOfPooledFlagsPerStack; i++) + { + yield return new(); + } + } + + return new(EnumerateFlags()); + } + + /// + /// Flags to track properties to warn on. + /// + private sealed class PropertyFlags + { + /// + /// The target field for the GetValue method. + /// + public IFieldSymbol? GetValueTargetField; + + /// + /// The target field for the SetValue method. + /// + public IFieldSymbol? SetValueTargetField; + } + + /// + /// Flags to track fields. + /// + private sealed class FieldFlags + { + /// + /// The name of the property. + /// + public string? PropertyName; + + /// + /// The type of the property (as in, of values that can be assigned to it). + /// + public ITypeSymbol? PropertyType; + + /// + /// Whether the initializer is valid. + /// + public bool IsInitializerValid; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index e254b1a25..61138c936 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -11,6 +11,11 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Diagnostics; /// internal static class DiagnosticDescriptors { + /// + /// The diagnostic id for . + /// + public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDP0017"; + /// /// "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)". /// @@ -218,4 +223,17 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] should not have the 'Property' suffix in their name, as it is redundant (the generated dependency properties will always add the 'Property' suffix to the name of their associated properties).", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The manual property '{0}' can be converted to a partial property using [GeneratedDependencyProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)". + /// + public static readonly DiagnosticDescriptor UseGeneratedDependencyPropertyForManualProperty = new( + id: UseGeneratedDependencyPropertyForManualPropertyId, + title: "Prefer using [GeneratedDependencyProperty] over manual properties", + messageFormat: """The manual property '{0}' can be converted to a partial property using [GeneratedDependencyProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)""", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Manual properties should be converted to partial properties using [GeneratedDependencyProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 2317a547b..726bacd3f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1220,4 +1220,207 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldNotInitialized_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty; + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldWithDifferentName_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty OtherNameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(OtherNameProperty); + set => SetValue(OtherNameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("null", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"NameProperty\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(int)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(MyControl)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(object)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(42)")] + [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: {{name}}, + propertyType: {{propertyType}}, + ownerType: {{ownerType}}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingGetter_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingSetter_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "string")] + [DataRow("string", "string?")] + [DataRow("object", "object")] + [DataRow("object", "object?")] + [DataRow("int", "int")] + [DataRow("int?", "int?")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns( + string dependencyPropertyType, + string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} {|WCTDP0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From ef70aab7dd703e1c5ea192fa0627cda954f9299e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:25:06 -0800 Subject: [PATCH 061/200] Pass the location of the target field --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 316edea2f..3a6dbe99e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -392,9 +392,15 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } + // Find the parent field for the operation (we're guaranteed to only fine one) + if (context.Operation.Syntax.FirstAncestor()?.GetLocation() is not Location fieldLocation) + { + return; + } + fieldFlags.PropertyName = propertyName; fieldFlags.PropertyType = propertyTypeSymbol; - fieldFlags.IsInitializerValid = true; + fieldFlags.FieldLocation = fieldLocation; }, OperationKind.FieldInitializer); // Finally, we can consume this information when we finish processing the symbol @@ -421,14 +427,27 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla continue; } - // Validate the combination of accessors and target field, and warn if that's the case - if (fieldFlags.PropertyName == pair.Key.Name && - SymbolEqualityComparer.Default.Equals(fieldFlags.PropertyType, pair.Key.Type) && - fieldFlags.IsInitializerValid) + // We only support rewriting when the property name matches the field being initialized. + // Note that the property name here is the literal being passed for the 'name' parameter. + if (fieldFlags.PropertyName != pair.Key.Name) + { + continue; + } + + // Make sure that the 'propertyType' value matches the actual type of the property. + // We are intentionally not handling combinations of nullable value types here. + if (!SymbolEqualityComparer.Default.Equals(fieldFlags.PropertyType, pair.Key.Type)) + { + return; + } + + // Finally, check whether the field was valid (if so, we will have a valid location) + if (fieldFlags.FieldLocation is Location fieldLocation) { context.ReportDiagnostic(Diagnostic.Create( UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), + [fieldLocation], pair.Key)); } } @@ -448,7 +467,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla { fieldFlags.PropertyName = null; fieldFlags.PropertyType = null; - fieldFlags.IsInitializerValid = false; + fieldFlags.FieldLocation = null; fieldFlagsStack.Push(fieldFlags); } @@ -518,8 +537,8 @@ private sealed class FieldFlags public ITypeSymbol? PropertyType; /// - /// Whether the initializer is valid. + /// The location of the target field being initialized. /// - public bool IsInitializerValid; + public Location? FieldLocation; } } From 08707e6a08b00a6d6aef143393cbb2a417991c32 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:30:27 -0800 Subject: [PATCH 062/200] Add empty 'CodeFixers' project --- ...endencyPropertyGenerator.CodeFixers.csproj | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj new file mode 100644 index 000000000..76b6c0f7c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/CommunityToolkit.DependencyPropertyGenerator.CodeFixers.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + enable + true + true + + + $(NoWarn);IDE0130 + + + + + + + + + + + From 2389d335130fc1839e22fe54130f537e93157c0c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:30:36 -0800 Subject: [PATCH 063/200] Bump 'Microsoft.CodeAnalysis.CSharp' to latest --- ...yToolkit.DependencyPropertyGenerator.SourceGenerators.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index 6b470e010..76d0c208f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -10,7 +10,7 @@ - + From 0281e3662916fa96b17093e0fcb7c936be52f876 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:34:47 -0800 Subject: [PATCH 064/200] Pack code fixers into NuGet package --- ...ommunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index 18a5c1315..9f3c18d45 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -25,9 +25,10 @@ - + + @@ -39,7 +40,8 @@ - + + From a032d85220d1793f46c2498dbc260c61a503bc53 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:34:57 -0800 Subject: [PATCH 065/200] Add 'InternalsVisibleTo' for code fixers --- ...olkit.DependencyPropertyGenerator.SourceGenerators.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj index 76d0c208f..994ec7f6f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators.csproj @@ -9,6 +9,11 @@ $(NoWarn);IDE0130 + + + + + From 85240215b1f94a7123b5d0e9566c72757bf3a90d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:49:42 -0800 Subject: [PATCH 066/200] Add draft 'UseGeneratedDependencyPropertyOnManualPropertyCodeFixer' --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs new file mode 100644 index 000000000..3b1ca0133 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -0,0 +1,277 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.CodeFixers; + +/// +/// A code fixer that converts manual properties into partial properties using [GeneratedDependencytProperty]. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UseGeneratedDependencyPropertyOnManualPropertyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = [UseGeneratedDependencyPropertyForManualPropertyId]; + + /// + public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider() + { + return new FixAllProvider(); + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + // We can only possibly fix diagnostics with an additional location + if (diagnostic.AdditionalLocations is not [{ } fieldLocation]) + { + return; + } + + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the property declaration and the field declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration && + root.FindNode(fieldLocation.SourceSpan) is FieldDeclarationSyntax fieldDeclaration) + { + // Get the semantic model, as we need to resolve symbols + SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!; + + // Register the code fix to update the semi-auto property to a partial property + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, semanticModel, root, propertyDeclaration, fieldDeclaration), + equivalenceKey: "Use a partial property"), + diagnostic); + } + } + + /// + /// Tries to get an for the [GeneratedDependencyProperty] attribute. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The resulting attribute list, if successfully retrieved. + /// Whether could be retrieved successfully. + private static bool TryGetGeneratedObservablePropertyAttributeList( + Document document, + SemanticModel semanticModel, + [NotNullWhen(true)] out AttributeListSyntax? observablePropertyAttributeList) + { + // Make sure we can resolve the '[GeneratedDependencyProperty]' attribute + if (semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute) is not INamedTypeSymbol attributeSymbol) + { + observablePropertyAttributeList = null; + + return false; + } + + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute syntax for the new '[GeneratedDependencyProperty]' attribute here too + SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(attributeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + + observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + + return true; + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The original tree root belonging to the current document. + /// The for the property being updated. + /// The for the declared property to remove. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static async Task ConvertToPartialProperty( + Document document, + SemanticModel semanticModel, + SyntaxNode root, + PropertyDeclarationSyntax propertyDeclaration, + FieldDeclarationSyntax fieldDeclaration) + { + await Task.CompletedTask; + + // If we can't generate the new attribute list, bail (this should never happen) + if (!TryGetGeneratedObservablePropertyAttributeList(document, semanticModel, out AttributeListSyntax? observablePropertyAttributeList)) + { + return document; + } + + // Create an editor to perform all mutations + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + ConvertToPartialProperty( + propertyDeclaration, + fieldDeclaration, + observablePropertyAttributeList, + syntaxEditor); + + // Create the new document with the single change + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The for the property being updated. + /// The for the declared property to remove. + /// The with the attribute to add. + /// The instance to use. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static void ConvertToPartialProperty( + PropertyDeclarationSyntax propertyDeclaration, + FieldDeclarationSyntax fieldDeclaration, + AttributeListSyntax observablePropertyAttributeList, + SyntaxEditor syntaxEditor) + { + // Start setting up the updated attribute lists + SyntaxList attributeLists = propertyDeclaration.AttributeLists; + + if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..]) + { + // Remove the trivia from the original first attribute + attributeLists = attributeLists.Replace( + nodeInList: firstAttributeListSyntax, + newNode: firstAttributeListSyntax.WithoutTrivia()); + + // If the property has at least an attribute list, move the trivia from it to the new attribute + observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); + + // Insert the new attribute + attributeLists = attributeLists.Insert(0, observablePropertyAttributeList); + } + else + { + // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list + observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration); + + // Save the new attribute list + attributeLists = attributeLists.Add(observablePropertyAttributeList); + } + + // Get a new property that is partial and with semicolon token accessors + PropertyDeclarationSyntax updatedPropertyDeclaration = + propertyDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .WithoutLeadingTrivia() + .WithAttributeLists(attributeLists) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithAccessorList(AccessorList(List( + [ + // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only + propertyDeclaration.AccessorList!.Accessors[0] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithAdditionalAnnotations(Formatter.Annotation), + propertyDeclaration.AccessorList!.Accessors[1] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); + + syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + + // Also remove the field declaration (it'll be generated now) + syntaxEditor.RemoveNode(fieldDeclaration); + + // Find the parent type for the property + TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; + + // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration). + // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + } + } + + /// + /// A custom with the logic from . + /// + private sealed class FixAllProvider : DocumentBasedFixAllProvider + { + /// + protected override async Task FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + // Get the semantic model, as we need to resolve symbols + if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel) + { + return document; + } + + // Get the document root (this should always succeed) + if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root) + { + return document; + } + + // If we can't generate the new attribute list, bail (this should never happen) + if (!TryGetGeneratedObservablePropertyAttributeList(document, semanticModel, out AttributeListSyntax? observablePropertyAttributeList)) + { + return document; + } + + // Create an editor to perform all mutations (across all edits in the file) + SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services); + + foreach (Diagnostic diagnostic in diagnostics) + { + // Get the current property declaration for the diagnostic + if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration) + { + continue; + } + + // Also check that we can find the target field to remove + if (diagnostic.AdditionalLocations is not [{ } fieldLocation] || + root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) + { + continue; + } + + ConvertToPartialProperty( + propertyDeclaration, + fieldDeclaration, + observablePropertyAttributeList, + syntaxEditor); + } + + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + } +} From df50c2f325d3cd71fda71ba05734c1d4696357a7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 19:55:06 -0800 Subject: [PATCH 067/200] Add 'CSharpCodeFixTest<,>' type --- ...t.DependencyPropertyGenerator.Tests.csproj | 3 +- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 2 +- ...harpCodeFixerTest{TAnalyzer,TCodeFixer}.cs | 42 +++++++++++++++++++ .../Test_Analyzers.cs | 2 +- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index 7828f4b89..5b4fbb504 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -19,7 +19,8 @@ - + + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index 10478a2bc..c85c520a1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -13,7 +13,7 @@ using Windows.UI.Xaml; using CommunityToolkit.WinUI; -namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; /// /// A custom that uses a specific C# language version to parse code. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs new file mode 100644 index 000000000..6bbc2a8f0 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs @@ -0,0 +1,42 @@ +// 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.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to produce diagnostics. +/// The type of code fix to test. +internal sealed class CSharpCodeFixTest : CSharpCodeFixTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFixer : CodeFixProvider, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + public CSharpCodeFixTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 726bacd3f..d8305a64a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; -using CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; using Microsoft.CodeAnalysis.CSharp; using Microsoft.VisualStudio.TestTools.UnitTesting; From 7f57e6c78f919143e17800e8e9e8ecef517ccfb8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 20:20:05 -0800 Subject: [PATCH 068/200] Add basic code fixer test --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 2 +- ...t.DependencyPropertyGenerator.Tests.csproj | 1 + ...ndencyPropertyOnManualPropertyCodeFixer.cs | 86 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 3b1ca0133..d9f15e63a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -20,7 +20,7 @@ using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -namespace CommunityToolkit.Mvvm.CodeFixers; +namespace CommunityToolkit.GeneratedDependencyProperty; /// /// A code fixer that converts manual properties into partial properties using [GeneratedDependencytProperty]. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index 5b4fbb504..ea88d28b9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs new file mode 100644 index 000000000..9e6d81b9f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -0,0 +1,86 @@ +// 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.Threading.Tasks; +using CommunityToolkit.WinUI; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< + CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyCodeFixer>; +using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer +{ + [TestMethod] + [DataRow("string", "string")] + [DataRow("string", "string?")] + [DataRow("object", "object")] + [DataRow("object", "object?")] + [DataRow("int", "int")] + [DataRow("int?", "int?")] + public async Task SimpleProperty(string underlyingType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof({{underlyingType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = { AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + }} + }; + + await test.RunAsync(); + } +} From 866492bce5a274b88cf4dbaf5e8f1727ef8064f9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 21:19:05 -0800 Subject: [PATCH 069/200] Optimize registration for default values --- .../DependencyPropertyGenerator.Execute.cs | 2 +- .../Test_DependencyPropertyGenerator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 3da03d7d2..4c5c82c5b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -436,7 +436,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) string typeMetadata = propertyInfo switch { // Shared codegen - { DefaultValue: DependencyPropertyDefaultValue.Null, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + { DefaultValue: DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $""" diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index cfe303d5c..940ea53f7 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -45,7 +45,7 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int))); + typeMetadata: null); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] @@ -464,7 +464,7 @@ partial class MyControl name: "Number", propertyType: typeof(int), ownerType: typeof(MyControl), - typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(int))); + typeMetadata: null); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] From 26fdeed582a9b57004689c31ea2002876d3dd6ef Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 22:21:00 -0800 Subject: [PATCH 070/200] Handle more default properties in analyzer --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 55 +++++++++++++++++- .../Extensions/IOperationExtensions.cs | 56 +++++++++++++++++++ .../Models/TypedConstantInfo.Factory.cs | 48 ++++++++++++++++ .../Test_Analyzers.cs | 52 ++++++++++++++++- 4 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 3a6dbe99e..5d4dfbf58 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -9,6 +9,7 @@ using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using CommunityToolkit.GeneratedDependencyProperty.Models; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -50,6 +51,11 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia /// private static readonly ObjectPool> FieldFlagsStackPool = new(CreateFlagsStack); + /// + /// The property name for the serialized property value, if present. + /// + public const string DefaultValuePropertyName = "DefaultValue"; + /// public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; @@ -386,10 +392,48 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - // For now, just check that the metadata is 'null' + // First, check if the metadata is 'null' (simplest case) if (propertyMetadataArgument.Value.ConstantValue is not { HasValue: true, Value: null }) { - return; + // Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type + if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument] } objectCreationOperation) + { + return; + } + + // Make sure the object being created is actually 'PropertyMetadata' + if (!SymbolEqualityComparer.Default.Equals(objectCreationOperation.Type, propertyMetadataSymbol)) + { + return; + } + + // The argument should be a conversion operation (boxing) + if (defaultValueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) + { + return; + } + + // Check whether the value is a default constant value. + // If it is, then the property is valid (no explicit value). + if (!conversionOperation.Operand.IsConstantValueDefault()) + { + // If that is not the case, check if it's some constant value we can forward + if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) + { + // As a last resort, check if this is explicitly a 'default(T)' expression + if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) + { + return; + } + + // Also make sure the type matches the property type (it's not technically guaranteed). + // If this succeeds, we can safely convert the property, the generated code will be fine. + if (!SymbolEqualityComparer.Default.Equals(defaultValueExpressionType, propertyTypeSymbol)) + { + return; + } + } + } } // Find the parent field for the operation (we're guaranteed to only fine one) @@ -448,6 +492,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), [fieldLocation], + ImmutableDictionary.Create().Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()), pair.Key)); } } @@ -467,6 +512,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla { fieldFlags.PropertyName = null; fieldFlags.PropertyType = null; + fieldFlags.DefaultValue = null; fieldFlags.FieldLocation = null; fieldFlagsStack.Push(fieldFlags); @@ -536,6 +582,11 @@ private sealed class FieldFlags /// public ITypeSymbol? PropertyType; + /// + /// The default value to use (not present if it does not need to be set explicitly). + /// + public TypedConstantInfo? DefaultValue; + /// /// The location of the target field being initialized. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs new file mode 100644 index 000000000..a6d7ba672 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs @@ -0,0 +1,56 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class IOperationExtensions +{ + /// + /// Checks whether a given operation represents a default constant value. + /// + /// The input instance. + /// Whether represents a default constant value. + public static bool IsConstantValueDefault(this IOperation operation) + { + if (operation is not { Type: not null, ConstantValue.HasValue: true }) + { + return false; + } + + // Easy check for reference types + if (operation is { Type.IsReferenceType: true, ConstantValue.Value: null }) + { + return true; + } + + // Equivalent check for nullable value types too + if (operation is { Type.SpecialType: SpecialType.System_Nullable_T, ConstantValue.Value: null }) + { + return true; + } + + // Manually match for known primitive types + return (operation.Type.SpecialType, operation.ConstantValue.Value) switch + { + (SpecialType.System_Byte, default(byte)) or + (SpecialType.System_Char, default(char)) or + (SpecialType.System_Int16, default(short)) or + (SpecialType.System_UInt16, default(ushort)) or + (SpecialType.System_Int32, default(int)) or + (SpecialType.System_UInt32, default(uint)) or + (SpecialType.System_Int64, default(long)) or + (SpecialType.System_UInt64, default(ulong)) or + (SpecialType.System_Boolean, default(bool)) => true, + (SpecialType.System_Single, float x) when BitConverter.DoubleToInt64Bits(x) == 0 => true, + (SpecialType.System_Double, double x) when BitConverter.DoubleToInt64Bits(x) == 0 => true, + _ => false + }; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs index bb030884c..e98111cde 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; @@ -57,4 +58,51 @@ public static TypedConstantInfo Create(TypedConstant arg) _ => throw new ArgumentException("Invalid typed constant type"), }; } + + /// + /// Creates a new instance from a given value. + /// + /// The input value. + /// A instance representing . + /// Thrown if the input argument is not valid. + public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out TypedConstantInfo? result) + { + // Validate that we do have some constant value + if (operation is not { Type: { } operationType, ConstantValue.HasValue: true }) + { + result = null; + + return false; + } + + if (operation.ConstantValue.Value is null) + { + result = new Null(); + + return true; + } + + // Handle all known possible constant values + result = (operationType, operation.ConstantValue.Value) switch + { + ({ SpecialType: SpecialType.System_String }, string text) => new Primitive.String(text), + ({ SpecialType: SpecialType.System_Boolean}, bool flag) => new Primitive.Boolean(flag), + (_, byte b) => new Primitive.Of(b), + (_, char c) => new Primitive.Of(c), + (_, double d) => new Primitive.Of(d), + (_, float f) => new Primitive.Of(f), + (_, int i) => new Primitive.Of(i), + (_, long l) => new Primitive.Of(l), + (_, sbyte sb) => new Primitive.Of(sb), + (_, short sh) => new Primitive.Of(sh), + (_, uint ui) => new Primitive.Of(ui), + (_, ulong ul) => new Primitive.Of(ul), + (_, ushort ush) => new Primitive.Of(ush), + (_, ITypeSymbol type) => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + (INamedTypeSymbol { TypeKind: TypeKind.Enum}, object value) => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), + _ => throw new ArgumentException("Invalid typed constant type"), + }; + + return true; + } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index d8305a64a..e7b2e7f27 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1289,7 +1289,6 @@ public string? Name [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] - [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(42)")] [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_DoesNotWarn( string name, @@ -1423,4 +1422,55 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + [DataRow("string", "string", "null")] + [DataRow("string", "string", "default(string)")] + [DataRow("string", "string", "(string)null")] + [DataRow("string", "string", "\"\"")] + [DataRow("string", "string", "\"Hello\"")] + [DataRow("string", "string?", "null")] + [DataRow("object", "object", "null")] + [DataRow("object", "object?", "null")] + [DataRow("int", "int", "0")] + [DataRow("int", "int", "42")] + [DataRow("int", "int", "default(int)")] + [DataRow("int?", "int?", "null")] + [DataRow("int?", "int?", "0")] + [DataRow("int?", "int?", "42")] + [DataRow("int?", "int?", "default(int?)")] + [DataRow("int?", "int?", "null")] + [DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_Warns( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} {|WCTDP0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From bebdcacc211fbab45bce3dabfdcad277a770ea83 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 23:14:31 -0800 Subject: [PATCH 071/200] Handle even more default properties in analyzer --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 41 ++++- ...endencyPropertyOnManualPropertyAnalyzer.cs | 24 ++- .../Test_Analyzers.cs | 7 - ...ndencyPropertyOnManualPropertyCodeFixer.cs | 145 +++++++++++++++++- 4 files changed, 197 insertions(+), 20 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index d9f15e63a..c7dbf7ec7 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -56,6 +56,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return; } + // Retrieve the properties passed by the analyzer + string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); // Get the property declaration and the field declaration from the target diagnostic @@ -69,7 +72,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( title: "Use a partial property", - createChangedDocument: token => ConvertToPartialProperty(context.Document, semanticModel, root, propertyDeclaration, fieldDeclaration), + createChangedDocument: token => ConvertToPartialProperty( + context.Document, + semanticModel, + root, + propertyDeclaration, + fieldDeclaration, + defaultValue), equivalenceKey: "Use a partial property"), diagnostic); } @@ -113,13 +122,15 @@ private static bool TryGetGeneratedObservablePropertyAttributeList( /// The original tree root belonging to the current document. /// The for the property being updated. /// The for the declared property to remove. + /// The expression for the default value of the property, if present /// An updated document with the applied code fix, and being replaced with a partial property. private static async Task ConvertToPartialProperty( Document document, SemanticModel semanticModel, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration, - FieldDeclarationSyntax fieldDeclaration) + FieldDeclarationSyntax fieldDeclaration, + string? defaultValueExpression) { await Task.CompletedTask; @@ -136,7 +147,8 @@ private static async Task ConvertToPartialProperty( propertyDeclaration, fieldDeclaration, observablePropertyAttributeList, - syntaxEditor); + syntaxEditor, + defaultValueExpression); // Create the new document with the single change return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); @@ -149,13 +161,28 @@ private static async Task ConvertToPartialProperty( /// The for the declared property to remove. /// The with the attribute to add. /// The instance to use. + /// The expression for the default value of the property, if present /// An updated document with the applied code fix, and being replaced with a partial property. private static void ConvertToPartialProperty( PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, AttributeListSyntax observablePropertyAttributeList, - SyntaxEditor syntaxEditor) + SyntaxEditor syntaxEditor, + string? defaultValueExpression) { + // If we do have a default value expression, set it in the attribute. + // We extract the generated attribute so we can add the new argument. + // It's important to reuse it, as it has the "add usings" annotation. + if (defaultValueExpression is not null) + { + observablePropertyAttributeList = + AttributeList(SingletonSeparatedList( + observablePropertyAttributeList.Attributes[0] + .AddArgumentListArguments( + AttributeArgument(ParseExpression(defaultValueExpression)) + .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); + } + // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -264,11 +291,15 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider continue; } + // Retrieve the properties passed by the analyzer + string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + ConvertToPartialProperty( propertyDeclaration, fieldDeclaration, observablePropertyAttributeList, - syntaxEditor); + syntaxEditor, + defaultValue); } return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 5d4dfbf58..b85e5dbeb 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -413,14 +413,26 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - // Check whether the value is a default constant value. - // If it is, then the property is valid (no explicit value). - if (!conversionOperation.Operand.IsConstantValueDefault()) + bool isNullableValueType = propertyTypeSymbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + + // Check whether the value is a default constant value. If it is, then the property is valid (no explicit value). + // We need to special case nullable value types, as the default value for the underlying type is not the actual default. + if (!conversionOperation.Operand.IsConstantValueDefault() || isNullableValueType) { - // If that is not the case, check if it's some constant value we can forward - if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) + // The value is just 'null' with no type, special case this one and skip the other checks below + if (conversionOperation.Operand is { Type: null, ConstantValue: { HasValue: true, Value: null } }) + { + // This is only allowed for reference or nullable types. This 'null' is redundant, but support it nonetheless. + // It's not that uncommon for especially legacy codebases to have this kind of pattern in dependency properties. + if (!propertyTypeSymbol.IsReferenceType && !isNullableValueType) + { + return; + } + } + else if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) { - // As a last resort, check if this is explicitly a 'default(T)' expression + // If that is not the case, check if it's some constant value we can forward. In this case, we did not + // retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) { return; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index e7b2e7f27..82f98a78b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1225,7 +1225,6 @@ public partial class MyControl : Control public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldNotInitialized_DoesNotWarn() { const string source = """ - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -1252,7 +1251,6 @@ public string? Name public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldWithDifferentName_DoesNotWarn() { const string source = """ - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -1297,7 +1295,6 @@ public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_Invalid string typeMetadata) { string source = $$""" - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -1328,7 +1325,6 @@ public string? Name public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingGetter_DoesNotWarn() { const string source = """ - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -1358,7 +1354,6 @@ public string? Name public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingSetter_DoesNotWarn() { const string source = """ - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -1396,7 +1391,6 @@ public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidPr string propertyType) { string source = $$""" - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -1447,7 +1441,6 @@ public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidPr string defaultValueExpression) { string source = $$""" - using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 9e6d81b9f..b975b8cb7 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -30,7 +30,7 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer [DataRow("object", "object?")] [DataRow("int", "int")] [DataRow("int?", "int?")] - public async Task SimpleProperty(string underlyingType, string propertyType) + public async Task SimpleProperty(string dependencyPropertyType, string propertyType) { string original = $$""" using Windows.UI.Xaml; @@ -42,7 +42,7 @@ public class MyControl : Control { public static readonly DependencyProperty NameProperty = DependencyProperty.Register( name: nameof(Name), - propertyType: typeof({{underlyingType}}), + propertyType: typeof({{dependencyPropertyType}}), ownerType: typeof(MyControl), typeMetadata: null); @@ -83,4 +83,145 @@ public partial class MyControl : Control await test.RunAsync(); } + + [TestMethod] + [DataRow("string", "string", "null")] + [DataRow("string", "string", "default(string)")] + [DataRow("string", "string", "(string)null")] + [DataRow("string", "string?", "null")] + [DataRow("object", "object", "null")] + [DataRow("object", "object?", "null")] + [DataRow("int", "int", "0")] + [DataRow("int", "int", "default(int)")] + [DataRow("int?", "int?", "null")] + [DataRow("int?", "int?", "default(int?)")] + [DataRow("int?", "int?", "null")] + [DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")] + public async Task SimpleProperty_WithExplicitValue_DefaultValue( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = { AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + }} + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string", "string", "\"\"")] + [DataRow("string", "string", "\"Hello\"")] + [DataRow("int", "int", "42")] + [DataRow("int?", "int?", "0")] + [DataRow("int?", "int?", "42")] + public async Task SimpleProperty_WithExplicitValue_NotDefault( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValueExpression}})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = { AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + }} + }; + + await test.RunAsync(); + } } From be37f5f3c2ab9823115e219ef17ac5caf999f5a3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Dec 2024 16:12:01 -0800 Subject: [PATCH 072/200] Add more generator unit tests --- .../Test_DependencyPropertyGenerator.cs | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 940ea53f7..6162b1442 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -2699,4 +2699,180 @@ public partial string? Name CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); } + + [TestMethod] + + // The 'string' and 'object' types are special + [DataRow("string", "string", "null")] + [DataRow("string", "string?", "null")] + [DataRow("object", "object", "null")] + [DataRow("object", "object?", "null")] + + // Well known WinRT primitive types + [DataRow("int", "int", "null")] + [DataRow("byte", "byte", "null")] + [DataRow("sbyte", "sbyte", "null")] + [DataRow("short", "short", "null")] + [DataRow("ushort", "ushort", "null")] + [DataRow("uint", "uint", "null")] + [DataRow("long", "long", "null")] + [DataRow("ulong", "ulong", "null")] + [DataRow("char", "char", "null")] + [DataRow("float", "float", "null")] + [DataRow("double", "double", "null")] + + // Well known WinRT struct types + [DataRow("Matrix3x2", "Matrix3x2", "null")] + [DataRow("Matrix4x4", "Matrix4x4", "null")] + [DataRow("Plane", "Plane", "null")] + [DataRow("Quaternion", "Quaternion", "null")] + [DataRow("Vector2", "Vector2", "null")] + [DataRow("Vector3", "Vector3", "null")] + [DataRow("Vector4", "Vector4", "null")] + [DataRow("Point", "Point", "null")] + [DataRow("Rect", "Rect", "null")] + + // Well known WinRT enum types + [DataRow("Visibility", "Visibility", "null")] + + // Nullable types, they're always just 'null' + [DataRow("int?", "int?", "null")] + [DataRow("byte?", "byte?", "null")] + [DataRow("char?", "char?", "null")] + [DataRow("long?", "long?", "null")] + [DataRow("float?", "float?", "null")] + [DataRow("double?", "double?", "null")] + [DataRow("DateTimeOffset?", "DateTimeOffset?", "null")] + [DataRow("TimeSpan?", "TimeSpan?", "null")] + [DataRow("Guid?", "Guid?", "null")] + [DataRow("KeyValuePair?", "KeyValuePair?", "null")] + + // Custom struct types + [DataRow("MyStruct", "MyStruct", "new PropertyMetadata(default(MyStruct))", "public struct MyStruct { public int X; }")] + [DataRow("MyStruct", "MyStruct", "new PropertyMetadata(default(MyStruct))", "public struct MyStruct { public string X { get; set; }")] + + // Custom enum types + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "public enum MyEnum { A, B, C }")] + public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized( + string dependencyPropertyType, + string propertyType, + string propertyMetadataExpression, + string? typeDefinition = "") + { + string source = $$""" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + #nullable enable + + namespace MyNamespace; + + {{typeDefinition}} + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial {{propertyType}} Name { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{propertyType}} Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + {{propertyType}} __unboxedValue = ({{propertyType}})__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref {{propertyType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref {{propertyType}} propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged({{propertyType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); + } } From 26d8f033ecbba7aa4104024f50286d038647b5de Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Dec 2024 17:49:55 -0800 Subject: [PATCH 073/200] Handle supported default value types --- .../DependencyPropertyGenerator.Execute.cs | 41 ++++++++- .../Extensions/ITypeSymbolExtensions.cs | 47 ++++++++++ .../Models/DependencyPropertyDefaultValue.cs | 3 +- .../CSharpGeneratorTest{TGenerator}.cs | 2 + .../Test_DependencyPropertyGenerator.cs | 86 ++++++++++--------- 5 files changed, 135 insertions(+), 44 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 4c5c82c5b..1cb241624 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -322,7 +322,44 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // First we need to special case non nullable values, as for those we need 'default'. if (propertySymbol.Type is { IsValueType: true } and not INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }) { - return new DependencyPropertyDefaultValue.Default(propertySymbol.Type.GetFullyQualifiedName()); + string fullyQualifiedTypeName = propertySymbol.Type.GetFullyQualifiedName(); + + // There is a special case for this: if the type of the property is a built-in WinRT + // projected enum type or struct type (ie. some projected value type in general, except + // for 'Nullable' values), then we can just use 'null' and bypass creating the property + // metadata. The WinRT runtime will automatically instantiate a default value for us. + if (propertySymbol.Type.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || + propertySymbol.Type.IsContainedInNamespace("Windows.Foundation.Numerics")) + { + return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); + } + + // Special case a few more well known value types that are mapped for WinRT + if (propertySymbol.Type.Name is "Point" or "Rect" or "Size" && + propertySymbol.Type.IsContainedInNamespace("Windows.Foundation")) + { + return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); + } + + // Lastly, special case the well known primitive types + if (propertySymbol.Type.SpecialType is + SpecialType.System_Int32 or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Char or + SpecialType.System_Single or + SpecialType.System_Double) + { + return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); + } + + // In all other cases, just use 'default(T)' here + return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: false); } // For all other ones, we can just use the 'null' placeholder again @@ -436,7 +473,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) string typeMetadata = propertyInfo switch { // Shared codegen - { DefaultValue: DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default, IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } + { DefaultValue: DependencyPropertyDefaultValue.Null or DependencyPropertyDefaultValue.Default(_, true), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => "null", { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: false, IsSharedPropertyChangedCallbackImplemented: false } => $""" diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index eba208177..873b5fc43 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -136,4 +136,51 @@ static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder BuildFrom(symbol, in builder); } + + /// + /// Checks whether a given type is contained in a namespace with a specified name. + /// + /// The input instance. + /// The namespace to check. + /// Whether is contained within . + public static bool IsContainedInNamespace(this ITypeSymbol symbol, string? namespaceName) + { + static void BuildFrom(INamespaceSymbol? symbol, ref readonly ImmutableArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (i.e. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + // Special case for no containing namespace + if (symbol.ContainingNamespace is not { } containingNamespace) + { + return namespaceName is null; + } + + // Special case if the type is directly in the global namespace + if (containingNamespace.IsGlobalNamespace) + { + return containingNamespace.MetadataName == namespaceName; + } + + using ImmutableArrayBuilder builder = new(); + + BuildFrom(containingNamespace, in builder); + + return builder.WrittenSpan.SequenceEqual(namespaceName.AsSpan()); + } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs index b494f4c5c..29469028e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -27,7 +27,8 @@ public override string ToString() /// A type representing default value for a specific type. /// /// The input type name. - public sealed record Default(string TypeName) : DependencyPropertyDefaultValue + /// Indicates whether the type is projected, meaning WinRT can default initialize it automatically if needed. + public sealed record Default(string TypeName, bool IsProjectedType) : DependencyPropertyDefaultValue { /// public override string ToString() diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index 5390d1858..027198c79 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.Foundation; using Windows.UI.ViewManagement; using Windows.UI.Xaml; @@ -178,6 +179,7 @@ private static CSharpCompilation CreateCompilation(string source, LanguageVersio IEnumerable metadataReferences = [ .. Net80.References.All, + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 6162b1442..7836c6e5c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -2702,64 +2702,68 @@ public partial string? Name [TestMethod] - // The 'string' and 'object' types are special - [DataRow("string", "string", "null")] - [DataRow("string", "string?", "null")] - [DataRow("object", "object", "null")] - [DataRow("object", "object?", "null")] + // The 'string' type is special + [DataRow("string", "string", "object", "null")] + [DataRow("string", "string?", "object?", "null")] // Well known WinRT primitive types - [DataRow("int", "int", "null")] - [DataRow("byte", "byte", "null")] - [DataRow("sbyte", "sbyte", "null")] - [DataRow("short", "short", "null")] - [DataRow("ushort", "ushort", "null")] - [DataRow("uint", "uint", "null")] - [DataRow("long", "long", "null")] - [DataRow("ulong", "ulong", "null")] - [DataRow("char", "char", "null")] - [DataRow("float", "float", "null")] - [DataRow("double", "double", "null")] + [DataRow("int", "int", "object", "null")] + [DataRow("byte", "byte", "object", "null")] + [DataRow("sbyte", "sbyte", "object", "null")] + [DataRow("short", "short", "object", "null")] + [DataRow("ushort", "ushort", "object", "null")] + [DataRow("uint", "uint", "object", "null")] + [DataRow("long", "long", "object", "null")] + [DataRow("ulong", "ulong", "object", "null")] + [DataRow("char", "char", "object", "null")] + [DataRow("float", "float", "object", "null")] + [DataRow("double", "double", "object", "null")] // Well known WinRT struct types - [DataRow("Matrix3x2", "Matrix3x2", "null")] - [DataRow("Matrix4x4", "Matrix4x4", "null")] - [DataRow("Plane", "Plane", "null")] - [DataRow("Quaternion", "Quaternion", "null")] - [DataRow("Vector2", "Vector2", "null")] - [DataRow("Vector3", "Vector3", "null")] - [DataRow("Vector4", "Vector4", "null")] - [DataRow("Point", "Point", "null")] - [DataRow("Rect", "Rect", "null")] + [DataRow("global::Windows.Foundation.Numerics.Matrix3x2", "global::Windows.Foundation.Numerics.Matrix3x2", "object", "null")] + [DataRow("global::Windows.Foundation.Numerics.Matrix4x4", "global::Windows.Foundation.Numerics.Matrix4x4", "object", "null")] + [DataRow("global::Windows.Foundation.Numerics.Plane", "global::Windows.Foundation.Numerics.Plane", "object", "null")] + [DataRow("global::Windows.Foundation.Numerics.Quaternion", "global::Windows.Foundation.Numerics.Quaternion", "object", "null")] + [DataRow("global::Windows.Foundation.Numerics.Vector2", "global::Windows.Foundation.Numerics.Vector2", "object", "null")] + [DataRow("global::Windows.Foundation.Numerics.Vector3", "global::Windows.Foundation.Numerics.Vector3", "object", "null")] + [DataRow("global::Windows.Foundation.Numerics.Vector4", "global::Windows.Foundation.Numerics.Vector4", "object", "null")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "object", "null")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "object", "null")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "object", "null")] // Well known WinRT enum types - [DataRow("Visibility", "Visibility", "null")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "object", "null")] // Nullable types, they're always just 'null' - [DataRow("int?", "int?", "null")] - [DataRow("byte?", "byte?", "null")] - [DataRow("char?", "char?", "null")] - [DataRow("long?", "long?", "null")] - [DataRow("float?", "float?", "null")] - [DataRow("double?", "double?", "null")] - [DataRow("DateTimeOffset?", "DateTimeOffset?", "null")] - [DataRow("TimeSpan?", "TimeSpan?", "null")] - [DataRow("Guid?", "Guid?", "null")] - [DataRow("KeyValuePair?", "KeyValuePair?", "null")] + [DataRow("int?", "int?", "object?", "null")] + [DataRow("byte?", "byte?", "object?", "null")] + [DataRow("char?", "char?", "object?", "null")] + [DataRow("long?", "long?", "object?", "null")] + [DataRow("float?", "float?", "object?", "null")] + [DataRow("double?", "double?", "object?", "null")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "object?", "null")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "object?", "null")] + [DataRow("global::System.Guid?", "global::System.Guid?", "object?", "null")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "object?", "null")] // Custom struct types - [DataRow("MyStruct", "MyStruct", "new PropertyMetadata(default(MyStruct))", "public struct MyStruct { public int X; }")] - [DataRow("MyStruct", "MyStruct", "new PropertyMetadata(default(MyStruct))", "public struct MyStruct { public string X { get; set; }")] + [DataRow("global::MyNamespace.MyStruct", "global::MyNamespace.MyStruct", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::MyNamespace.MyStruct))", "public struct MyStruct { public int X; }")] + [DataRow("global::MyNamespace.MyStruct", "global::MyNamespace.MyStruct", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::MyNamespace.MyStruct))", "public struct MyStruct { public string X { get; set; } }")] // Custom enum types - [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "public enum MyEnum { A, B, C }")] + [DataRow("global::MyNamespace.MyEnum", "global::MyNamespace.MyEnum", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::MyNamespace.MyEnum))", "public enum MyEnum { A, B, C }")] public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized( string dependencyPropertyType, string propertyType, + string defaultValueDefinition, string propertyMetadataExpression, string? typeDefinition = "") { string source = $$""" + using System; + using System.Collections.Generic; + using Windows.Foundation; + using Windows.Foundation.Numerics; using Windows.UI.Xaml; using CommunityToolkit.WinUI; @@ -2832,7 +2836,7 @@ public partial {{propertyType}} Name /// The raw property value that has been retrieved from . /// This method is invoked on the boxed value retrieved via on . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNameGet(ref object? propertyValue); + partial void OnNameGet(ref {{defaultValueDefinition}} propertyValue); /// Executes the logic for when the accessor is invoked /// The unboxed property value that has been retrieved from . @@ -2844,7 +2848,7 @@ public partial {{propertyType}} Name /// The boxed property value that has been produced before assigning to . /// This method is invoked on the boxed value that is about to be passed to on . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnNameSet(ref object? propertyValue); + partial void OnNameSet(ref {{defaultValueDefinition}} propertyValue); /// Executes the logic for when the accessor is invoked /// The property value that is being assigned to . From a3e4c026fb0e235b9f43aa2cbe71c03891f8bee7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Dec 2024 17:55:50 -0800 Subject: [PATCH 074/200] Remove unnecessary using directive --- ...UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b975b8cb7..1a8f31def 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -13,10 +13,6 @@ using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyCodeFixer>; -using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< - CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, - CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyCodeFixer, - Microsoft.CodeAnalysis.Testing.DefaultVerifier>; namespace CommunityToolkit.GeneratedDependencyProperty.Tests; From d8aee78e7bff6841f7e2a446f933957251132129 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Dec 2024 23:30:50 -0800 Subject: [PATCH 075/200] Fix handling of numerics, add more projected types --- .../DependencyPropertyGenerator.Execute.cs | 10 ++++++++-- .../Extensions/ITypeSymbolExtensions.cs | 10 ++++++++++ .../Test_DependencyPropertyGenerator.cs | 16 +++++++++------- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 1cb241624..f3dc16bc8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -320,7 +320,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // In all other cases, we'll automatically use the default value of the type in question. // First we need to special case non nullable values, as for those we need 'default'. - if (propertySymbol.Type is { IsValueType: true } and not INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }) + if (!propertySymbol.Type.IsDefaultValueNull()) { string fullyQualifiedTypeName = propertySymbol.Type.GetFullyQualifiedName(); @@ -329,7 +329,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // for 'Nullable' values), then we can just use 'null' and bypass creating the property // metadata. The WinRT runtime will automatically instantiate a default value for us. if (propertySymbol.Type.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || - propertySymbol.Type.IsContainedInNamespace("Windows.Foundation.Numerics")) + propertySymbol.Type.IsContainedInNamespace("System.Numerics")) { return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); } @@ -341,6 +341,12 @@ public static DependencyPropertyDefaultValue GetDefaultValue( return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); } + // Special case two more system types + if (propertySymbol.Type is INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) + { + return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); + } + // Lastly, special case the well known primitive types if (propertySymbol.Type.SpecialType is SpecialType.System_Int32 or diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 873b5fc43..261ddd64e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -13,6 +13,16 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; /// internal static class ITypeSymbolExtensions { + /// + /// Checks whether a given type has a default value of . + /// + /// The input instance to check. + /// Whether the default value of is . + public static bool IsDefaultValueNull(this ITypeSymbol symbol) + { + return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + } + /// /// Checks whether or not a given type symbol has a specified fully qualified metadata name. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 7836c6e5c..5d01fd0b8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -2720,16 +2720,18 @@ public partial string? Name [DataRow("double", "double", "object", "null")] // Well known WinRT struct types - [DataRow("global::Windows.Foundation.Numerics.Matrix3x2", "global::Windows.Foundation.Numerics.Matrix3x2", "object", "null")] - [DataRow("global::Windows.Foundation.Numerics.Matrix4x4", "global::Windows.Foundation.Numerics.Matrix4x4", "object", "null")] - [DataRow("global::Windows.Foundation.Numerics.Plane", "global::Windows.Foundation.Numerics.Plane", "object", "null")] - [DataRow("global::Windows.Foundation.Numerics.Quaternion", "global::Windows.Foundation.Numerics.Quaternion", "object", "null")] - [DataRow("global::Windows.Foundation.Numerics.Vector2", "global::Windows.Foundation.Numerics.Vector2", "object", "null")] - [DataRow("global::Windows.Foundation.Numerics.Vector3", "global::Windows.Foundation.Numerics.Vector3", "object", "null")] - [DataRow("global::Windows.Foundation.Numerics.Vector4", "global::Windows.Foundation.Numerics.Vector4", "object", "null")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "object", "null")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4", "object", "null")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane", "object", "null")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion", "object", "null")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2", "object", "null")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3", "object", "null")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4", "object", "null")] [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "object", "null")] [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "object", "null")] [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "object", "null")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "object", "null")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "object", "null")] // Well known WinRT enum types [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "object", "null")] From a7c51302b141c3885cb755d7a26cddfe835ed1d4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Dec 2024 23:46:20 -0800 Subject: [PATCH 076/200] Handle projected enums in analyzer, add tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 18 +++- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 4 +- .../Test_Analyzers.cs | 89 ++++++++++++++++++- 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index b85e5dbeb..037414fe0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -429,10 +429,22 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } } - else if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) + else if (TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) { - // If that is not the case, check if it's some constant value we can forward. In this case, we did not - // retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. + // We have found a valid constant. As an optimization, we check whether the constant was the value + // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the + // case, the XAML infrastructure can default that values automatically, meaning we can skip the + // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. + if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType && + operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + { + fieldFlags.DefaultValue = null; + } + } + else + { + // If we don't have a constant, check if it's some constant value we can forward. In this case, we + // did not retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) { return; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index c85c520a1..441d35d82 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -4,14 +4,15 @@ using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.WinUI; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis; +using Windows.Foundation; using Windows.UI.ViewManagement; using Windows.UI.Xaml; -using CommunityToolkit.WinUI; namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; @@ -49,6 +50,7 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe CSharpAnalyzerTest test = new(languageVersion) { TestCode = source }; test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 82f98a78b..13f7dbbe5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1379,6 +1379,44 @@ public string? Name await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn( + string dependencyPropertyType, + string propertyType, + string defaultValueExpression) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("string", "string")] [DataRow("string", "string?")] @@ -1386,6 +1424,21 @@ public string? Name [DataRow("object", "object?")] [DataRow("int", "int")] [DataRow("int?", "int?")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "null")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "null")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] + [DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "default(global::System.Collections.Generic.KeyValuePair?)")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns( string dependencyPropertyType, string propertyType) @@ -1412,6 +1465,10 @@ public partial class MyControl : Control set => SetValue(NameProperty, value); } } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } """; await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); @@ -1434,7 +1491,33 @@ public partial class MyControl : Control [DataRow("int?", "int?", "42")] [DataRow("int?", "int?", "default(int?)")] [DataRow("int?", "int?", "null")] - [DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "default(global::System.Numerics.Matrix3x2)")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4", "default(global::System.Numerics.Matrix4x4)")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane", "default(global::System.Numerics.Plane)")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion", "default(global::System.Numerics.Quaternion)")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2", "default(global::System.Numerics.Vector2)")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3", "default(global::System.Numerics.Vector3)")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4", "default(global::System.Numerics.Vector4)")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "default(global::Windows.Foundation.Point)")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "default(global::Windows.Foundation.Rect)")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")] + [DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "default(global::System.Collections.Generic.KeyValuePair?)")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_Warns( string dependencyPropertyType, string propertyType, @@ -1462,6 +1545,10 @@ public partial class MyControl : Control set => SetValue(NameProperty, value); } } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } """; await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); From 3c250dac2e643294b77b71fa053d19d343787acd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Dec 2024 00:03:52 -0800 Subject: [PATCH 077/200] Fix some analyzer bugs, add unit tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 8 ++- .../Extensions/ITypeSymbolExtensions.cs | 32 ++++++++++ .../Test_Analyzers.cs | 2 + ...ndencyPropertyOnManualPropertyCodeFixer.cs | 62 +++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 037414fe0..2e898dc22 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -438,7 +438,13 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType && operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) { - fieldFlags.DefaultValue = null; + // Before actually enabling the optimization, validate that the default value is actually + // the same as the default value of the enum (ie. the value of its first declared field). + if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) && + conversionOperation.Operand.ConstantValue.Value == defaultValue) + { + fieldFlags.DefaultValue = null; + } } } else diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 261ddd64e..959425807 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; using CommunityToolkit.GeneratedDependencyProperty.Helpers; using Microsoft.CodeAnalysis; @@ -23,6 +24,37 @@ public static bool IsDefaultValueNull(this ITypeSymbol symbol) return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; } + /// + /// Tries to get the default value of a given enum type. + /// + /// The input instance to check. + /// The resulting default value for , if it was an enum type. + /// Whether was retrieved successfully. + public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNullWhen(true)] out object? value) + { + if (symbol.TypeKind is not TypeKind.Enum) + { + value = default; + + return false; + } + + // The default value of the enum is the value of its first constant field + foreach (ISymbol memberSymbol in symbol.GetMembers()) + { + if (memberSymbol is IFieldSymbol { IsConst: true, ConstantValue: object defaultValue }) + { + value = defaultValue; + + return true; + } + } + + value = default; + + return false; + } + /// /// Checks whether or not a given type symbol has a specified fully qualified metadata name. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 13f7dbbe5..db6972429 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1382,6 +1382,7 @@ public string? Name [TestMethod] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")] [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn( string dependencyPropertyType, string propertyType, @@ -1502,6 +1503,7 @@ public class MyClass { } [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "default(global::Windows.Foundation.Rect)")] [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")] [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")] [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 1a8f31def..01cf0971a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.Foundation; using Windows.UI.ViewManagement; using Windows.UI.Xaml; using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< @@ -25,7 +26,41 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer [DataRow("object", "object")] [DataRow("object", "object?")] [DataRow("int", "int")] + [DataRow("byte", "byte")] + [DataRow("sbyte", "sbyte")] + [DataRow("short", "short")] + [DataRow("ushort", "ushort")] + [DataRow("uint", "uint")] + [DataRow("long", "long")] + [DataRow("ulong", "ulong")] + [DataRow("char", "char")] + [DataRow("float", "float")] + [DataRow("double", "double")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility")] [DataRow("int?", "int?")] + [DataRow("byte?", "byte?")] + [DataRow("char?", "char?")] + [DataRow("long?", "long?")] + [DataRow("float?", "float?")] + [DataRow("double?", "double?")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] + [DataRow("global::System.Guid?", "global::System.Guid?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] public async Task SimpleProperty(string dependencyPropertyType, string propertyType) { string original = $$""" @@ -48,6 +83,9 @@ public class MyControl : Control set => SetValue(NameProperty, value); } } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } """; string @fixed = $$""" @@ -62,6 +100,9 @@ public partial class MyControl : Control [GeneratedDependencyProperty] public partial {{propertyType}} {|CS9248:Name|} { get; set; } } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } """; CSharpCodeFixTest test = new(LanguageVersion.Preview) @@ -71,6 +112,7 @@ public partial class MyControl : Control ReferenceAssemblies = ReferenceAssemblies.Net.Net80, TestState = { AdditionalReferences = { + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) @@ -93,6 +135,23 @@ public partial class MyControl : Control [DataRow("int?", "int?", "default(int?)")] [DataRow("int?", "int?", "null")] [DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")] + [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "default(global::System.Numerics.Matrix3x2)")] + [DataRow("global::System.Numerics.Matrix4x4", "global::System.Numerics.Matrix4x4", "default(global::System.Numerics.Matrix4x4)")] + [DataRow("global::System.Numerics.Plane", "global::System.Numerics.Plane", "default(global::System.Numerics.Plane)")] + [DataRow("global::System.Numerics.Quaternion", "global::System.Numerics.Quaternion", "default(global::System.Numerics.Quaternion)")] + [DataRow("global::System.Numerics.Vector2", "global::System.Numerics.Vector2", "default(global::System.Numerics.Vector2)")] + [DataRow("global::System.Numerics.Vector3", "global::System.Numerics.Vector3", "default(global::System.Numerics.Vector3)")] + [DataRow("global::System.Numerics.Vector4", "global::System.Numerics.Vector4", "default(global::System.Numerics.Vector4)")] + [DataRow("global::Windows.Foundation.Point", "global::Windows.Foundation.Point", "default(global::Windows.Foundation.Point)")] + [DataRow("global::Windows.Foundation.Rect", "global::Windows.Foundation.Rect", "default(global::Windows.Foundation.Rect)")] + [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "null")] + [DataRow("global::System.Guid?", "global::System.Guid?", "null")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] public async Task SimpleProperty_WithExplicitValue_DefaultValue( string dependencyPropertyType, string propertyType, @@ -145,6 +204,7 @@ public partial class MyControl : Control ReferenceAssemblies = ReferenceAssemblies.Net.Net80, TestState = { AdditionalReferences = { + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) @@ -160,6 +220,7 @@ public partial class MyControl : Control [DataRow("int", "int", "42")] [DataRow("int?", "int?", "0")] [DataRow("int?", "int?", "42")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] public async Task SimpleProperty_WithExplicitValue_NotDefault( string dependencyPropertyType, string propertyType, @@ -212,6 +273,7 @@ public partial class MyControl : Control ReferenceAssemblies = ReferenceAssemblies.Net.Net80, TestState = { AdditionalReferences = { + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) From 3241ea549f47952806e5b9d5ea6e24bd5a7bd5cc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Dec 2024 10:08:31 -0800 Subject: [PATCH 078/200] Adjust priority for enum typed constants --- .../Models/TypedConstantInfo.Factory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs index e98111cde..e6fcecc9f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -87,6 +87,7 @@ public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out Typed { ({ SpecialType: SpecialType.System_String }, string text) => new Primitive.String(text), ({ SpecialType: SpecialType.System_Boolean}, bool flag) => new Primitive.Boolean(flag), + (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), (_, byte b) => new Primitive.Of(b), (_, char c) => new Primitive.Of(c), (_, double d) => new Primitive.Of(d), @@ -99,7 +100,6 @@ public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out Typed (_, ulong ul) => new Primitive.Of(ul), (_, ushort ush) => new Primitive.Of(ush), (_, ITypeSymbol type) => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), - (INamedTypeSymbol { TypeKind: TypeKind.Enum}, object value) => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), _ => throw new ArgumentException("Invalid typed constant type"), }; From 7d65ea17110bb265e410ca58916374a49fb2ebe3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Dec 2024 11:53:18 -0800 Subject: [PATCH 079/200] Fix handling of defaulted custom structs in analyzer --- .../DependencyPropertyGenerator.Execute.cs | 59 ++------------- ...endencyPropertyOnManualPropertyAnalyzer.cs | 14 +++- .../Extensions/WinRTExtensions.cs | 71 ++++++++++++++++++ .../Models/DependencyPropertyDefaultValue.cs | 5 ++ .../Models/TypedConstantInfo.cs | 5 ++ .../Test_Analyzers.cs | 32 ++++---- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 74 ++++++++++++++++++- 7 files changed, 189 insertions(+), 71 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index f3dc16bc8..566094d56 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -27,11 +27,6 @@ partial class DependencyPropertyGenerator /// private static partial class Execute { - /// - /// Placeholder for . - /// - private static readonly DependencyPropertyDefaultValue.Null NullInfo = new(); - /// /// Generates the sources for the embedded types, for PrivateAssets="all" scenarios. /// @@ -274,7 +269,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( } // Invalid callback, the analyzer will emit an error - return NullInfo; + return DependencyPropertyDefaultValue.Null.Instance; } token.ThrowIfCancellationRequested(); @@ -313,7 +308,7 @@ public static DependencyPropertyDefaultValue GetDefaultValue( } // Otherwise, the value has been explicitly set to 'null', so let's respect that - return NullInfo; + return DependencyPropertyDefaultValue.Null.Instance; } token.ThrowIfCancellationRequested(); @@ -322,54 +317,14 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // First we need to special case non nullable values, as for those we need 'default'. if (!propertySymbol.Type.IsDefaultValueNull()) { - string fullyQualifiedTypeName = propertySymbol.Type.GetFullyQualifiedName(); - - // There is a special case for this: if the type of the property is a built-in WinRT - // projected enum type or struct type (ie. some projected value type in general, except - // for 'Nullable' values), then we can just use 'null' and bypass creating the property - // metadata. The WinRT runtime will automatically instantiate a default value for us. - if (propertySymbol.Type.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || - propertySymbol.Type.IsContainedInNamespace("System.Numerics")) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // Special case a few more well known value types that are mapped for WinRT - if (propertySymbol.Type.Name is "Point" or "Rect" or "Size" && - propertySymbol.Type.IsContainedInNamespace("Windows.Foundation")) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // Special case two more system types - if (propertySymbol.Type is INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // Lastly, special case the well known primitive types - if (propertySymbol.Type.SpecialType is - SpecialType.System_Int32 or - SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Char or - SpecialType.System_Single or - SpecialType.System_Double) - { - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: true); - } - - // In all other cases, just use 'default(T)' here - return new DependencyPropertyDefaultValue.Default(fullyQualifiedTypeName, IsProjectedType: false); + // For non nullable types, we return 'default(T)', unless we can optimize for projected types + return new DependencyPropertyDefaultValue.Default( + TypeName: propertySymbol.Type.GetFullyQualifiedName(), + IsProjectedType: propertySymbol.Type.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)); } // For all other ones, we can just use the 'null' placeholder again - return NullInfo; + return DependencyPropertyDefaultValue.Null.Instance; } /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 2e898dc22..10881e9f5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -65,6 +65,7 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(static context => { // Get the XAML mode to use @@ -393,7 +394,18 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } // First, check if the metadata is 'null' (simplest case) - if (propertyMetadataArgument.Value.ConstantValue is not { HasValue: true, Value: null }) + if (propertyMetadataArgument.Value.ConstantValue is { HasValue: true, Value: null }) + { + // Here we need to special case non nullable value types that are not well known WinRT projected types. + // In this case, we cannot rely on XAML calling their default constructor. Rather, we need to preserve + // the explicit 'null' value that users had in their code. The analyzer will then warn on these cases + if (!propertyTypeSymbol.IsDefaultValueNull() && + !propertyTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + else { // Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument] } objectCreationOperation) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs new file mode 100644 index 000000000..de7983612 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -0,0 +1,71 @@ +// 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.GeneratedDependencyProperty.Constants; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for WinRT scenarios. +/// +internal static class WinRTExtensions +{ + /// + /// Checks whether a given type is a well known WinRT projected value type (ie. a type that XAML can default). + /// + /// The input instance to check. + /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. + /// Whether is a well known WinRT projected value type.. + public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, bool useWindowsUIXaml) + { + // This method only cares about non nullable value types + if (symbol.IsDefaultValueNull()) + { + return false; + } + + // There is a special case for this: if the type of the property is a built-in WinRT + // projected enum type or struct type (ie. some projected value type in general, except + // for 'Nullable' values), then we can just use 'null' and bypass creating the property + // metadata. The WinRT runtime will automatically instantiate a default value for us. + if (symbol.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || + symbol.IsContainedInNamespace("System.Numerics")) + { + return true; + } + + // Special case a few more well known value types that are mapped for WinRT + if (symbol.Name is "Point" or "Rect" or "Size" && + symbol.IsContainedInNamespace("Windows.Foundation")) + { + return true; + } + + // Special case two more system types + if (symbol is INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) + { + return true; + } + + // Lastly, special case the well known primitive types + if (symbol.SpecialType is + SpecialType.System_Int32 or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Char or + SpecialType.System_Single or + SpecialType.System_Double) + { + return true; + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs index 29469028e..993c9d717 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -16,6 +16,11 @@ internal abstract partial record DependencyPropertyDefaultValue /// public sealed record Null : DependencyPropertyDefaultValue { + /// + /// The shared instance (the type is stateless). + /// + public static Null Instance { get; } = new(); + /// public override string ToString() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index 48accd4e1..427f25813 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -23,6 +23,11 @@ internal abstract partial record TypedConstantInfo /// public sealed record Null : TypedConstantInfo { + /// + /// The shared instance (the type is stateless). + /// + public static Null Instance { get; } = new(); + /// public override string ToString() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index db6972429..b073ab716 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1382,7 +1382,6 @@ public string? Name [TestMethod] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")] [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")] - [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn( string dependencyPropertyType, string propertyType, @@ -1425,21 +1424,21 @@ public enum MyEnum { A, B, C } [DataRow("object", "object?")] [DataRow("int", "int")] [DataRow("int?", "int?")] - [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "null")] - [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")] - [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "null")] - [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")] - [DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")] - [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "default(global::System.Collections.Generic.KeyValuePair?)")] - [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?", "null")] - [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")] - [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")] - [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")] - [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")] - [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")] - [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] - [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")] - [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")] + [DataRow("global::System.TimeSpan", "global::System.TimeSpan")] + [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] + [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset")] + [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")] + [DataRow("global::System.Guid?", "global::System.Guid?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] + [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?" )] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns( string dependencyPropertyType, string propertyType) @@ -1504,6 +1503,7 @@ public class MyClass { } [DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")] [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")] [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 01cf0971a..5b7badbb1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -57,10 +57,10 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")] [DataRow("global::System.Guid?", "global::System.Guid?")] [DataRow("global::System.Collections.Generic.KeyValuePair?", "global::System.Collections.Generic.KeyValuePair?")] - [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")] - [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")] + [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass?")] public async Task SimpleProperty(string dependencyPropertyType, string propertyType) { string original = $$""" @@ -86,6 +86,7 @@ public class MyControl : Control public struct MyStruct { public string X { get; set; } } public enum MyEnum { A, B, C } + public class MyClass { } """; string @fixed = $$""" @@ -101,6 +102,75 @@ public partial class MyControl : Control public partial {{propertyType}} {|CS9248:Name|} { get; set; } } + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + public class MyClass { } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = { AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + }} + }; + + await test.RunAsync(); + } + + // These are custom value types, on properties where the metadata was set to 'null'. In this case, the + // default value would just be 'null', as XAML can't default initialize them. To preserve behavior, + // we must include an explicit default value. This will warn when the code is recompiled, but that + // is expected, because this specific scenario was (1) niche, and (2) kinda busted already anyway. + [TestMethod] + [DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")] + public async Task SimpleProperty_ExplicitNull(string dependencyPropertyType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public struct MyStruct { public string X { get; set; } } + public enum MyEnum { A, B, C } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + public struct MyStruct { public string X { get; set; } } public enum MyEnum { A, B, C } """; From e48226cb53eba00ce13164b66ea3a1d823d17654 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Dec 2024 15:03:39 -0800 Subject: [PATCH 080/200] Improve formatting for known enum members --- .../Extensions/ITypeSymbolExtensions.cs | 40 ++++++++++++++++++- .../Models/TypedConstantInfo.Factory.cs | 14 +++++-- .../Models/TypedConstantInfo.cs | 14 +++++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 959425807..04b23a33a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -34,7 +34,7 @@ public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNu { if (symbol.TypeKind is not TypeKind.Enum) { - value = default; + value = null; return false; } @@ -50,7 +50,43 @@ public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNu } } - value = default; + value = null; + + return false; + } + + /// + /// Tries to get the name of the enum field matching a given value. + /// + /// The input instance to check. + /// The value for to try to get the field for. + /// The name of the field with the specified value, if found. + /// Whether was successfully retrieved. + public static bool TryGetEnumFieldName(this ITypeSymbol symbol, object value, [NotNullWhen(true)] out string? fieldName) + { + if (symbol.TypeKind is not TypeKind.Enum) + { + fieldName = null; + + return false; + } + + // The default value of the enum is the value of its first constant field + foreach (ISymbol memberSymbol in symbol.GetMembers()) + { + if (memberSymbol is not IFieldSymbol { IsConst: true, ConstantValue: object fieldValue } fieldSymbol) + { + continue; + } + + if (fieldValue == value) + { + fieldName = fieldSymbol.Name; + + return true; + } + } + fieldName = null; return false; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs index e6fcecc9f..757d43467 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; namespace CommunityToolkit.GeneratedDependencyProperty.Models; @@ -53,8 +54,12 @@ public static TypedConstantInfo Create(TypedConstant arg) ushort ush => new Primitive.Of(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), + (TypedConstantKind.Type, ITypeSymbol type) + => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + (TypedConstantKind.Enum, object value) when arg.Type!.TryGetEnumFieldName(value, out string? fieldName) + => new KnownEnum(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), fieldName), + (TypedConstantKind.Enum, object value) + => new Enum(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), _ => throw new ArgumentException("Invalid typed constant type"), }; } @@ -87,7 +92,10 @@ public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out Typed { ({ SpecialType: SpecialType.System_String }, string text) => new Primitive.String(text), ({ SpecialType: SpecialType.System_Boolean}, bool flag) => new Primitive.Boolean(flag), - (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), + (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) when (operationType.TryGetEnumFieldName(value, out string? fieldName)) + => new KnownEnum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), fieldName), + (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) + => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), (_, byte b) => new Primitive.Of(b), (_, char c) => new Primitive.Of(c), (_, double d) => new Primitive.Of(d), diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index 427f25813..18b781dd4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -138,6 +138,20 @@ public override string ToString() } } + /// + /// A type representing a known enum value. + /// + /// The enum type name. + /// The enum field name. + public sealed record KnownEnum(string TypeName, string FieldName) : TypedConstantInfo + { + /// + public override string ToString() + { + return $"{TypeName}.{FieldName}"; + } + } + /// /// A type representing an enum value. /// From 4d085d37f58b2406e78278312d3ceeca37764b07 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 16 Dec 2024 07:31:18 -0800 Subject: [PATCH 081/200] Improve codegen for property changed callbacks --- .../DependencyPropertyGenerator.Execute.cs | 123 +- .../Helpers/IndentedTextWriter.cs | 2 +- .../Test_DependencyPropertyGenerator.cs | 1232 ++++++++++++++++- 3 files changed, 1291 insertions(+), 66 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 566094d56..99f7c4270 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -812,6 +812,9 @@ public static bool RequiresAdditionalTypes(EquatableArrayThe instance to write into. public static void WriteAdditionalTypes(EquatableArray propertyInfos, IndentedTextWriter writer) { + string fullyQualifiedTypeName = propertyInfos[0].Hierarchy.GetFullyQualifiedTypeName(); + + // Define the 'PropertyChangedCallbacks' type writer.WriteLine("using global::System.Runtime.CompilerServices;"); writer.WriteLine($"using global::{WellKnownTypeNames.XamlNamespace(propertyInfos[0].UseWindowsUIXaml)};"); writer.WriteLine(); @@ -821,15 +824,39 @@ public static void WriteAdditionalTypes(EquatableArray p /// """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName); - writer.WriteLine("file static class PropertyChangedCallbacks"); + writer.WriteLine("file sealed class PropertyChangedCallbacks"); using (writer.WriteBlock()) { - string fullyQualifiedTypeName = propertyInfos[0].Hierarchy.GetFullyQualifiedTypeName(); + // Shared dummy instance field (to make delegate invocations faster) + writer.WriteLine(""" + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + """, isMultiline: true); + + int numberOfSharedPropertyCallbacks = propertyInfos.Count(static property => !property.IsPropertyChangedCallbackImplemented && property.IsSharedPropertyChangedCallbackImplemented); + bool shouldCacheSharedPropertyChangedCallback = numberOfSharedPropertyCallbacks > 1; + bool shouldGenerateSharedPropertyCallback = numberOfSharedPropertyCallbacks > 0; + + // If the shared callback should be cached, do that here + if (shouldCacheSharedPropertyChangedCallback) + { + writer.WriteLine(); + writer.WriteLine(""" + /// Shared instance, for all properties only using the shared callback. + private static readonly PropertyChangedCallback SharedPropertyChangedCallback = new(Instance.OnPropertyChanged); + """, isMultiline: true); + } // Write the public accessors to use in property initializers - writer.WriteLineSeparatedMembers(propertyInfos.AsSpan(), (propertyInfo, writer) => + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) { + if (!propertyInfo.IsPropertyChangedCallbackImplemented && !propertyInfo.IsSharedPropertyChangedCallbackImplemented) + { + continue; + } + + writer.WriteLine(); writer.WriteLine($$""" /// /// Gets a value for . @@ -837,56 +864,106 @@ public static void WriteAdditionalTypes(EquatableArray p /// The value with the right callbacks. public static PropertyChangedCallback {{propertyInfo.PropertyName}}() { - static void Invoke(object d, DependencyPropertyChangedEventArgs e) - { - {{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d; - """, isMultiline: true); writer.IncreaseIndent(); - writer.IncreaseIndent(); - // Per-property callback, if present + // There are 3 possible scenarios to handle: + // 1) The property uses a dedicated property changed callback. In this case we always need a dedicated stub. + // 2) The property uses the shared callback only, and there's more than one property like this. Reuse the instance. + // 3) This is the only property using the shared callback only. In that case, create a new delegate over it. if (propertyInfo.IsPropertyChangedCallbackImplemented) { - writer.WriteLine($"On{propertyInfo.PropertyName}PropertyChanged(__this, e);"); + writer.WriteLine($"return new(Instance.On{propertyInfo.PropertyName}PropertyChanged);"); + } + else if (shouldCacheSharedPropertyChangedCallback) + { + writer.WriteLine("return SharedPropertyChangedCallback;"); + } + else + { + writer.WriteLine("return new(Instance.OnPropertyChanged);"); + } + + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + + // Write the private combined + foreach (DependencyPropertyInfo propertyInfo in propertyInfos) + { + if (!propertyInfo.IsPropertyChangedCallbackImplemented) + { + continue; } - // Shared callback, if present + writer.WriteLine(); + writer.WriteLine($$""" + /// + private void On{{propertyInfo.PropertyName}}PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + {{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d; + + PropertyChangedUnsafeAccessors.On{{propertyInfo.PropertyName}}PropertyChanged(__this, e); + """, isMultiline: true); + + // Shared callback, if needed if (propertyInfo.IsSharedPropertyChangedCallbackImplemented) { - writer.WriteLine("OnPropertyChanged(__this, e);"); + writer.IncreaseIndent(); + writer.WriteLine($"PropertyChangedUnsafeAccessors.On{propertyInfo.PropertyName}PropertyChanged(__this, e);"); + writer.DecreaseIndent(); } - // Close the method and return the 'Invoke' method as a delegate (just one allocation here) - writer.DecreaseIndent(); - writer.DecreaseIndent(); - writer.WriteLine(""" - } + writer.WriteLine("}"); + } - return new(Invoke); + // If we need to generate the shared callback, let's also generate its target method + if (shouldGenerateSharedPropertyCallback) + { + writer.WriteLine(); + writer.WriteLine($$""" + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + {{fullyQualifiedTypeName}} __this = ({{fullyQualifiedTypeName}})d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); } """, isMultiline: true); - }); + } + } + + // Define the 'PropertyChangedAccessors' type + writer.WriteLine(); + writer.WriteLine($""" + /// + /// Contains all unsafe accessors for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(GeneratorName); + writer.WriteLine("file sealed class PropertyChangedUnsafeAccessors"); + using (writer.WriteBlock()) + { // Write the accessors for all WinRT-based callbacks (not the shared one) foreach (DependencyPropertyInfo propertyInfo in propertyInfos.Where(static property => property.IsPropertyChangedCallbackImplemented)) { - writer.WriteLine(); + writer.WriteLine(skipIfPresent: true); writer.WriteLine($""" /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "On{propertyInfo.PropertyName}PropertyChanged")] - private static extern void On{propertyInfo.PropertyName}PropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); + public static extern void On{propertyInfo.PropertyName}PropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); """, isMultiline: true); } // Also emit one for the shared callback, if it's ever used if (propertyInfos.Any(static property => property.IsSharedPropertyChangedCallbackImplemented)) { - writer.WriteLine(); + writer.WriteLine(skipIfPresent: true); writer.WriteLine($""" /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] - private static extern void OnPropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); + public static extern void OnPropertyChanged({fullyQualifiedTypeName} _, DependencyPropertyChangedEventArgs e); """, isMultiline: true); } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs index 0883e84d1..b244356d6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Helpers/IndentedTextWriter.cs @@ -227,7 +227,7 @@ public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameo /// Indicates whether to skip adding the line if there already is one. public void WriteLine(bool skipIfPresent = false) { - if (skipIfPresent && this.builder.WrittenSpan is [.., '\n', '\n']) + if (skipIfPresent && this.builder.WrittenSpan is [.., '\n' or '{', '\n']) { return; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 5d01fd0b8..a84b3f2d0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -276,27 +276,40 @@ namespace CommunityToolkit.WinUI.DependencyPropertyGenerator [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - file static class PropertyChangedCallbacks + file sealed class PropertyChangedCallbacks { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + /// /// Gets a value for . /// /// The value with the right callbacks. public static PropertyChangedCallback Number() { - static void Invoke(object d, DependencyPropertyChangedEventArgs e) - { - global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + return new(Instance.OnNumberPropertyChanged); + } - OnNumberPropertyChanged(__this, e); - } + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; - return new(Invoke); + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); } + } + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] - private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); } } """; @@ -791,27 +804,40 @@ namespace CommunityToolkit.WinUI.DependencyPropertyGenerator [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - file static class PropertyChangedCallbacks + file sealed class PropertyChangedCallbacks { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + /// /// Gets a value for . /// /// The value with the right callbacks. public static PropertyChangedCallback Number() { - static void Invoke(object d, DependencyPropertyChangedEventArgs e) - { - global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + return new(Instance.OnNumberPropertyChanged); + } - OnNumberPropertyChanged(__this, e); - } + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; - return new(Invoke); + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); } + } + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] - private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); } } """; @@ -1064,27 +1090,40 @@ namespace CommunityToolkit.WinUI.DependencyPropertyGenerator [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - file static class PropertyChangedCallbacks + file sealed class PropertyChangedCallbacks { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + /// /// Gets a value for . /// /// The value with the right callbacks. public static PropertyChangedCallback Number() { - static void Invoke(object d, DependencyPropertyChangedEventArgs e) - { - global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + return new(Instance.OnNumberPropertyChanged); + } - OnNumberPropertyChanged(__this, e); - } + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; - return new(Invoke); + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); } + } + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] - private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); } } """; @@ -1221,27 +1260,40 @@ namespace CommunityToolkit.WinUI.DependencyPropertyGenerator [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - file static class PropertyChangedCallbacks + file sealed class PropertyChangedCallbacks { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + /// /// Gets a value for . /// /// The value with the right callbacks. public static PropertyChangedCallback Number() { - static void Invoke(object d, DependencyPropertyChangedEventArgs e) - { - global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + return new(Instance.OnPropertyChanged); + } - OnPropertyChanged(__this, e); - } + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; - return new(Invoke); + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); } + } + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] - private static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); } } """; @@ -1382,32 +1434,45 @@ namespace CommunityToolkit.WinUI.DependencyPropertyGenerator [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - file static class PropertyChangedCallbacks + file sealed class PropertyChangedCallbacks { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + /// /// Gets a value for . /// /// The value with the right callbacks. public static PropertyChangedCallback Number() { - static void Invoke(object d, DependencyPropertyChangedEventArgs e) - { - global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + return new(Instance.OnNumberPropertyChanged); + } - OnNumberPropertyChanged(__this, e); - OnPropertyChanged(__this, e); - } + /// + private void OnNumberPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; - return new(Invoke); + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnNumberPropertyChanged(__this, e); } + } + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnNumberPropertyChanged")] - private static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + public static extern void OnNumberPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); /// [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] - private static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); } } """; @@ -2330,6 +2395,1089 @@ public partial string? LastName CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + [TestMethod] + public void MultipleProperties_WithNoCaching_WithJustOnePropertyCallback() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnFirstNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return new(Instance.OnFirstNamePropertyChanged); + } + + /// + private void OnFirstNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnFirstNamePropertyChanged")] + public static extern void OnFirstNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithSharedPropertyCallback() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.LastName())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// Shared instance, for all properties only using the shared callback. + private static readonly PropertyChangedCallback SharedPropertyChangedCallback = new(Instance.OnPropertyChanged); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return SharedPropertyChangedCallback; + } + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback LastName() + { + return SharedPropertyChangedCallback; + } + + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithMixedPropertyCallbacks() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnFirstNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.LastName())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return new(Instance.OnFirstNamePropertyChanged); + } + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback LastName() + { + return new(Instance.OnPropertyChanged); + } + + /// + private void OnFirstNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + } + + /// + private void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnPropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnFirstNamePropertyChanged")] + public static extern void OnFirstNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void MultipleProperties_WithNoCaching_WithMixedPropertyCallbacks2() + { + const string source = """ + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? FirstName { get; set; } + + [GeneratedDependencyProperty] + public partial string? LastName { get; set; } + + partial void OnFirstNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnLastNamePropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty FirstNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "FirstName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.FirstName())); + + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty LastNameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "LastName", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: global::CommunityToolkit.WinUI.DependencyPropertyGenerator.PropertyChangedCallbacks.LastName())); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? FirstName + { + get + { + object? __boxedValue = GetValue(FirstNameProperty); + + OnFirstNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnFirstNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnFirstNameSet(ref value); + + object? __boxedValue = value; + + OnFirstNameSet(ref __boxedValue); + + SetValue(FirstNameProperty, __boxedValue); + + OnFirstNameChanged(value); + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? LastName + { + get + { + object? __boxedValue = GetValue(LastNameProperty); + + OnLastNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnLastNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnLastNameSet(ref value); + + object? __boxedValue = value; + + OnLastNameSet(ref __boxedValue); + + SetValue(LastNameProperty, __boxedValue); + + OnLastNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnFirstNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnLastNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + + namespace CommunityToolkit.WinUI.DependencyPropertyGenerator + { + using global::System.Runtime.CompilerServices; + using global::Windows.UI.Xaml; + + /// + /// Contains shared property changed callbacks for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedCallbacks + { + /// Shared instance, used to speedup delegate invocations (avoids the shuffle thunks). + private static readonly PropertyChangedCallbacks Instance = new(); + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback FirstName() + { + return new(Instance.OnFirstNamePropertyChanged); + } + + /// + /// Gets a value for . + /// + /// The value with the right callbacks. + public static PropertyChangedCallback LastName() + { + return new(Instance.OnLastNamePropertyChanged); + } + + /// + private void OnFirstNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnFirstNamePropertyChanged(__this, e); + } + + /// + private void OnLastNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + global::MyNamespace.MyControl __this = (global::MyNamespace.MyControl)d; + + PropertyChangedUnsafeAccessors.OnLastNamePropertyChanged(__this, e); + PropertyChangedUnsafeAccessors.OnLastNamePropertyChanged(__this, e); + } + } + + /// + /// Contains all unsafe accessors for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + file sealed class PropertyChangedUnsafeAccessors + { + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnFirstNamePropertyChanged")] + public static extern void OnFirstNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnLastNamePropertyChanged")] + public static extern void OnLastNamePropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + + /// + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "OnPropertyChanged")] + public static extern void OnPropertyChanged(global::MyNamespace.MyControl _, DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + [TestMethod] [DataRow("int")] [DataRow("object")] From bea72f9aef917854e1ababc434c914483b84c1c2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 18 Dec 2024 12:02:12 -0800 Subject: [PATCH 082/200] Add incrementality tests --- .../CSharpGeneratorTest{TGenerator}.cs | 8 +- ...endencyPropertyGenerator_Incrementality.cs | 138 ++++++++++++++++++ 2 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index 027198c79..0240cf3d0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -55,7 +55,7 @@ public static void VerifyDiagnostics(string source, params string[] diagnosticsI /// The input source to process. /// The expected source to be generated. /// The language version to use to run the test. - public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp12) + public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp13) { RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); @@ -89,7 +89,7 @@ public static void VerifyIncrementalSteps( IncrementalStepRunReason outputReason, IncrementalStepRunReason? diagnosticsSourceReason, IncrementalStepRunReason sourceReason, - LanguageVersion languageVersion = LanguageVersion.CSharp12) + LanguageVersion languageVersion = LanguageVersion.CSharp13) { Compilation compilation = CreateCompilation(source, languageVersion); @@ -173,7 +173,7 @@ public static void VerifyIncrementalSteps( /// The input source to process. /// The language version to use to run the test. /// The resulting object. - private static CSharpCompilation CreateCompilation(string source, LanguageVersion languageVersion = LanguageVersion.CSharp12) + private static CSharpCompilation CreateCompilation(string source, LanguageVersion languageVersion = LanguageVersion.CSharp13) { // Get all assembly references for the .NET TFM and ComputeSharp IEnumerable metadataReferences = @@ -209,7 +209,7 @@ private static void RunGenerator( string source, out Compilation compilation, out ImmutableArray diagnostics, - LanguageVersion languageVersion = LanguageVersion.CSharp12) + LanguageVersion languageVersion = LanguageVersion.CSharp13) { Compilation originalCompilation = CreateCompilation(source, languageVersion); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs new file mode 100644 index 000000000..fbb1fa6c8 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs @@ -0,0 +1,138 @@ +// 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.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_DependencyPropertyGenerator_Incrementality +{ + [TestMethod] + public void ModifiedOptions_ModifiesOutput() + { + const string source = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + const string updatedSource = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = 42)] + public partial int Number { get; set; } + } + """"; + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Modified, + diagnosticsReason: null, + outputReason: IncrementalStepRunReason.Modified, + diagnosticsSourceReason: null, + sourceReason: IncrementalStepRunReason.Modified); + } + + [TestMethod] + public void AddedLeadingTrivia_DoesNotModifyOutput() + { + const string source = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + const string updatedSource = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + /// + /// This is some property. + /// + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Unchanged, + diagnosticsReason: null, + outputReason: IncrementalStepRunReason.Cached, + diagnosticsSourceReason: null, + sourceReason: IncrementalStepRunReason.Cached); + } + + [TestMethod] + public void AddedOtherMember_DoesNotModifyOutput() + { + const string source = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + const string updatedSource = """" + using Windows.UI.Xaml; + using CommunityToolkit.WinUI; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + public void Foo() + { + } + + [GeneratedDependencyProperty] + public partial int Number { get; set; } + } + """"; + + CSharpGeneratorTest.VerifyIncrementalSteps( + source, + updatedSource, + executeReason: IncrementalStepRunReason.Unchanged, + diagnosticsReason: null, + outputReason: IncrementalStepRunReason.Cached, + diagnosticsSourceReason: null, + sourceReason: IncrementalStepRunReason.Cached); + } +} From 0262e7836e082c94016f1245560cd72c16ee34cd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 18 Dec 2024 12:05:01 -0800 Subject: [PATCH 083/200] Remove unnecessary test code --- .../Constants/WellKnownTrackingNames.cs | 5 -- .../CSharpGeneratorTest{TGenerator}.cs | 64 +++---------------- ...endencyPropertyGenerator_Incrementality.cs | 6 -- 3 files changed, 8 insertions(+), 67 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs index 1539c7ea0..36502c28f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownTrackingNames.cs @@ -14,11 +14,6 @@ internal static class WellKnownTrackingNames /// public const string Execute = nameof(Execute); - /// - /// The filtered transform with just output diagnostics. - /// - public const string Diagnostics = nameof(Diagnostics); - /// /// The filtered transform with just output sources. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs index 0240cf3d0..ca28be27b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -76,18 +76,14 @@ public static void VerifySources(string source, (string Filename, string Source) /// The input source to process. /// The updated source to process. /// The reason for the first "Execute" step. - /// The reason for the "Diagnostics" step. /// The reason for the "Output" step. - /// The reason for the output step for the diagnostics. /// The reason for the final output source. /// The language version to use to run the test. public static void VerifyIncrementalSteps( string source, string updatedSource, IncrementalStepRunReason executeReason, - IncrementalStepRunReason? diagnosticsReason, IncrementalStepRunReason outputReason, - IncrementalStepRunReason? diagnosticsSourceReason, IncrementalStepRunReason sourceReason, LanguageVersion languageVersion = LanguageVersion.CSharp13) { @@ -110,61 +106,17 @@ public static void VerifyIncrementalSteps( GeneratorRunResult result = driver.GetRunResult().Results.Single(); - // Get the generated sources and validate them. We have two possible cases: if no diagnostics - // are produced, then just the output source node is triggered. Otherwise, we'll also have one - // output node which is used to emit the gathered diagnostics from the initial transform step. - if (diagnosticsSourceReason is not null) - { - Assert.AreEqual( - expected: 2, - actual: - result.TrackedOutputSteps - .SelectMany(outputStep => outputStep.Value) - .SelectMany(output => output.Outputs) - .Count()); - - // The "Diagnostics" name has one more parent compared to "Output", because it also - // has one extra Where(...) call on the node (used to filter out empty diagnostics). - Assert.AreEqual( - expected: diagnosticsSourceReason, - actual: - result.TrackedOutputSteps - .Single().Value - .Single(run => run.Inputs[0].Source.Inputs[0].Source.Name == "Diagnostics") - .Outputs.Single().Reason); - - Assert.AreEqual( - expected: sourceReason, - actual: - result.TrackedOutputSteps - .Single().Value - .Single(run => run.Inputs[0].Source.Name == "Output") - .Outputs.Single().Reason); - } - else - { - (object Value, IncrementalStepRunReason Reason)[] sourceOuputs = - result.TrackedOutputSteps - .SelectMany(outputStep => outputStep.Value) - .SelectMany(output => output.Outputs) - .ToArray(); - - Assert.AreEqual(1, sourceOuputs.Length); - Assert.AreEqual(sourceReason, sourceOuputs[0].Reason); - } + // Get the generated sources and validate them + (object Value, IncrementalStepRunReason Reason)[] sourceOuputs = + result.TrackedOutputSteps + .SelectMany(outputStep => outputStep.Value) + .SelectMany(output => output.Outputs) + .ToArray(); + Assert.AreEqual(1, sourceOuputs.Length); + Assert.AreEqual(sourceReason, sourceOuputs[0].Reason); Assert.AreEqual(executeReason, result.TrackedSteps["Execute"].Single().Outputs[0].Reason); Assert.AreEqual(outputReason, result.TrackedSteps["Output"].Single().Outputs[0].Reason); - - // Check the diagnostics reason, which might not be present - if (diagnosticsReason is not null) - { - Assert.AreEqual(diagnosticsReason, result.TrackedSteps["Diagnostics"].Single().Outputs[0].Reason); - } - else - { - Assert.IsFalse(result.TrackedSteps.ContainsKey("Diagnostics")); - } } /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs index fbb1fa6c8..eebe7591c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator_Incrementality.cs @@ -45,9 +45,7 @@ public partial class MyControl : DependencyObject source, updatedSource, executeReason: IncrementalStepRunReason.Modified, - diagnosticsReason: null, outputReason: IncrementalStepRunReason.Modified, - diagnosticsSourceReason: null, sourceReason: IncrementalStepRunReason.Modified); } @@ -87,9 +85,7 @@ public partial class MyControl : DependencyObject source, updatedSource, executeReason: IncrementalStepRunReason.Unchanged, - diagnosticsReason: null, outputReason: IncrementalStepRunReason.Cached, - diagnosticsSourceReason: null, sourceReason: IncrementalStepRunReason.Cached); } @@ -130,9 +126,7 @@ public void Foo() source, updatedSource, executeReason: IncrementalStepRunReason.Unchanged, - diagnosticsReason: null, outputReason: IncrementalStepRunReason.Cached, - diagnosticsSourceReason: null, sourceReason: IncrementalStepRunReason.Cached); } } From c315a6c597dd639852821eb1d75ed58dbeec8086 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 20 Dec 2024 12:26:38 +0100 Subject: [PATCH 084/200] Fix some leftovers --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index c7dbf7ec7..2898162fb 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -89,17 +89,17 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) /// /// The original document being fixed. /// The instance for the current compilation. - /// The resulting attribute list, if successfully retrieved. - /// Whether could be retrieved successfully. - private static bool TryGetGeneratedObservablePropertyAttributeList( + /// The resulting attribute list, if successfully retrieved. + /// Whether could be retrieved successfully. + private static bool TryGetGeneratedDependencyPropertyAttributeList( Document document, SemanticModel semanticModel, - [NotNullWhen(true)] out AttributeListSyntax? observablePropertyAttributeList) + [NotNullWhen(true)] out AttributeListSyntax? generatedDependencyPropertyAttributeList) { // Make sure we can resolve the '[GeneratedDependencyProperty]' attribute if (semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute) is not INamedTypeSymbol attributeSymbol) { - observablePropertyAttributeList = null; + generatedDependencyPropertyAttributeList = null; return false; } @@ -109,7 +109,7 @@ private static bool TryGetGeneratedObservablePropertyAttributeList( // Create the attribute syntax for the new '[GeneratedDependencyProperty]' attribute here too SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(attributeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); - observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); return true; } @@ -135,7 +135,7 @@ private static async Task ConvertToPartialProperty( await Task.CompletedTask; // If we can't generate the new attribute list, bail (this should never happen) - if (!TryGetGeneratedObservablePropertyAttributeList(document, semanticModel, out AttributeListSyntax? observablePropertyAttributeList)) + if (!TryGetGeneratedDependencyPropertyAttributeList(document, semanticModel, out AttributeListSyntax? generatedDependencyPropertyAttributeList)) { return document; } @@ -146,7 +146,7 @@ private static async Task ConvertToPartialProperty( ConvertToPartialProperty( propertyDeclaration, fieldDeclaration, - observablePropertyAttributeList, + generatedDependencyPropertyAttributeList, syntaxEditor, defaultValueExpression); @@ -159,14 +159,14 @@ private static async Task ConvertToPartialProperty( /// /// The for the property being updated. /// The for the declared property to remove. - /// The with the attribute to add. + /// The with the attribute to add. /// The instance to use. /// The expression for the default value of the property, if present /// An updated document with the applied code fix, and being replaced with a partial property. private static void ConvertToPartialProperty( PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, - AttributeListSyntax observablePropertyAttributeList, + AttributeListSyntax generatedDependencyPropertyAttributeList, SyntaxEditor syntaxEditor, string? defaultValueExpression) { @@ -175,9 +175,9 @@ private static void ConvertToPartialProperty( // It's important to reuse it, as it has the "add usings" annotation. if (defaultValueExpression is not null) { - observablePropertyAttributeList = + generatedDependencyPropertyAttributeList = AttributeList(SingletonSeparatedList( - observablePropertyAttributeList.Attributes[0] + generatedDependencyPropertyAttributeList.Attributes[0] .AddArgumentListArguments( AttributeArgument(ParseExpression(defaultValueExpression)) .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); @@ -194,18 +194,18 @@ private static void ConvertToPartialProperty( newNode: firstAttributeListSyntax.WithoutTrivia()); // If the property has at least an attribute list, move the trivia from it to the new attribute - observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); + generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); // Insert the new attribute - attributeLists = attributeLists.Insert(0, observablePropertyAttributeList); + attributeLists = attributeLists.Insert(0, generatedDependencyPropertyAttributeList); } else { // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list - observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration); + generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(propertyDeclaration); // Save the new attribute list - attributeLists = attributeLists.Add(observablePropertyAttributeList); + attributeLists = attributeLists.Add(generatedDependencyPropertyAttributeList); } // Get a new property that is partial and with semicolon token accessors @@ -268,7 +268,7 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider } // If we can't generate the new attribute list, bail (this should never happen) - if (!TryGetGeneratedObservablePropertyAttributeList(document, semanticModel, out AttributeListSyntax? observablePropertyAttributeList)) + if (!TryGetGeneratedDependencyPropertyAttributeList(document, semanticModel, out AttributeListSyntax? generatedDependencyPropertyAttributeList)) { return document; } @@ -297,7 +297,7 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider ConvertToPartialProperty( propertyDeclaration, fieldDeclaration, - observablePropertyAttributeList, + generatedDependencyPropertyAttributeList, syntaxEditor, defaultValue); } From 48734058e84c40d76015143e7957f6b6bc5a826b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 20 Dec 2024 12:50:13 +0100 Subject: [PATCH 085/200] Remove unnecessary parentheses --- .../Models/TypedConstantInfo.Factory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs index 757d43467..8c49ad307 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -92,7 +92,7 @@ public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out Typed { ({ SpecialType: SpecialType.System_String }, string text) => new Primitive.String(text), ({ SpecialType: SpecialType.System_Boolean}, bool flag) => new Primitive.Boolean(flag), - (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) when (operationType.TryGetEnumFieldName(value, out string? fieldName)) + (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) when operationType.TryGetEnumFieldName(value, out string? fieldName) => new KnownEnum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), fieldName), (INamedTypeSymbol { TypeKind: TypeKind.Enum }, object value) => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value), From fc423c5bdb14acc07e68ebfd2e9ce3790c7a67d6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 20 Dec 2024 13:52:09 +0100 Subject: [PATCH 086/200] Improve code fixer for known enum members --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 91 ++++++++++++++++--- .../Extensions/ITypeSymbolExtensions.cs | 2 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 69 +++++++++++++- 3 files changed, 148 insertions(+), 14 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 2898162fb..9004cfa8d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -114,6 +114,71 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList( return true; } + /// + /// Updates an for the [GeneratedDependencyProperty] attribute with the right default value. + /// + /// The original document being fixed. + /// The instance for the current compilation. + /// The expression for the default value of the property, if present + /// The updated attribute syntax. + private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList( + Document document, + SemanticModel semanticModel, + AttributeListSyntax generatedDependencyPropertyAttributeList, + string? defaultValueExpression) + { + // If we do have a default value expression, set it in the attribute. + // We extract the generated attribute so we can add the new argument. + // It's important to reuse it, as it has the "add usings" annotation. + if (defaultValueExpression is not null) + { + ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); + + // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed' + if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + { + string fullyQualifiedTypeName = expressionSyntax.ToFullString(); + + // Ensure we strip the global prefix, if present (it should always be present) + if (fullyQualifiedTypeName.StartsWith("global::")) + { + fullyQualifiedTypeName = fullyQualifiedTypeName["global::".Length..]; + } + + // Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not + // a fully qualified type name. However, for virtually all cases for enum types, the two should match. + // That is, they will be the same if the type is not nested, and not generic, which is what we expect. + if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedTypeName) is INamedTypeSymbol enumTypeSymbol) + { + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the identifier syntax for the enum type, with the right annotations + SyntaxNode enumTypeSyntax = syntaxGenerator.TypeExpression(enumTypeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + + // Create the member access expression for the target enum type + SyntaxNode enumMemberAccessExpressionSyntax = syntaxGenerator.MemberAccessExpression(enumTypeSyntax, memberName); + + // Create the attribute argument to insert + SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax); + + // Actually add the argument to the existing attribute syntax + return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); + } + } + + // Otherwise, just add the new default value normally + return + AttributeList(SingletonSeparatedList( + generatedDependencyPropertyAttributeList.Attributes[0] + .AddArgumentListArguments( + AttributeArgument(ParseExpression(defaultValueExpression)) + .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); + } + + // If we have no value expression, we can just reuse the attribute with no changes + return generatedDependencyPropertyAttributeList; + } + /// /// Applies the code fix to a target identifier and returns an updated document. /// @@ -144,6 +209,8 @@ private static async Task ConvertToPartialProperty( SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); ConvertToPartialProperty( + document, + semanticModel, propertyDeclaration, fieldDeclaration, generatedDependencyPropertyAttributeList, @@ -157,6 +224,8 @@ private static async Task ConvertToPartialProperty( /// /// Applies the code fix to a target identifier and returns an updated document. /// + /// The original document being fixed. + /// The instance for the current compilation. /// The for the property being updated. /// The for the declared property to remove. /// The with the attribute to add. @@ -164,24 +233,20 @@ private static async Task ConvertToPartialProperty( /// The expression for the default value of the property, if present /// An updated document with the applied code fix, and being replaced with a partial property. private static void ConvertToPartialProperty( + Document document, + SemanticModel semanticModel, PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, AttributeListSyntax generatedDependencyPropertyAttributeList, SyntaxEditor syntaxEditor, string? defaultValueExpression) { - // If we do have a default value expression, set it in the attribute. - // We extract the generated attribute so we can add the new argument. - // It's important to reuse it, as it has the "add usings" annotation. - if (defaultValueExpression is not null) - { - generatedDependencyPropertyAttributeList = - AttributeList(SingletonSeparatedList( - generatedDependencyPropertyAttributeList.Attributes[0] - .AddArgumentListArguments( - AttributeArgument(ParseExpression(defaultValueExpression)) - .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); - } + // Update the attribute to insert with the default value, if present + generatedDependencyPropertyAttributeList = UpdateGeneratedDependencyPropertyAttributeList( + document, + semanticModel, + generatedDependencyPropertyAttributeList, + defaultValueExpression); // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -295,6 +360,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; ConvertToPartialProperty( + document, + semanticModel, propertyDeclaration, fieldDeclaration, generatedDependencyPropertyAttributeList, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 04b23a33a..c7306082b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -79,7 +79,7 @@ public static bool TryGetEnumFieldName(this ITypeSymbol symbol, object value, [N continue; } - if (fieldValue == value) + if (fieldValue.Equals(value)) { fieldName = fieldSymbol.Name; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 5b7badbb1..491008c08 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -290,7 +290,9 @@ public partial class MyControl : Control [DataRow("int", "int", "42")] [DataRow("int?", "int?", "0")] [DataRow("int?", "int?", "42")] - [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] + [DataRow("Visibility", "Visibility", "Visibility.Collapsed")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "(global::MyApp.MyEnum)5")] + [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "(global::MyApp.MyEnum)(-5)")] public async Task SimpleProperty_WithExplicitValue_NotDefault( string dependencyPropertyType, string propertyType, @@ -318,6 +320,8 @@ public partial class MyControl : Control set => SetValue(NameProperty, value); } } + + public enum MyEnum { A } """; string @fixed = $$""" @@ -334,6 +338,69 @@ public partial class MyControl : Control [GeneratedDependencyProperty(DefaultValue = {{defaultValueExpression}})] public partial {{propertyType}} {|CS9248:Name|} { get; set; } } + + public enum MyEnum { A } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + TestState = { AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), + MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) + }} + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace() + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(Windows.UI.Xaml.Automation.AnnotationType), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(Windows.UI.Xaml.Automation.AnnotationType.TrackChanges)); + + public Windows.UI.Xaml.Automation.AnnotationType [|Name|] + { + get => (Windows.UI.Xaml.Automation.AnnotationType)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Automation; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = AnnotationType.TrackChanges)] + public partial Windows.UI.Xaml.Automation.AnnotationType {|CS9248:Name|} { get; set; } + } """; CSharpCodeFixTest test = new(LanguageVersion.Preview) From b65e89975bfcb9b1a061a040121a5d66d97ca9cd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 21 Dec 2024 19:02:02 +0100 Subject: [PATCH 087/200] Add .targets to .NET 9 folders too --- .../CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index 9f3c18d45..b199aff64 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -36,8 +36,10 @@ + + From 7524a0c2c0bda0d4ce9fd18dbe700100fbbb6560 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 16:49:14 +0100 Subject: [PATCH 088/200] Simplify code fixer tests --- ...harpCodeFixerTest{TAnalyzer,TCodeFixer}.cs | 11 ++++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 56 ++----------------- 2 files changed, 16 insertions(+), 51 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs index 6bbc2a8f0..16ba5a5d5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpCodeFixerTest{TAnalyzer,TCodeFixer}.cs @@ -2,12 +2,16 @@ // 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.WinUI; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Testing; +using Windows.Foundation; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; @@ -32,6 +36,13 @@ internal sealed class CSharpCodeFixTest : CSharpCodeFixTe public CSharpCodeFixTest(LanguageVersion languageVersion) { this.languageVersion = languageVersion; + + ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + TestState.AnalyzerConfigFiles.Add(("/.editorconfig", "[*]\nend_of_line = lf")); } /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 491008c08..2384e3267 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -3,14 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; -using CommunityToolkit.WinUI; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Windows.Foundation; -using Windows.UI.ViewManagement; -using Windows.UI.Xaml; using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyCodeFixer>; @@ -110,15 +104,7 @@ public class MyClass { } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net80, - TestState = { AdditionalReferences = - { - MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), - MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) - }} + FixedCode = @fixed }; await test.RunAsync(); @@ -178,15 +164,7 @@ public enum MyEnum { A, B, C } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net80, - TestState = { AdditionalReferences = - { - MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), - MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) - }} + FixedCode = @fixed }; await test.RunAsync(); @@ -270,15 +248,7 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net80, - TestState = { AdditionalReferences = - { - MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), - MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) - }} + FixedCode = @fixed }; await test.RunAsync(); @@ -345,15 +315,7 @@ public enum MyEnum { A } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net80, - TestState = { AdditionalReferences = - { - MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), - MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) - }} + FixedCode = @fixed }; await test.RunAsync(); @@ -406,15 +368,7 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net80, - TestState = { AdditionalReferences = - { - MetadataReference.CreateFromFile(typeof(Point).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location), - MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location) - }} + FixedCode = @fixed }; await test.RunAsync(); From 45029c6f3fe845d162375c4da95f6602b80863ef Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 17:42:45 +0100 Subject: [PATCH 089/200] Undo temporary hacks and workarounds --- .github/workflows/build.yml | 95 +++++++++++++++++-- Directory.Build.targets | 1 + .../AppServices.SourceGenerators.Tests.csproj | 4 + ...oolkit.AppServices.SourceGenerators.csproj | 5 + .../src/CommunityToolkit.AppServices.csproj | 6 +- ...ependencyInjection.SourceGenerators.csproj | 30 ++++++ ...lkit.Extensions.DependencyInjection.csproj | 5 + 7 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0a0661ca..0897a88f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,8 +60,8 @@ jobs: strategy: fail-fast: false # prevent one matrix pipeline from being cancelled if one fails, we want them all to run to completion. matrix: - winui: [2, 3] # Temporary until we can get Uno/Wasm working - multitarget: ['uwp', 'wasdk'] # Temporary until we can get Uno/Wasm working + winui: [2, 3] + multitarget: ['uwp', 'wasdk', 'wasm', 'wpf', 'linuxgtk', 'macos', 'ios', 'android'] exclude: # WinUI 2 not supported on wasdk - winui: 2 @@ -134,7 +134,42 @@ jobs: # Generate full solution with all projects (sample gallery heads, components, tests) - name: Generate solution with ${{ matrix.multitarget }} gallery, components and tests - run: powershell -version 5.1 -command "./tooling/Build-Toolkit-Components.ps1 -Components DependencyPropertyGenerator -Release -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop + working-directory: ./ + run: powershell -version 5.1 -command "./tooling/GenerateAllSolution.ps1 -MultiTargets ${{ matrix.multitarget }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} -WinUIMajorVersion ${{ matrix.winui }}" -ErrorAction Stop + + # Build solution + - name: MSBuild (With diagnostics) + if: ${{ env.ENABLE_DIAGNOSTICS == 'true' }} + run: > + msbuild.exe /restore /nowarn:MSB4011 + /p:Configuration=Release + /m + ${{ env.ENABLE_DIAGNOSTICS == 'true' && '/bl' || '' }} + /v:${{ env.MSBUILD_VERBOSITY }} + CommunityToolkit.AllComponents.sln + + - name: MSBuild + if: ${{ env.ENABLE_DIAGNOSTICS == 'false' }} + run: msbuild.exe CommunityToolkit.AllComponents.sln /restore /nowarn:MSB4011 -p:Configuration=Release + + # Run tests + - name: Setup VSTest Path + uses: darenm/setup-vstest@3a16d909a1f3bbc65b52f8270d475d905e7d3e44 + + - name: Install Testspace Module + uses: testspace-com/setup-testspace@v1 + with: + domain: ${{ github.repository_owner }} + + - name: Run component tests against ${{ matrix.multitarget }} + if: ${{ matrix.multitarget == 'uwp' || matrix.multitarget == 'wasdk' }} + id: test-platform + run: vstest.console.exe ./tooling/**/CommunityToolkit.Tests.${{ matrix.multitarget }}.build.appxrecipe /Framework:FrameworkUap10 /logger:"trx;LogFileName=${{ matrix.multitarget }}.trx" /Blame + + - name: Create test reports + run: | + testspace '[${{ matrix.multitarget }}]./TestResults/*.trx' + if: ${{ (matrix.multitarget == 'uwp' || matrix.multitarget == 'wasdk') && (steps.test-generator.conclusion == 'success' || steps.test-platform.conclusion == 'success') }} - name: Artifact - Diagnostic Logs uses: actions/upload-artifact@v4 @@ -178,7 +213,7 @@ jobs: strategy: fail-fast: false # prevent one matrix pipeline from being cancelled if one fails, we want them all to run to completion. matrix: - winui: [2, 3] # Temporary until we can get Uno/Wasm working + winui: [2, 3] env: VERSION_PROPERTY: ${{ github.ref == 'refs/heads/main' && format('build.{0}', github.run_number) || format('pull-{0}.{1}', github.event.number, github.run_number) }} @@ -234,7 +269,7 @@ jobs: # Build and pack component nupkg - name: Build and pack component packages - run: ./tooling/Build-Toolkit-Components.ps1 -Components DependencyPropertyGenerator -MultiTargets uwp,wasdk -ExcludeMultiTargets ${{ env.EXCLUDED_MULTITARGETS }} -WinUIMajorVersion ${{ matrix.winui }} -DateForVersion ${{ env.VERSION_DATE }} ${{ env.VERSION_PROPERTY != '' && format('-PreviewVersion "{0}"', env.VERSION_PROPERTY) || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-EnableBinlogs' || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-Verbose' || '' }} -BinlogOutput ./ -NupkgOutput ./ -Release + run: ./tooling/Build-Toolkit-Components.ps1 -MultiTargets all -ExcludeMultiTargets ${{ env.EXCLUDED_MULTITARGETS }} -WinUIMajorVersion ${{ matrix.winui }} -DateForVersion ${{ env.VERSION_DATE }} ${{ env.VERSION_PROPERTY != '' && format('-PreviewVersion "{0}"', env.VERSION_PROPERTY) || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-EnableBinlogs' || '' }} ${{ env.ENABLE_DIAGNOSTICS == 'true' && '-Verbose' || '' }} -BinlogOutput ./ -NupkgOutput ./ -Release - name: Validate package names if: ${{ env.VERSION_PROPERTY != '' }} @@ -271,4 +306,52 @@ jobs: if: ${{ (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }} with: name: build-logs-winui${{ matrix.winui }} - path: ./*.*log \ No newline at end of file + path: ./*.*log + + wasm-linux: + runs-on: ubuntu-latest + env: + HEADS_DIRECTORY: tooling/ProjectHeads + + steps: + - name: Install .NET SDK v${{ env.DOTNET_VERSION }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: .NET Info (if diagnostics) + if: ${{ env.ENABLE_DIAGNOSTICS == 'true' }} + run: dotnet --info + + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: recursive + + # Restore Tools from Manifest list in the Repository + - name: Restore dotnet tools + run: dotnet tool restore + + - name: Generate solution + shell: pwsh + working-directory: ./ + run: ./tooling/GenerateAllSolution.ps1${{ env.ENABLE_DIAGNOSTICS == 'true' && ' -UseDiagnostics' || '' }} + + - name: Install ninja for WASM native dependencies + run: sudo apt-get install ninja-build + + # Issue with Comment Links currently, see: https://github.com/mrlacey/CommentLinks/issues/38 + # See launch.json configuration file for analogous command we're emulating here to build LINK: ../../.vscode/launch.json:CommunityToolkit.App.Wasm.csproj + - name: dotnet build + working-directory: ./${{ env.HEADS_DIRECTORY }}/AllComponents/Wasm/ + run: dotnet build /r /bl /p:UnoSourceGeneratorUseGenerationHost=true /p:UnoSourceGeneratorUseGenerationController=false + + # TODO: Do we want to run tests here? Can we do that on linux easily? + + - name: Artifact - Diagnostic Logs + uses: actions/upload-artifact@v4 + if: ${{ (env.ENABLE_DIAGNOSTICS == 'true' || env.COREHOST_TRACE != '') && always() }} + with: + name: linux-logs + path: ./**/*.*log \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets index a2a397dfa..77e29d5d4 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -26,6 +26,7 @@ + \ No newline at end of file diff --git a/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj b/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj index 3fceba82d..46b9111b5 100644 --- a/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj +++ b/components/AppServices/AppServices.SourceGenerators.Tests/AppServices.SourceGenerators.Tests.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj index 78f3c3ccc..64d80c88a 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj @@ -22,4 +22,9 @@ + + + + + diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index c7db44f7b..cf4e183b4 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -7,7 +7,6 @@ CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) false - false @@ -56,4 +55,9 @@ + + + + + diff --git a/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj b/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj new file mode 100644 index 000000000..e80e6cf87 --- /dev/null +++ b/components/Extensions.DependencyInjection/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators/CommunityToolkit.Extensions.DependencyInjection.SourceGenerators.csproj @@ -0,0 +1,30 @@ + + + netstandard2.0 + false + true + + + $(NoWarn);CS8500 + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj b/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj index 4aa44f5af..34c3bb7e4 100644 --- a/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj +++ b/components/Extensions.DependencyInjection/src/CommunityToolkit.Extensions.DependencyInjection.csproj @@ -30,4 +30,9 @@ + + + + + From f8886736eb238f1d142cda37653c72ccf1f23837 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 17:45:17 +0100 Subject: [PATCH 090/200] Fix tooling submodule pointer --- tooling | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling b/tooling index 4d331c6b2..b121eb57c 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 4d331c6b24e258afcd42d28ebf63bea781744541 +Subproject commit b121eb57cc0fdca03206a9e1a08960d7e3cd824c From 079da46fa395a6c1483293f4ff9c6753e9d35adc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 17:48:37 +0100 Subject: [PATCH 091/200] Only build for UWP and WindowsAppSDK --- components/DependencyPropertyGenerator/src/MultiTarget.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/src/MultiTarget.props b/components/DependencyPropertyGenerator/src/MultiTarget.props index b11c19426..6a4600344 100644 --- a/components/DependencyPropertyGenerator/src/MultiTarget.props +++ b/components/DependencyPropertyGenerator/src/MultiTarget.props @@ -4,6 +4,6 @@ MultiTarget is a custom property that indicates which target a project is designed to be built for / run on. Used to create project references, generate solution files, enable/disable TargetFrameworks, and build nuget packages. --> - uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + uwp;wasdk; \ No newline at end of file From cce95d374cf39918a54ee2fa550ff439b0d13555 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 19:32:39 +0100 Subject: [PATCH 092/200] Fix TFMs for UWP projects --- .../AppServices/src/CommunityToolkit.AppServices.csproj | 4 ++++ .../CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 3 +++ 2 files changed, 7 insertions(+) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index cf4e183b4..322750f44 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -6,7 +6,11 @@ true CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) + false false + + + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index b199aff64..ffe37c06a 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -9,6 +9,9 @@ CommunityToolkit.WinUI.DependencyPropertyGeneratorRns false false + + + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; From a4ced343df8b67613377989bfb5514df6604e0ba Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 19:33:09 +0100 Subject: [PATCH 093/200] Simplify packed .targets files --- ...munityToolkit.WinUI.DependencyPropertyGenerator.csproj | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index ffe37c06a..344bbdf47 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -38,12 +38,8 @@ - - - - - - + + From 65fdb63f95235b24e9e69ef2c4badaee8804ef04 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 20:25:23 +0100 Subject: [PATCH 094/200] Disable 'UseUwpTools' property --- ...ommunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index 344bbdf47..f86fb87a2 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -10,6 +10,12 @@ false false + + false + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; From 5a89531e90e83607aab463595789e5f1d8b1f43d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 25 Dec 2024 22:38:20 +0100 Subject: [PATCH 095/200] Fix filenames for packaged .targets files --- ...unityToolkit.WinUI.DependencyPropertyGenerator.csproj | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index f86fb87a2..a88fb0996 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -43,9 +43,12 @@ - - - + + + From ea7190925ea9e2206619d5445828b4902eda08c3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 02:32:35 +0100 Subject: [PATCH 096/200] Fix typos in .targets file --- ...mmunityToolkit.WinUI.DependencyPropertyGenerator.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 933629788..ced7e4f8a 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -2,9 +2,9 @@ - true - true - false + true + true + false From a2f339c1bcd68c8f57acf7773ae7f7b23e936c31 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 02:37:35 +0100 Subject: [PATCH 097/200] Fix .targets again, add basic test --- .../CommunityToolkit.DependencyPropertyGenerator.Tests.csproj | 3 +++ ...CommunityToolkit.WinUI.DependencyPropertyGenerator.targets | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj index ea88d28b9..94514510f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/CommunityToolkit.DependencyPropertyGenerator.Tests.csproj @@ -40,4 +40,7 @@ Link="EmbeddedResources\GeneratedDependencyPropertyAttribute.cs" LogicalName="GeneratedDependencyPropertyAttribute.g.cs" /> + + + diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index ced7e4f8a..612e2bd54 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -50,8 +50,8 @@ + DependsOnTargets="ResolveAssemblyReferences" + BeforeTargets="CoreCompile"> - uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; + + uap10.0.17763;net8.0-windows10.0.18362.0;net9.0-windows10.0.18362.0; diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index a88fb0996..c400281b5 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -16,8 +16,11 @@ --> false - - uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; + + uap10.0.17763;net8.0-windows10.0.18362.0;net9.0-windows10.0.18362.0; diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj index bc175d9ac..37b8fd8c0 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -16,8 +16,11 @@ false false - - uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; + + uap10.0.17763;net8.0-windows10.0.18362.0;net9.0-windows10.0.18362.0; From 9924f775ba7ce00c47963a7a8a03aadc814131fd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 13:28:25 +0100 Subject: [PATCH 100/200] Add more code fixer tests --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 2384e3267..5b2d20d22 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -373,4 +373,146 @@ public partial class MyControl : Control await test.RunAsync(); } + + [TestMethod] + public async Task MultipleProperties_HandlesSpacingCorrectly() + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? [|Name1|] + { + get => (string?)GetValue(Name1Property); + set => SetValue(Name1Property, value); + } + + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name1|} { get; set; } + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithInterspersedMembers_HandlesSpacingCorrectly() + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// This is another member + public int Blah => 42; + + public string? [|Name1|] + { + get => (string?)GetValue(Name1Property); + set => SetValue(Name1Property, value); + } + + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + // There is an extra leading blank line here for now, likely a 'SyntaxEditor' bug + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + + /// This is another member + public int Blah => 42; + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name1|} { get; set; } + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From 0c45212918a8d15fd349ebeae40c7f419643e90a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 14:21:47 +0100 Subject: [PATCH 101/200] Improve handling of known constants --- .../Models/TypedConstantInfo.cs | 160 +++++++++++++++--- 1 file changed, 138 insertions(+), 22 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index 18b781dd4..6e3104de9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using CommunityToolkit.GeneratedDependencyProperty.Helpers; @@ -95,32 +98,145 @@ public override string ToString() public sealed record Of(T Value) : TypedConstantInfo where T : unmanaged, IEquatable { + /// + /// The cached map of constant fields for the type. + /// + private static readonly FrozenDictionary ConstantFields = GetConstantFields(); + /// public override string ToString() { - LiteralExpressionSyntax expressionSyntax = LiteralExpression(SyntaxKind.NumericLiteralExpression, Value switch + static ExpressionSyntax GetExpression(T value) + { + // Try to match named constants first + if (TryGetConstantExpression(value, out ExpressionSyntax? expression)) + { + return expression; + } + + // Special logic for doubles + if (value is double d) + { + // Handle 'double.NaN' explicitly, as 'ToString()' won't work on it at all + if (double.IsNaN(d)) + { + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(SyntaxKind.DoubleKeyword)), IdentifierName("NaN")); + } + + // Handle 0, to avoid matching against positive/negative zeros + if (d == 0) + { + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal("0.0", 0.0)); + } + + string rawLiteral = d.ToString("R", CultureInfo.InvariantCulture); + + // For doubles, we need to manually format it and always add the trailing "D" suffix. + // This ensures that the correct type is produced if the expression was assigned to + // an object (eg. the literal was used in an attribute object parameter/property). + string literal = rawLiteral.Contains(".") ? rawLiteral : $"{rawLiteral}D"; + + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(literal, d)); + } + + // Same special handling for floats as well + if (value is float f) + { + // Handle 'float.NaN' as above + if (float.IsNaN(f)) + { + return MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(SyntaxKind.FloatKeyword)), IdentifierName("NaN")); + } + + // Handle 0, same as above too + if (f == 0) + { + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal("0.0F", 0.0f)); + } + + // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(f)); + } + + // Handle all other supported types as well + return LiteralExpression(SyntaxKind.NumericLiteralExpression, value switch + { + byte b => Literal(b), + char c => Literal(c), + 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") + }); + } + + return GetExpression(Value).NormalizeWhitespace(eol: "\n").ToFullString(); + } + + /// + /// Tries to get a constant expression for a given value. + /// + /// The value to try to get an expression for. + /// The resulting expression, if successfully retrieved. + /// The expression for , if available. + /// Thrown if is not of a supported type. + private static bool TryGetConstantExpression(T value, [NotNullWhen(true)] out ExpressionSyntax? expression) + { + if (ConstantFields.TryGetValue(value, out string? name)) { - byte b => Literal(b), - char c => Literal(c), - - // For doubles, we need to manually format it and always add the trailing "D" suffix. - // This ensures that the correct type is produced if the expression was assigned to - // an object (eg. the literal was used in an attribute object parameter/property). - double d => Literal(d.ToString("R", CultureInfo.InvariantCulture) + "D", d), - - // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed - 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") - }); - - return expressionSyntax.NormalizeWhitespace(eol: "\n").ToFullString(); + SyntaxKind syntaxKind = value switch + { + byte => SyntaxKind.ByteKeyword, + char => SyntaxKind.CharKeyword, + double => SyntaxKind.DoubleKeyword, + float => SyntaxKind.FloatKeyword, + int => SyntaxKind.IntKeyword, + long => SyntaxKind.LongKeyword, + sbyte => SyntaxKind.SByteKeyword, + short => SyntaxKind.ShortKeyword, + uint => SyntaxKind.UIntKeyword, + ulong => SyntaxKind.ULongKeyword, + ushort => SyntaxKind.UShortKeyword, + _ => throw new ArgumentException("Invalid primitive type") + }; + + expression = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(syntaxKind)), IdentifierName(name)); + + return true; + } + + expression = null; + + return false; + } + + /// + /// Gets a mapping of all well known constant fields for the current type. + /// + /// The mapping of all well known constant fields for the current type. + private static FrozenDictionary GetConstantFields() + { + return typeof(T) + .GetFields() + .Where(static info => info.IsLiteral) + .Where(static info => info.FieldType == typeof(T)) + .Select(static info => (Value: (T)info.GetRawConstantValue(), info.Name)) + .Where(static info => !EqualityComparer.Default.Equals(info.Value, default)) + .ToFrozenDictionary( + keySelector: static info => info.Value, + elementSelector: static info => info.Name); + + } } } From 500ed3657a9ba703739ae461b2635283edeae49b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 15:17:41 +0100 Subject: [PATCH 102/200] Improve number formatting, add fixer tests --- .../Models/TypedConstantInfo.cs | 11 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 141 +++++++++++++++++- 2 files changed, 143 insertions(+), 9 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index 6e3104de9..4ffb03a31 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -136,7 +136,7 @@ static ExpressionSyntax GetExpression(T value) // For doubles, we need to manually format it and always add the trailing "D" suffix. // This ensures that the correct type is produced if the expression was assigned to // an object (eg. the literal was used in an attribute object parameter/property). - string literal = rawLiteral.Contains(".") ? rawLiteral : $"{rawLiteral}D"; + string literal = rawLiteral.Contains(".") ? rawLiteral : $"{rawLiteral}.0"; return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(literal, d)); } @@ -158,8 +158,13 @@ static ExpressionSyntax GetExpression(T value) return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal("0.0F", 0.0f)); } - // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed - return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(f)); + string rawLiteral = f.ToString("R", CultureInfo.InvariantCulture); + + // For floats, Roslyn will automatically add the "F" suffix, so no extra work is needed. + // However, we still format it manually to ensure we consistently add ".0" as suffix. + string literal = rawLiteral.Contains(".") ? $"{rawLiteral}F" : $"{rawLiteral}.0F"; + + return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(literal, f)); } // Handle all other supported types as well diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 5b2d20d22..10d540254 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -324,7 +324,7 @@ public enum MyEnum { A } [TestMethod] public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace() { - string original = $$""" + const string original = """ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -348,7 +348,7 @@ public Windows.UI.Xaml.Automation.AnnotationType [|Name|] } """; - string @fixed = $$""" + const string @fixed = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Automation; @@ -377,7 +377,7 @@ public partial class MyControl : Control [TestMethod] public async Task MultipleProperties_HandlesSpacingCorrectly() { - string original = $$""" + const string original = """ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -413,7 +413,7 @@ public string? [|Name2|] } """; - string @fixed = $$""" + const string @fixed = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -444,7 +444,7 @@ public partial class MyControl : Control [TestMethod] public async Task MultipleProperties_WithInterspersedMembers_HandlesSpacingCorrectly() { - string original = $$""" + const string original = """ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -484,7 +484,7 @@ public string? [|Name2|] """; // There is an extra leading blank line here for now, likely a 'SyntaxEditor' bug - string @fixed = $$""" + const string @fixed = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -515,4 +515,133 @@ public partial class MyControl : Control await test.RunAsync(); } + + [TestMethod] + [DataRow("float", "0.0F", "1.0F", "0.123F")] + [DataRow("double", "0.0", "4.0", "0.123")] + public async Task MultipleProperties_HandlesWellKnownLiterals(string propertyType, string zeroExpression, string literalExpression, string decimalLiteralExpression) + { + string original = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty P1Property = DependencyProperty.Register( + name: "P1", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{zeroExpression}})); + + public static readonly DependencyProperty P2Property = DependencyProperty.Register( + name: "P2", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{propertyType}}.MinValue)); + + public static readonly DependencyProperty P3Property = DependencyProperty.Register( + name: "P3", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{propertyType}}.NaN)); + + public static readonly DependencyProperty P4Property = DependencyProperty.Register( + name: "P4", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{propertyType}}.Pi)); + + public static readonly DependencyProperty P5Property = DependencyProperty.Register( + name: "P5", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{literalExpression}})); + + public static readonly DependencyProperty P6Property = DependencyProperty.Register( + name: "P6", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata({{decimalLiteralExpression}})); + + public {{propertyType}} [|P1|] + { + get => ({{propertyType}})GetValue(P1Property); + set => SetValue(P1Property, value); + } + + public {{propertyType}} [|P2|] + { + get => ({{propertyType}})GetValue(P2Property); + set => SetValue(P2Property, value); + } + + public {{propertyType}} [|P3|] + { + get => ({{propertyType}})GetValue(P3Property); + set => SetValue(P3Property, value); + } + + public {{propertyType}} [|P4|] + { + get => ({{propertyType}})GetValue(P4Property); + set => SetValue(P4Property, value); + } + + public {{propertyType}} [|P5|] + { + get => ({{propertyType}})GetValue(P5Property); + set => SetValue(P5Property, value); + } + + public {{propertyType}} [|P6|] + { + get => ({{propertyType}})GetValue(P6Property); + set => SetValue(P6Property, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial {{propertyType}} {|CS9248:P1|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{propertyType}}.MinValue)] + public partial {{propertyType}} {|CS9248:P2|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{propertyType}}.NaN)] + public partial {{propertyType}} {|CS9248:P3|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{propertyType}}.Pi)] + public partial {{propertyType}} {|CS9248:P4|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{literalExpression}})] + public partial {{propertyType}} {|CS9248:P5|} { get; set; } + + [GeneratedDependencyProperty(DefaultValue = {{decimalLiteralExpression}})] + public partial {{propertyType}} {|CS9248:P6|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From f4ec6f5a536d1d0dcdb5ab72020ef47878d69d17 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 15:35:35 +0100 Subject: [PATCH 103/200] Bump SDK of UWP on .NET 9 to 19041 --- .../AppServices/src/CommunityToolkit.AppServices.csproj | 6 +++--- ...ommunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 6 +++--- .../Notifications/src/CommunityToolkit.Notifications.csproj | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index f7581f94a..18a3ef41f 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -10,10 +10,10 @@ false - uap10.0.17763;net8.0-windows10.0.18362.0;net9.0-windows10.0.18362.0; + uap10.0.17763;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0; diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index c400281b5..e31bcb860 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -17,10 +17,10 @@ false - uap10.0.17763;net8.0-windows10.0.18362.0;net9.0-windows10.0.18362.0; + uap10.0.17763;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0; diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj index 37b8fd8c0..249b85a32 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -17,10 +17,10 @@ false - uap10.0.17763;net8.0-windows10.0.18362.0;net9.0-windows10.0.18362.0; + uap10.0.17763;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0; From 53f3195a71e1383f41bd86a1a289312716fe2dcd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 26 Dec 2024 16:40:16 +0100 Subject: [PATCH 104/200] Remove Uno workaround, fix TFMs --- .../AppServices/src/CommunityToolkit.AppServices.csproj | 7 ++++--- ...mmunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 7 ++++--- .../src/CommunityToolkit.Notifications.csproj | 7 ++++--- tooling | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 18a3ef41f..f218ee405 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -8,12 +8,13 @@ $(PackageIdPrefix).$(ToolkitComponentName) false false + false - uap10.0.17763;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0; + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index e31bcb860..8b4d7ad61 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -15,12 +15,13 @@ not a UWP library per se. So to support the 17763 SDK, we can just disable the UWP build tools. --> false + false - uap10.0.17763;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0; + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj index 249b85a32..4a27c5737 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -15,12 +15,13 @@ disable false false + false - uap10.0.17763;net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0; + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/tooling b/tooling index 93931e0be..eb797b54f 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 93931e0beda3520fcc68e8bf9975a4ebb7067674 +Subproject commit eb797b54f753257c68d724621a47ee38155d603d From f3613a34857a48ad3e8a45b3b868a02736a9a051 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 26 Dec 2024 14:14:40 +0100 Subject: [PATCH 105/200] Trivia handling --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 33 +++++++++++++++++++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 2 -- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 9004cfa8d..f51bee401 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -298,6 +298,39 @@ private static void ConvertToPartialProperty( syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + // Special handling for the leading trivia of members following the field declaration we are about to remove. + // There is an edge case that can happen when a type declaration is as follows: + // + // class ContainingType + // { + // public static readonly DependencyProperty NameProperty = ...; + // + // public void SomeOtherMember() { } + // + // public string? Name { ... } + // } + // + // In this case, just removing the target field for the dependency property being rewritten (that is, 'NameProperty') + // will cause an extra blank line to be left after the edits, right above the member immediately following the field. + // To work around this, we look for such a member and check its trivia, and then manually remove a leading blank line. + if (fieldDeclaration.Parent is TypeDeclarationSyntax fieldParentTypeDeclaration) + { + int fieldDeclarationIndex = fieldParentTypeDeclaration.Members.IndexOf(fieldDeclaration); + + // Check whether there is a member immediatley following the field + if (fieldDeclarationIndex >= 0 && fieldDeclarationIndex < fieldParentTypeDeclaration.Members.Count - 1) + { + MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration.Members[fieldDeclarationIndex + 1]; + SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia(); + + // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one + if (leadingTrivia.Count > 0 && leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + syntaxEditor.ReplaceNode(nextMember, (nextMember, _) => nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0))); + } + } + } + // Also remove the field declaration (it'll be generated now) syntaxEditor.RemoveNode(fieldDeclaration); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 10d540254..9860dcbde 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -483,7 +483,6 @@ public string? [|Name2|] } """; - // There is an extra leading blank line here for now, likely a 'SyntaxEditor' bug const string @fixed = """ using CommunityToolkit.WinUI; using Windows.UI.Xaml; @@ -495,7 +494,6 @@ namespace MyApp; public partial class MyControl : Control { - /// This is another member public int Blah => 42; From c0318083c29223d7ea781c4584d4e55628a9d694 Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Thu, 26 Dec 2024 20:13:22 +0100 Subject: [PATCH 106/200] Use the lambda overload --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 105 ++++++++++-------- 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index f51bee401..36e0074b0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -241,62 +241,69 @@ private static void ConvertToPartialProperty( SyntaxEditor syntaxEditor, string? defaultValueExpression) { - // Update the attribute to insert with the default value, if present - generatedDependencyPropertyAttributeList = UpdateGeneratedDependencyPropertyAttributeList( - document, - semanticModel, - generatedDependencyPropertyAttributeList, - defaultValueExpression); + // Replace the property with the partial property using the attribute. Note that it's important to use the + // lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and + // replacing the original one. Doing that would cause the following 'ReplaceNode' call to adjust the leading + // trivia of trailing members after the fields being removed to not work incorrectly, and fail to be resolved. + syntaxEditor.ReplaceNode(propertyDeclaration, (node, _) => + { + PropertyDeclarationSyntax propertyDeclaration = (PropertyDeclarationSyntax)node; - // Start setting up the updated attribute lists - SyntaxList attributeLists = propertyDeclaration.AttributeLists; + // Update the attribute to insert with the default value, if present + generatedDependencyPropertyAttributeList = UpdateGeneratedDependencyPropertyAttributeList( + document, + semanticModel, + generatedDependencyPropertyAttributeList, + defaultValueExpression); - if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..]) - { - // Remove the trivia from the original first attribute - attributeLists = attributeLists.Replace( - nodeInList: firstAttributeListSyntax, - newNode: firstAttributeListSyntax.WithoutTrivia()); + // Start setting up the updated attribute lists + SyntaxList attributeLists = propertyDeclaration.AttributeLists; - // If the property has at least an attribute list, move the trivia from it to the new attribute - generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); + if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..]) + { + // Remove the trivia from the original first attribute + attributeLists = attributeLists.Replace( + nodeInList: firstAttributeListSyntax, + newNode: firstAttributeListSyntax.WithoutTrivia()); - // Insert the new attribute - attributeLists = attributeLists.Insert(0, generatedDependencyPropertyAttributeList); - } - else - { - // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list - generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(propertyDeclaration); + // If the property has at least an attribute list, move the trivia from it to the new attribute + generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); - // Save the new attribute list - attributeLists = attributeLists.Add(generatedDependencyPropertyAttributeList); - } + // Insert the new attribute + attributeLists = attributeLists.Insert(0, generatedDependencyPropertyAttributeList); + } + else + { + // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list + generatedDependencyPropertyAttributeList = generatedDependencyPropertyAttributeList.WithTriviaFrom(propertyDeclaration); - // Get a new property that is partial and with semicolon token accessors - PropertyDeclarationSyntax updatedPropertyDeclaration = - propertyDeclaration - .AddModifiers(Token(SyntaxKind.PartialKeyword)) - .WithoutLeadingTrivia() - .WithAttributeLists(attributeLists) - .WithAdditionalAnnotations(Formatter.Annotation) - .WithAccessorList(AccessorList(List( - [ - // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only - propertyDeclaration.AccessorList!.Accessors[0] - .WithBody(null) - .WithExpressionBody(null) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - .WithAdditionalAnnotations(Formatter.Annotation), - propertyDeclaration.AccessorList!.Accessors[1] - .WithBody(null) - .WithExpressionBody(null) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) - .WithAdditionalAnnotations(Formatter.Annotation) - ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); + // Save the new attribute list + attributeLists = attributeLists.Add(generatedDependencyPropertyAttributeList); + } - syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + // Get a new property that is partial and with semicolon token accessors + return + propertyDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .WithoutLeadingTrivia() + .WithAttributeLists(attributeLists) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithAccessorList(AccessorList(List( + [ + // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only + propertyDeclaration.AccessorList!.Accessors[0] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithAdditionalAnnotations(Formatter.Annotation), + propertyDeclaration.AccessorList!.Accessors[1] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); + }); // Special handling for the leading trivia of members following the field declaration we are about to remove. // There is an edge case that can happen when a type declaration is as follows: From 9ae5fba5abc728c9cc1461f0a2c217d221763b94 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 02:59:31 +0100 Subject: [PATCH 107/200] Tweak WinRT types matching logic --- .../Extensions/WinRTExtensions.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs index de7983612..738cfac37 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -30,7 +30,13 @@ public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, b // projected enum type or struct type (ie. some projected value type in general, except // for 'Nullable' values), then we can just use 'null' and bypass creating the property // metadata. The WinRT runtime will automatically instantiate a default value for us. - if (symbol.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) || + if (symbol.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + { + return true; + } + + // Special case for projected numeric types + if (symbol.Name is "Matrix3x2" or "Matrix4x4" or "Plane" or "Quaternion" or "Vector2" or "Vector3" or "Vector4" && symbol.IsContainedInNamespace("System.Numerics")) { return true; @@ -51,17 +57,17 @@ public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, b // Lastly, special case the well known primitive types if (symbol.SpecialType is - SpecialType.System_Int32 or - SpecialType.System_Byte or - SpecialType.System_SByte or - SpecialType.System_Int16 or - SpecialType.System_UInt16 or - SpecialType.System_UInt32 or - SpecialType.System_Int64 or - SpecialType.System_UInt64 or - SpecialType.System_Char or - SpecialType.System_Single or - SpecialType.System_Double) + SpecialType.System_Int32 or + SpecialType.System_Byte or + SpecialType.System_SByte or + SpecialType.System_Int16 or + SpecialType.System_UInt16 or + SpecialType.System_UInt32 or + SpecialType.System_Int64 or + SpecialType.System_UInt64 or + SpecialType.System_Char or + SpecialType.System_Single or + SpecialType.System_Double) { return true; } From 74919d5cf1b7d594132c8110abf906e840f705f4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 11:54:14 +0100 Subject: [PATCH 108/200] Fix AppServices build --- components/AppServices/src/CommunityToolkit.AppServices.csproj | 2 +- tooling | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index f218ee405..eb38fa4fd 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -37,7 +37,7 @@ - + Windows Desktop Extensions for the UWP diff --git a/tooling b/tooling index eb797b54f..3e178bc8f 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit eb797b54f753257c68d724621a47ee38155d603d +Subproject commit 3e178bc8f3d0eceb4ef9e955542ea5ce3892ca8a From 7d5c559169ac541196afb2123867d8191d4f74db Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 12:35:03 +0100 Subject: [PATCH 109/200] Add more test coverage for XML docs --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 9860dcbde..c57f393db 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -441,6 +441,89 @@ public partial class MyControl : Control await test.RunAsync(); } + [TestMethod] + public async Task MultipleProperties_WithXmlDocs_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class MyObject : DependencyObject + { + /// + /// Blah. + /// + public static readonly DependencyProperty TargetObjectProperty = DependencyProperty.Register( + nameof(TargetObject), + typeof(TElement?), + typeof(MyObject), + null); + + /// + /// Blah. + /// + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( + nameof(Value), + typeof(TValue?), + typeof(MyObject), + null); + + /// + /// Blah. + /// + public TValue? [|Value|] + { + get => (TValue?)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + /// + /// Blah. + /// + public TElement? [|TargetObject|] + { + get => (TElement?)GetValue(TargetObjectProperty); + set => SetValue(TargetObjectProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class MyObject : DependencyObject + { + /// + /// Blah. + /// + [GeneratedDependencyProperty] + public partial TValue? {|CS9248:Value|} { get; set; } + + /// + /// Blah. + /// + [GeneratedDependencyProperty] + public partial TElement? {|CS9248:TargetObject|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + [TestMethod] public async Task MultipleProperties_WithInterspersedMembers_HandlesSpacingCorrectly() { From 17bc9972d65ac2b893e3a88096ab5a575614d234 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 12:54:21 +0100 Subject: [PATCH 110/200] Fix handling of EOLs in removed members --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 97 ++++++++++++------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 36e0074b0..450519add 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2,6 +2,7 @@ // 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.Collections.Immutable; using System.Composition; using System.Diagnostics.CodeAnalysis; @@ -217,6 +218,8 @@ private static async Task ConvertToPartialProperty( syntaxEditor, defaultValueExpression); + RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor); + // Create the new document with the single change return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); } @@ -305,39 +308,6 @@ private static void ConvertToPartialProperty( ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); }); - // Special handling for the leading trivia of members following the field declaration we are about to remove. - // There is an edge case that can happen when a type declaration is as follows: - // - // class ContainingType - // { - // public static readonly DependencyProperty NameProperty = ...; - // - // public void SomeOtherMember() { } - // - // public string? Name { ... } - // } - // - // In this case, just removing the target field for the dependency property being rewritten (that is, 'NameProperty') - // will cause an extra blank line to be left after the edits, right above the member immediately following the field. - // To work around this, we look for such a member and check its trivia, and then manually remove a leading blank line. - if (fieldDeclaration.Parent is TypeDeclarationSyntax fieldParentTypeDeclaration) - { - int fieldDeclarationIndex = fieldParentTypeDeclaration.Members.IndexOf(fieldDeclaration); - - // Check whether there is a member immediatley following the field - if (fieldDeclarationIndex >= 0 && fieldDeclarationIndex < fieldParentTypeDeclaration.Members.Count - 1) - { - MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration.Members[fieldDeclarationIndex + 1]; - SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia(); - - // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one - if (leadingTrivia.Count > 0 && leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) - { - syntaxEditor.ReplaceNode(nextMember, (nextMember, _) => nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0))); - } - } - } - // Also remove the field declaration (it'll be generated now) syntaxEditor.RemoveNode(fieldDeclaration); @@ -352,6 +322,58 @@ private static void ConvertToPartialProperty( } } + /// + /// Removes any leftover leading end of lines on remaining members following any removed fields. + /// + /// The collection of all fields that have been removed. + /// The instance to use. + private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollection fieldDeclarations, SyntaxEditor syntaxEditor) + { + foreach (FieldDeclarationSyntax fieldDeclaration in fieldDeclarations) + { + // Special handling for the leading trivia of members following the field declaration we are about to remove. + // There is an edge case that can happen when a type declaration is as follows: + // + // class ContainingType + // { + // public static readonly DependencyProperty NameProperty = ...; + // + // public void SomeOtherMember() { } + // + // public string? Name { ... } + // } + // + // In this case, just removing the target field for the dependency property being rewritten (that is, 'NameProperty') + // will cause an extra blank line to be left after the edits, right above the member immediately following the field. + // To work around this, we look for such a member and check its trivia, and then manually remove a leading blank line. + if (fieldDeclaration.Parent is TypeDeclarationSyntax fieldParentTypeDeclaration) + { + int fieldDeclarationIndex = fieldParentTypeDeclaration.Members.IndexOf(fieldDeclaration); + + // Check whether there is a member immediatley following the field + if (fieldDeclarationIndex >= 0 && fieldDeclarationIndex < fieldParentTypeDeclaration.Members.Count - 1) + { + MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration.Members[fieldDeclarationIndex + 1]; + + // It's especially important to skip members that have been rmeoved. This would otherwise fail when computing + // the final document. We only care about fixing trivia for members that will still be present after all edits. + if (fieldDeclarations.Contains(nextMember)) + { + continue; + } + + SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia(); + + // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one + if (leadingTrivia.Count > 0 && leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + syntaxEditor.ReplaceNode(nextMember, (nextMember, _) => nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0))); + } + } + } + } + } + /// /// A custom with the logic from . /// @@ -381,6 +403,10 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider // Create an editor to perform all mutations (across all edits in the file) SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services); + // Create the set to track all fields being removed, to adjust whitespaces + HashSet fieldDeclarations = []; + + // Step 1: rewrite all properties and remove the fields foreach (Diagnostic diagnostic in diagnostics) { // Get the current property declaration for the diagnostic @@ -407,8 +433,13 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider generatedDependencyPropertyAttributeList, syntaxEditor, defaultValue); + + fieldDeclarations.Add(fieldDeclaration); } + // Step 2: remove any leftover leading end of lines on members following fields that have been removed + RemoveLeftoverLeadingEndOfLines(fieldDeclarations, syntaxEditor); + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); } } From 1dadd4d3979b1828e3722f40c1129de92a1f69a5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 16:14:36 +0100 Subject: [PATCH 111/200] Fix EOL handling in more scenarios --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 68 +++++--- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 159 ++++++++++++++++++ 2 files changed, 205 insertions(+), 22 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 450519add..27e9356ae 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -346,31 +346,55 @@ private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollection= 0 && fieldDeclarationIndex < fieldParentTypeDeclaration.Members.Count - 1) - { - MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration.Members[fieldDeclarationIndex + 1]; - - // It's especially important to skip members that have been rmeoved. This would otherwise fail when computing - // the final document. We only care about fixing trivia for members that will still be present after all edits. - if (fieldDeclarations.Contains(nextMember)) - { - continue; - } - - SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia(); - - // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one - if (leadingTrivia.Count > 0 && leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) - { - syntaxEditor.ReplaceNode(nextMember, (nextMember, _) => nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0))); - } - } + int fieldDeclarationIndex = fieldParentTypeDeclaration.Members.IndexOf(fieldDeclaration); + + // Check whether there is a member immediatley following the field + if (fieldDeclarationIndex == -1 || fieldDeclarationIndex >= fieldParentTypeDeclaration.Members.Count - 1) + { + continue; } + + MemberDeclarationSyntax nextMember = fieldParentTypeDeclaration.Members[fieldDeclarationIndex + 1]; + + // It's especially important to skip members that have been rmeoved. This would otherwise fail when computing + // the final document. We only care about fixing trivia for members that will still be present after all edits. + if (fieldDeclarations.Contains(nextMember)) + { + continue; + } + + SyntaxTriviaList leadingTrivia = nextMember.GetLeadingTrivia(); + + // Check whether this member has a first leading trivia that's just a blank line: we want to remove this one + if (leadingTrivia.Count == 0 || !leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia)) + { + continue; + } + + bool hasAnyPersistentPrecedingMemberDeclarations = false; + + // Last check: we only want to actually remove the end of line if there are no other members before the current + // one, that have persistend in the containing type after all edits. If that is not the case, that is, if there + // are other members before the current one, we want to keep that end of line. Otherwise, we'd end up with the + // current member being incorrectly declared right after the previous one, without a separating blank line. + for (int i = 0; i < fieldDeclarationIndex + 1; i++) + { + hasAnyPersistentPrecedingMemberDeclarations |= !fieldDeclarations.Contains(fieldParentTypeDeclaration.Members[i]); + } + + // If there's any other persistent members, stop here + if (hasAnyPersistentPrecedingMemberDeclarations) + { + continue; + } + + // Finally, we can actually remove this end of line trivia, as we're sure it's not actually intended + syntaxEditor.ReplaceNode(nextMember, (nextMember, _) => nextMember.WithLeadingTrivia(leadingTrivia.RemoveAt(0))); } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index c57f393db..2e25ec9dc 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -597,6 +597,165 @@ public partial class MyControl : Control await test.RunAsync(); } + [TestMethod] + public async Task MultipleProperties_WithLeadingPersistentMembers_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithLeadingPersistentMembers_WithXmlDocs_HandlesSpacingCorrectly() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + /// Blah + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// Blah + public static readonly DependencyProperty Name2Property = DependencyProperty.Register( + name: "Name2", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// Blah + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + /// Blah + public string? [|Name2|] + { + get => (string?)GetValue(Name2Property); + set => SetValue(Name2Property, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + /// Blah + public static readonly DependencyProperty Name1Property = DependencyProperty.Register( + name: "Name1", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// Blah + public string? Name1 + { + get => (string?)GetValue(Name1Property) ?? string.Empty; + set => SetValue(Name1Property, value); + } + + /// Blah + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name2|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + [TestMethod] [DataRow("float", "0.0F", "1.0F", "0.123F")] [DataRow("double", "0.0", "4.0", "0.123")] From f456963bd3e3eca77ac1152aed6d7758a33a2488 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 16:39:31 +0100 Subject: [PATCH 112/200] Respect accessibility of generated accessors --- .../DependencyPropertyGenerator.Execute.cs | 12 +- .../Test_DependencyPropertyGenerator.cs | 166 +++++++++++++++--- 2 files changed, 147 insertions(+), 31 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 99f7c4270..1c2208cf6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -554,8 +554,8 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) if (propertyInfo.IsLocalCachingEnabled) { writer.WriteLine($$""" - get => field; - set + {{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get => field; + {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set { On{{propertyInfo.PropertyName}}Set(ref value); @@ -602,7 +602,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) // would introduce a lot of overhead. If callers really do want to have a callback being invoked, they can implement // the one wired up to the property metadata directly. We can still invoke the ones only using the new value, though. writer.WriteLine($$""" - get + {{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get { object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property); @@ -610,7 +610,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) return __boxedValue; } - set + {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set { On{{propertyInfo.PropertyName}}Set(ref value); @@ -624,7 +624,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) { // Same as above but with the extra typed hook for both accessors writer.WriteLine($$""" - get + {{GetExpressionWithTrailingSpace(propertyInfo.GetterAccessibility)}}get { object? __boxedValue = GetValue({{propertyInfo.PropertyName}}Property); @@ -636,7 +636,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) return __unboxedValue; } - set + {{GetExpressionWithTrailingSpace(propertyInfo.SetterAccessibility)}}set { On{{propertyInfo.PropertyName}}Set(ref value); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index a84b3f2d0..f531672f5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -15,8 +15,8 @@ public partial class Test_DependencyPropertyGenerator public void SingleProperty_Int32_WithLocalCache() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -141,8 +141,8 @@ public partial int Number public void SingleProperty_Int32_WithLocalCache_WithCallback() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -321,8 +321,8 @@ file sealed class PropertyChangedUnsafeAccessors public void SingleProperty_Int32_WithLocalCache_WithDefaultValue() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -447,8 +447,8 @@ public partial int Number public void SingleProperty_Int32_WithNoCaching() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -563,8 +563,8 @@ public partial int Number public void SingleProperty_Int32_WithNoCaching_UnsetValue() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -679,8 +679,8 @@ public partial int Number public void SingleProperty_Int32_WithNoCaching_WithCallback() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -849,8 +849,8 @@ file sealed class PropertyChangedUnsafeAccessors public void SingleProperty_Int32_WithNoCaching_WithDefaultValue() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -965,8 +965,8 @@ public partial int Number public void SingleProperty_Int32_WithNoCaching_WithDefaultValue_WithCallback() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1135,8 +1135,8 @@ file sealed class PropertyChangedUnsafeAccessors public void SingleProperty_Int32_WithNoCaching_WithSharedCallback() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1305,8 +1305,8 @@ file sealed class PropertyChangedUnsafeAccessors public void SingleProperty_Int32_WithNoCaching_WithBothCallbacks() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1484,8 +1484,8 @@ file sealed class PropertyChangedUnsafeAccessors public void SingleProperty_String_WithLocalCache() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1610,8 +1610,8 @@ public partial string? Name public void SingleProperty_String_WithNoCaching() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1726,8 +1726,8 @@ public partial string? Name public void SingleProperty_String_WithNoCaching_Required() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1842,8 +1842,8 @@ public required partial string Name public void SingleProperty_String_WithNoCaching_New() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -1963,8 +1963,8 @@ partial class MyControl public void SingleProperty_String_WithNoCaching_Virtual() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -2081,8 +2081,8 @@ public virtual partial string Name public void SingleProperty_String_WithNoCaching_Override(string modifiers) { string source = $$""" - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -2198,12 +2198,128 @@ partial class MyControl CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + [TestMethod] + public void SingleProperty_String_WithNoCaching_CustomAccessibility() + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + internal partial string Name { protected get; private set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal partial string Name + { + protected get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string __unboxedValue = (string)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + private set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } + [TestMethod] public void MultipleProperties_WithNoCaching_CorrectSpacing() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -2399,8 +2515,8 @@ public partial string? LastName public void MultipleProperties_WithNoCaching_WithJustOnePropertyCallback() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -2650,8 +2766,8 @@ file sealed class PropertyChangedUnsafeAccessors public void MultipleProperties_WithNoCaching_WithSharedPropertyCallback() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -2915,8 +3031,8 @@ file sealed class PropertyChangedUnsafeAccessors public void MultipleProperties_WithNoCaching_WithMixedPropertyCallbacks() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -3194,8 +3310,8 @@ file sealed class PropertyChangedUnsafeAccessors public void MultipleProperties_WithNoCaching_WithMixedPropertyCallbacks2() { const string source = """ - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -3485,8 +3601,8 @@ file sealed class PropertyChangedUnsafeAccessors public void SingleProperty_Int32_WithNoCaching_WithDefaultValueCallback(string returnType) { string source = $$""" - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -3608,8 +3724,8 @@ public partial int Number public void SingleProperty_NullableOfInt32_WithNoCaching_WithDefaultValueCallback(string returnType) { string source = $$""" - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; namespace MyNamespace; @@ -3731,8 +3847,8 @@ public partial int? Number public void SingleProperty_String_WithNoCaching_WithDefaultValueCallback(string returnType) { string source = $$""" - using Windows.UI.Xaml; using CommunityToolkit.WinUI; + using Windows.UI.Xaml; #nullable enable @@ -3912,10 +4028,10 @@ public void SingleProperty_MultipleTypes_WithNoCaching_DefaultValueIsOptimized( string source = $$""" using System; using System.Collections.Generic; + using CommunityToolkit.WinUI; using Windows.Foundation; using Windows.Foundation.Numerics; using Windows.UI.Xaml; - using CommunityToolkit.WinUI; #nullable enable From 70d5857ee7357abf7eda4fd2425b4fc21bf749ae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 16:45:37 +0100 Subject: [PATCH 113/200] Add 'SyntaxTriviaExtensions', code tweaks --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 3 ++- .../Extensions/SyntaxTriviaExtensions.cs | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTriviaExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 27e9356ae..c1c7eff80 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -371,7 +372,7 @@ private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollection +/// Extension methods for the type. +/// +internal static class SyntaxTriviaExtensions +{ + /// + /// Deconstructs a into its value. + /// + /// The input value. + /// The resulting value for . + public static void Deconstruct(this SyntaxTrivia syntaxTrivia, out SyntaxKind syntaxKind) + { + syntaxKind = syntaxTrivia.Kind(); + } +} From 072c750fa2dc179ffbcb550f7477c9f14b0991a5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 18:30:15 +0100 Subject: [PATCH 114/200] Add 'AttributeInfo' type and associated logic --- .../Extensions/SymbolInfoExtensions.cs | 48 ++++++++ .../Extensions/SyntaxTokenExtensions.cs | 24 ++++ .../Models/AttributeInfo.cs | 105 ++++++++++++++++++ .../Models/TypedConstantInfo.Factory.cs | 92 ++++++++++++++- 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs new file mode 100644 index 000000000..fff56a90f --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -0,0 +1,48 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolInfoExtensions +{ + /// + /// Tries to get the resolved attribute type symbol from a given value. + /// + /// The value to check. + /// The resulting attribute type symbol, if correctly resolved. + /// Whether is resolved to a symbol. + /// + /// This can be used to ensure users haven't eg. spelled names incorrectly or missed a using directive. Normally, code would just + /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore + /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. + /// + public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol) + { + ISymbol? attributeSymbol = symbolInfo.Symbol; + + // If no symbol is selected and there is a single candidate symbol, use that + if (attributeSymbol is null && symbolInfo.CandidateSymbols is [ISymbol candidateSymbol]) + { + attributeSymbol = candidateSymbol; + } + + // Extract the symbol from either the current one or the containing type + if ((attributeSymbol as INamedTypeSymbol ?? attributeSymbol?.ContainingType) is not INamedTypeSymbol resultingSymbol) + { + typeSymbol = null; + + return false; + } + + typeSymbol = resultingSymbol; + + return true; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs new file mode 100644 index 000000000..0e29a31db --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/SyntaxTokenExtensions.cs @@ -0,0 +1,24 @@ +// 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.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenExtensions +{ + /// + /// Deconstructs a into its value. + /// + /// The input value. + /// The resulting value for . + public static void Deconstruct(this SyntaxToken syntaxToken, out SyntaxKind syntaxKind) + { + syntaxKind = syntaxToken.Kind(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs new file mode 100644 index 000000000..8ac269e7c --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs @@ -0,0 +1,105 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty.Models; + +/// +/// A model representing an attribute declaration. +/// +/// The type name of the attribute. +/// The values for all constructor arguments for the attribute. +/// The values for all named arguments for the attribute. +internal sealed record AttributeInfo( + string TypeName, + EquatableArray ConstructorArgumentInfo, + EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) +{ + /// + /// Creates a new instance from a given syntax node. + /// + /// The symbol for the attribute type. + /// The instance for the current run. + /// The sequence of instances to process. + /// The cancellation token for the current operation. + /// The resulting instance, if available + /// Whether a resulting instance could be created. + public static bool TryCreate( + INamedTypeSymbol typeSymbol, + SemanticModel semanticModel, + IEnumerable arguments, + CancellationToken token, + [NotNullWhen(true)] out AttributeInfo? info) + { + string typeName = typeSymbol.GetFullyQualifiedName(); + + using ImmutableArrayBuilder constructorArguments = new(); + using ImmutableArrayBuilder<(string, TypedConstantInfo)> namedArguments = new(); + + foreach (AttributeArgumentSyntax argument in arguments) + { + // The attribute expression has to have an available operation to extract information from + if (semanticModel.GetOperation(argument.Expression, token) is not IOperation operation) + { + continue; + } + + // Try to get the info for the current argument + if (!TypedConstantInfo.TryCreate(operation, semanticModel, argument.Expression, token, out TypedConstantInfo? argumentInfo)) + { + info = null; + + return false; + } + + // Try to get the identifier name if the current expression is a named argument expression. If it + // isn't, then the expression is a normal attribute constructor argument, so no extra work is needed. + if (argument.NameEquals is { Name.Identifier.ValueText: string argumentName }) + { + namedArguments.Add((argumentName, argumentInfo)); + } + else + { + constructorArguments.Add(argumentInfo); + } + } + + info = new AttributeInfo( + typeName, + constructorArguments.ToImmutable(), + namedArguments.ToImmutable()); + + return true; + } + + /// + public override string ToString() + { + // Gather the constructor arguments + IEnumerable arguments = + ConstructorArgumentInfo + .Select(static arg => AttributeArgument(ParseExpression(arg.ToString()))); + + // Gather the named arguments + IEnumerable namedArguments = + NamedArgumentInfo.Select(static arg => + AttributeArgument(ParseExpression(arg.Value.ToString())) + .WithNameEquals(NameEquals(IdentifierName(arg.Name)))); + + // Get the attribute to emit + AttributeSyntax attributeDeclaration = Attribute(IdentifierName(TypeName), AttributeArgumentList(SeparatedList(arguments.Concat(namedArguments)))); + + return attributeDeclaration.NormalizeWhitespace(eol: "\n").ToFullString(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs index 8c49ad307..5e37311c8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -3,11 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Helpers; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; namespace CommunityToolkit.GeneratedDependencyProperty.Models; @@ -65,11 +70,12 @@ public static TypedConstantInfo Create(TypedConstant arg) } /// - /// Creates a new instance from a given value. + /// Creates a new instance from a given instance. /// - /// The input value. + /// The input instance. /// A instance representing . /// Thrown if the input argument is not valid. + /// This method only supports constant values. public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out TypedConstantInfo? result) { // Validate that we do have some constant value @@ -113,4 +119,86 @@ public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out Typed return true; } + + /// + /// Creates a new instance from a given instance. + /// + /// The input instance. + /// The that was used to retrieve . + /// The that was retrieved from. + /// The cancellation token for the current operation. + /// The resulting instance, if available. + /// Whether a resulting instance could be created. + /// Thrown if the input argument is not valid. + public static bool TryCreate( + IOperation operation, + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken token, + [NotNullWhen(true)] out TypedConstantInfo? result) + { + if (TryCreate(operation, out result)) + { + return true; + } + + if (operation is ITypeOfOperation typeOfOperation) + { + result = new Type(typeOfOperation.TypeOperand.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + + return true; + } + + if (operation is IArrayCreationOperation) + { + string? elementTypeName = ((IArrayTypeSymbol?)operation.Type)?.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + // If the element type is not available (since the attribute wasn't checked), just default to object + elementTypeName ??= "object"; + + // Handle all possible ways of initializing arrays in attributes + IEnumerable? arrayElementExpressions = expression switch + { + ImplicitArrayCreationExpressionSyntax { Initializer.Expressions: { } expressions } => expressions, + ArrayCreationExpressionSyntax { Initializer.Expressions: { } expressions } => expressions, + CollectionExpressionSyntax { Elements: { } elements } => elements.OfType().Select(static element => element.Expression), + _ => null + }; + + // No element expressions found, just return an empty array + if (arrayElementExpressions is null) + { + result = new Array(elementTypeName, ImmutableArray.Empty); + + return true; + } + + using ImmutableArrayBuilder items = new(); + + // Enumerate all array elements and extract serialized info for them + foreach (ExpressionSyntax elementExpressions in arrayElementExpressions) + { + if (semanticModel.GetOperation(elementExpressions, token) is not IOperation initializationOperation) + { + goto Failure; + } + + if (!TryCreate(initializationOperation, semanticModel, elementExpressions, token, out TypedConstantInfo? elementInfo)) + { + goto Failure; + } + + items.Add(elementInfo); + } + + result = new Array(elementTypeName, items.ToImmutable()); + + return true; + } + + Failure: + result = null; + + return false; + } } From 9af0344ea75dec821e65684c60ca139fdb5477d3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 18:30:39 +0100 Subject: [PATCH 115/200] Gather forwarded attributes for generated fields --- .../DependencyPropertyGenerator.Execute.cs | 75 +++++++++++++++++++ .../DependencyPropertyGenerator.cs | 12 ++- .../Models/DependencyPropertyInfo.cs | 4 +- 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 1c2208cf6..e5184ec1d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; @@ -393,6 +394,73 @@ public static bool IsSharedPropertyChangedCallbackImplemented(IPropertySymbol pr return false; } + /// + /// Gathers all forwarded attributes for the generated property. + /// + ///The input node. + /// The instance for the current run. + /// The collection of forwarded attributes to add new ones to. + /// The current collection of gathered diagnostics. + /// The cancellation token for the current operation. + public static void GetForwardedAttributes( + PropertyDeclarationSyntax node, + SemanticModel semanticModel, + CancellationToken token, + out ImmutableArray staticFieldAttributes) + { + using ImmutableArrayBuilder builder = new(); + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in node.AttributeLists) + { + // Only look for the 'static' attribute target, which can be used to target the generated 'DependencyProperty' static field. + // Roslyn will normally emit a 'CS0657' warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support '[GeneratedDependencyProperty]'. We can't use 'field' + // as trigger, as that's used for the actual 'field' keyword, when local caching is enabled. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.StaticKeyword)) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the 'AttributeData' as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the 'AttributeInfo' model. After all, attributes only support constant values, 'typeof(T)' + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? []; + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + { + continue; + } + + builder.Add(attributeInfo); + } + } + + staticFieldAttributes = builder.ToImmutable(); + } + /// /// Writes all implementations of partial dependency property declarations. /// @@ -514,6 +582,13 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) /// """, isMultiline: true); writer.WriteGeneratedAttributes(GeneratorName, includeNonUserCodeAttributes: false); + + // Write any forwarded attributes + foreach (AttributeInfo attributeInfo in propertyInfo.StaticFieldAttributes) + { + writer.WriteLine(attributeInfo.ToString()); + } + writer.Write($$""" public static readonly global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}}.Register( name: "{{propertyInfo.PropertyName}}", diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index fb9edc8b3..4abd428e5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -121,6 +121,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); + // Get any forwarded attributes + Execute.GetForwardedAttributes( + (PropertyDeclarationSyntax)context.TargetNode, + context.SemanticModel, + token, + out ImmutableArray staticFieldAttributes); + + token.ThrowIfCancellationRequested(); + // Finally, get the hierarchy too HierarchyInfo hierarchyInfo = HierarchyInfo.From(propertySymbol.ContainingType); @@ -141,7 +150,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, IsNet8OrGreater: isNet8OrGreater, - UseWindowsUIXaml: useWindowsUIXaml); + UseWindowsUIXaml: useWindowsUIXaml, + StaticFieldAttributes: staticFieldAttributes); }) .WithTrackingName(WellKnownTrackingNames.Execute) .Where(static item => item is not null)!; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs index 428d9bf3d..a53c35f6f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -25,6 +25,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// Indicates whether the WinRT-based shared property changed callback is implemented. /// Indicates whether the current target is .NET 8 or greater. /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. +/// The attributes to emit on the generated static field, if any. internal sealed record DependencyPropertyInfo( HierarchyInfo Hierarchy, string PropertyName, @@ -40,4 +41,5 @@ internal sealed record DependencyPropertyInfo( bool IsPropertyChangedCallbackImplemented, bool IsSharedPropertyChangedCallbackImplemented, bool IsNet8OrGreater, - bool UseWindowsUIXaml); + bool UseWindowsUIXaml, + EquatableArray StaticFieldAttributes); From e56f8aef031c4c7a8ff010b903051d498d1c3009 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 18:46:43 +0100 Subject: [PATCH 116/200] Fix attributes generation, add unit tests --- .../DependencyPropertyGenerator.Execute.cs | 2 +- .../Models/TypedConstantInfo.Factory.cs | 2 +- .../Test_DependencyPropertyGenerator.cs | 136 ++++++++++++++++++ 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index e5184ec1d..c310fe84d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -586,7 +586,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) // Write any forwarded attributes foreach (AttributeInfo attributeInfo in propertyInfo.StaticFieldAttributes) { - writer.WriteLine(attributeInfo.ToString()); + writer.WriteLine($"[{attributeInfo}]"); } writer.Write($$""" diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs index 5e37311c8..b3c51677b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.Factory.cs @@ -149,7 +149,7 @@ public static bool TryCreate( return true; } - if (operation is IArrayCreationOperation) + if (operation is (IArrayCreationOperation or ICollectionExpressionOperation) and { Type: null or IArrayTypeSymbol }) { string? elementTypeName = ((IArrayTypeSymbol?)operation.Type)?.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index f531672f5..88b0021fb 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4145,4 +4145,140 @@ public partial {{propertyType}} Name CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.CSharp13); } + + [TestMethod] + [DataRow("A", "global::MyNamespace.AAttribute()")] + [DataRow("B(42, 10)", "global::MyNamespace.BAttribute(42, 10)")] + [DataRow("""C(10, X = "Test", Y = 42)""", """global::MyNamespace.CAttribute(10, X = "Test", Y = 42)""")] + [DataRow("D(Foo.B, typeof(string), new[] { 1, 2, 3 })", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + [DataRow("D(Foo.B, typeof(string), new int[] { 1, 2, 3 })", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + [DataRow("D(Foo.B, typeof(string), [1, 2, 3])", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + public void SingleProperty_String_WithNoCaching_WithForwardedAttribute( + string attributeDefinition, + string attributeForwarding) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + [static: {{attributeDefinition}}] + public partial string? Name { get; set; } + } + + public class AAttribute : Attribute; + public class BAttribute(int X, int Y) : Attribute; + public class CAttribute(int Z) : Attribute + { + public string X { get; set; } + public int Y { get; set; } + } + public class DAttribute(Foo X, Type Y, int[] Z) : Attribute; + public enum Foo { A, B } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [{{attributeForwarding}}] + public static readonly global::Windows.UI.Xaml.DependencyProperty NameProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get + { + object? __boxedValue = GetValue(NameProperty); + + OnNameGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNameGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNameSet(ref value); + + object? __boxedValue = value; + + OnNameSet(ref __boxedValue); + + SetValue(NameProperty, __boxedValue); + + OnNameChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNameChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNamePropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } } From f75dcd5f6bd79aec6b3098e0e00f489bcd90148c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 18:55:45 +0100 Subject: [PATCH 117/200] Update analyzer for attributes, add unit tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 11 ++- .../Test_Analyzers.cs | 70 +++++++++++++++++++ .../Test_DependencyPropertyGenerator.cs | 1 + 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 10881e9f5..64d5a18fa 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -11,6 +11,7 @@ using CommunityToolkit.GeneratedDependencyProperty.Helpers; using CommunityToolkit.GeneratedDependencyProperty.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -479,14 +480,20 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } // Find the parent field for the operation (we're guaranteed to only fine one) - if (context.Operation.Syntax.FirstAncestor()?.GetLocation() is not Location fieldLocation) + if (context.Operation.Syntax.FirstAncestor() is not { } fieldDeclaration) + { + return; + } + + // Ensure that the field only has attributes we can forward, or not attributes at all + if (fieldDeclaration.AttributeLists.Any(static list => list.Target is { Identifier: not SyntaxToken(SyntaxKind.FieldKeyword) })) { return; } fieldFlags.PropertyName = propertyName; fieldFlags.PropertyType = propertyTypeSymbol; - fieldFlags.FieldLocation = fieldLocation; + fieldFlags.FieldLocation = fieldDeclaration.GetLocation(); }, OperationKind.FieldInitializer); // Finally, we can consume this information when we finish processing the symbol diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index b073ab716..3ec48596e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1417,6 +1417,40 @@ public enum MyEnum { A, B, C } await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithInvalidAttribute_DoesNotWarn() + { + const string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [property: Test] + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("string", "string")] [DataRow("string", "string?")] @@ -1474,6 +1508,42 @@ public class MyClass { } await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("[Test]")] + [DataRow("[field: Test]")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithAttributeOnField_Warns(string attributeDeclaration) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + {{attributeDeclaration}} + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? {|WCTDP0017:Name|} + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("string", "string", "null")] [DataRow("string", "string", "default(string)")] diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 88b0021fb..054bb8966 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4158,6 +4158,7 @@ public void SingleProperty_String_WithNoCaching_WithForwardedAttribute( string attributeForwarding) { string source = $$""" + using System; using CommunityToolkit.WinUI; using Windows.UI.Xaml; From e662f7635b71b1bbadc63f3417c7453b44da23fc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 19:07:02 +0100 Subject: [PATCH 118/200] Update code fixer, add unit tests --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 6 ++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index c1c7eff80..502f297e6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -285,6 +285,12 @@ private static void ConvertToPartialProperty( attributeLists = attributeLists.Add(generatedDependencyPropertyAttributeList); } + // Append any attributes we want to forward (any attributes on the field, they've already been validated) + foreach (AttributeListSyntax fieldAttributeList in fieldDeclaration.AttributeLists) + { + attributeLists = attributeLists.Add(fieldAttributeList.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.StaticKeyword)))); + } + // Get a new property that is partial and with semicolon token accessors return propertyDeclaration diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 2e25ec9dc..b2a8860be 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -374,6 +374,77 @@ public partial class MyControl : Control await test.RunAsync(); } + [TestMethod] + [DataRow("[A]", "[static: A]")] + [DataRow("""[Test(42, "Hello")]""", """[static: Test(42, "Hello")]""")] + [DataRow("""[field: Test(42, "Hello")]""", """[static: Test(42, "Hello")]""")] + [DataRow("""[A, Test(42, "Hello")]""", """[static: A, Test(42, "Hello")]""")] + [DataRow(""" + [A] + [Test(42, "Hello")] + """, """ + [static: A] + [static: Test(42, "Hello")] + """)] + public async Task SimpleProperty_WithForwardedAttributes( + string attributeDefinition, + string attributeForwarding) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + {{attributeDefinition}} + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: nameof(Name), + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class AAttribute : Attribute; + public class TestAttribute(int X, string Y) : Attribute; + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + {{attributeForwarding}} + public partial string? {|CS9248:Name|} { get; set; } + } + + public class AAttribute : Attribute; + public class TestAttribute(int X, string Y) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + [TestMethod] public async Task MultipleProperties_HandlesSpacingCorrectly() { From e617cf0423cd5ed4d0e4f5adf054f1c4c8a46cbc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 19:17:16 +0100 Subject: [PATCH 119/200] Add 'StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor' --- .../Diagnostics/SuppressionDescriptors.cs | 21 ++++++ ...DependencyPropertyDeclarationSuppressor.cs | 64 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs new file mode 100644 index 000000000..9122fc28e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -0,0 +1,21 @@ +// 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.GeneratedDependencyProperty.Diagnostics; + +/// +/// A container for all instances for suppressed diagnostics by analyzers in this project. +/// +internal static class SuppressionDescriptors +{ + /// + /// Gets a for a property using [GeneratedDependencyProperty] with an attribute list targeting the 'static' keyword. + /// + public static readonly SuppressionDescriptor StaticPropertyAttributeListForGeneratedDependencyPropertyDeclaration = new( + id: "WCTDPSPR0001", + suppressedDiagnosticId: "CS0658", + justification: "Properties using [GeneratedDependencyProperty] can use [static:] attribute lists to forward attributes to the generated dependency property fields."); +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs new file mode 100644 index 000000000..f85778f60 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs @@ -0,0 +1,64 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.SuppressionDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// +/// A diagnostic suppressor to suppress CS0658 warnings for properties with [GeneratedDependencyProperty] using a [static:] attribute list. +/// +/// +/// That is, this diagnostic suppressor will suppress the following diagnostic: +/// +/// public partial class MyControl : Control +/// { +/// [GeneratedDependencyProperty] +/// [static: JsonIgnore] +/// public partial string? Name { get; set; } +/// } +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor : DiagnosticSuppressor +{ + /// + public override ImmutableArray SupportedSuppressions { get; } = [StaticPropertyAttributeListForGeneratedDependencyPropertyDeclaration]; + + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + foreach (Diagnostic diagnostic in context.ReportedDiagnostics) + { + SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + + // Check that the target is effectively [static:] over a property declaration, which is the only case we are interested in + if (syntaxNode is AttributeTargetSpecifierSyntax { Parent: PropertyDeclarationSyntax propertyDeclaration, Identifier: SyntaxToken(SyntaxKind.StaticKeyword) }) + { + SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); + + // Get the property symbol from the property declaration + ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(propertyDeclaration, context.CancellationToken); + + // Check if the property is using [GeneratedDependencyProperty], in which case we should suppress the warning + if (declaredSymbol is IPropertySymbol propertySymbol && propertySymbol.HasAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols)) + { + context.ReportSuppression(Suppression.Create(StaticPropertyAttributeListForGeneratedDependencyPropertyDeclaration, diagnostic)); + } + } + } + } +} From 2a5f0fe55010b468dd9f2cbfeca7446e1955c0b5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 21:35:30 +0100 Subject: [PATCH 120/200] Add analyzer for forwarded attributes, and tests --- .../AnalyzerReleases.Shipped.md | 2 + .../DependencyPropertyGenerator.Execute.cs | 2 +- ...tyForwardedAttributeDeclarationAnalyzer.cs | 106 ++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 26 +++ .../Test_Analyzers.cs | 192 ++++++++++++++++++ 5 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index e547e0ee3..55f8ed3fc 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -24,3 +24,5 @@ WCTDP0014 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0015 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0016 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | +WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index c310fe84d..1773b993f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -414,7 +414,7 @@ public static void GetForwardedAttributes( foreach (AttributeListSyntax attributeList in node.AttributeLists) { // Only look for the 'static' attribute target, which can be used to target the generated 'DependencyProperty' static field. - // Roslyn will normally emit a 'CS0657' warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // Roslyn will normally emit a 'CS0658' warning (invalid target), but that is automatically suppressed by a dedicated diagnostic // suppressor that recognizes uses of this target specifically to support '[GeneratedDependencyProperty]'. We can't use 'field' // as trigger, as that's used for the actual 'field' keyword, when local caching is enabled. if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.StaticKeyword)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs new file mode 100644 index 000000000..0824c140e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs @@ -0,0 +1,106 @@ +// 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.Collections.Immutable; +using CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using CommunityToolkit.GeneratedDependencyProperty.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates an error when a property with [GeneratedDependencyProperty] is using invalid forwarded attributes. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyForwardedAttributeDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + InvalidDependencyPropertyTargetedAttributeType, + InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Get the 'DependencyObject' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyObject(useWindowsUIXaml)) is not { } dependencyObjectSymbol) + { + return; + } + + context.RegisterSymbolStartAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + context.RegisterSyntaxNodeAction(context => + { + foreach (AttributeListSyntax attributeList in ((PropertyDeclarationSyntax)context.Node).AttributeLists) + { + // Only target attributes that would be forwarded, ignore all others + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.StaticKeyword)) + { + continue; + } + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Emit a diagnostic (and stop here for this attribute) if we can't resolve the symbol for the attribute to forward + if (!context.SemanticModel.GetSymbolInfo(attribute, context.CancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDependencyPropertyTargetedAttributeType, + attribute.GetLocation(), + propertySymbol, + attribute.Name.ToFullString())); + + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? []; + + // Also emit a diagnostic if we fail to create the object model for the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, context.SemanticModel, attributeArguments, context.CancellationToken, out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression, + attribute.GetLocation(), + propertySymbol, + attributeTypeSymbol)); + } + } + } + }, SyntaxKind.PropertyDeclaration); + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 61138c936..d86c378e5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -236,4 +236,30 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Manual properties should be converted to partial properties using [GeneratedDependencyProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)". + /// + public static readonly DiagnosticDescriptor InvalidDependencyPropertyTargetedAttributeType = new( + id: "WCTDP0018", + title: "Invalid dependency property targeted attribute type", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must correctly be resolved to valid types.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)". + /// + public static readonly DiagnosticDescriptor InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression = new( + id: "WCTDP0019", + title: "Invalid dependency property targeted attribute expression", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must have arguments using supported expressions.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 3ec48596e..30b0a2fb2 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.GeneratedDependencyProperty.Tests; @@ -1625,4 +1626,195 @@ public class MyClass { } await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoForwardedAttribute_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_ValidForwardedAttribute_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_TypoInAttributeName_NotTargetingStatic_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [Testt] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13, + [ + // /0/Test0.cs(9,6): error CS0246: The type or namespace name 'Testt' could not be found (are you missing a using directive or an assembly reference?) + DiagnosticResult.CompilerError("CS0246").WithSpan(9, 6, 9, 11).WithArguments("Testt"), + + // /0/Test0.cs(9,6): error CS0246: The type or namespace name 'TesttAttribute' could not be found (are you missing a using directive or an assembly reference?) + DiagnosticResult.CompilerError("CS0246").WithSpan(9, 6, 9, 11).WithArguments("TesttAttribute") + ]); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_MissingUsingDirective_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp + { + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDP0018:Test|}] + public string? Name { get; set; } + } + } + + namespace MyAttributes + { + public class TestAttribute : Attribute; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_TypoInAttributeName_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDP0018:Testt|}] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/683 + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_InvalidExpressionOnFieldAttribute_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDP0019:Test(TestAttribute.M)|}] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute + { + public static string M => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_InvalidExpressionOnFieldAttribute_WithExistingParameter_Warns() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + public class MyControl : Control + { + [GeneratedDependencyProperty] + [static: {|WCTDP0019:Test(TestAttribute.M)|}] + public string? Name { get; set; } + } + + public class TestAttribute(string P) : Attribute + { + public static string M => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From 81e2ad30761265ed1896cd121855974ae414be72 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 21:37:37 +0100 Subject: [PATCH 121/200] Fix outdated comments --- .../AppServices/src/CommunityToolkit.AppServices.csproj | 5 +---- ...CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj | 5 +---- .../Notifications/src/CommunityToolkit.Notifications.csproj | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index eb38fa4fd..ef7579bf9 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -10,10 +10,7 @@ false false - + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj index 8b4d7ad61..de4d74d91 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.csproj @@ -17,10 +17,7 @@ false false - + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj index 4a27c5737..6db92daf4 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -17,10 +17,7 @@ false false - + uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; From 8a048a82b020580d8e40838a8486529e77d73820 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 23:24:54 +0100 Subject: [PATCH 122/200] Strip trivia from forwarded attributes --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 5 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 91 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 502f297e6..8907683a8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -285,10 +285,11 @@ private static void ConvertToPartialProperty( attributeLists = attributeLists.Add(generatedDependencyPropertyAttributeList); } - // Append any attributes we want to forward (any attributes on the field, they've already been validated) + // Append any attributes we want to forward (any attributes on the field, they've already been validated). + // We also need to strip all trivia, to avoid accidentally carrying over XML docs from the field declaration. foreach (AttributeListSyntax fieldAttributeList in fieldDeclaration.AttributeLists) { - attributeLists = attributeLists.Add(fieldAttributeList.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.StaticKeyword)))); + attributeLists = attributeLists.Add(fieldAttributeList.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.StaticKeyword))).WithoutTrivia()); } // Get a new property that is partial and with semicolon token accessors diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b2a8860be..0d2cf9c09 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -955,4 +955,95 @@ public partial class MyControl : Control await test.RunAsync(); } + + [TestMethod] + public async Task MultipleProperties_WithXmlDocs_WithForwardedAttributes_TrimsAttributTrivia() + { + const string original = """ + using System; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ExpressionProperty = DependencyProperty.Register( + nameof(Expression), + typeof(string), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + [Test(42, "Test")] + public static readonly DependencyProperty InputProperty = DependencyProperty.Register( + nameof(Input), + typeof(object), + typeof(MyObject), + null); + + /// + /// Blah. + /// + public string? [|Expression|] + { + get => (string?)GetValue(ExpressionProperty); + set => SetValue(ExpressionProperty, value); + } + + /// + /// Blah. + /// + public object? [|Input|] + { + get => (object?)GetValue(InputProperty); + set => SetValue(InputProperty, value); + } + } + + public class TestAttribute(int X, string Y) : Attribute; + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Blah. + /// + [GeneratedDependencyProperty] + public partial string? {|CS9248:Expression|} { get; set; } + + /// + /// Blah. + /// + [GeneratedDependencyProperty] + [static: Test(42, "Test")] + public partial object? {|CS9248:Input|} { get; set; } + } + + public class TestAttribute(int X, string Y) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From c36129fac3039efb23b7c72c1f593ee5a79a59ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 23:41:28 +0100 Subject: [PATCH 123/200] Add 'CSharpSuppressorTest' --- .../CSharpSuppressorTest{TSuppressor}.cs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs new file mode 100644 index 000000000..bae55d74b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpSuppressorTest{TSuppressor}.cs @@ -0,0 +1,160 @@ +// 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.WinUI; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.Foundation; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; + +/// +/// A custom for testing diagnostic suppressors. +/// +/// The type of the suppressor to test. +// Adapted from https://github.com/ImmediatePlatform/Immediate.Validations +public sealed class CSharpSuppressorTest : CSharpAnalyzerTest + where TSuppressor : DiagnosticSuppressor, new() +{ + /// + /// The list of analyzers to run on the input code. + /// + private readonly List _analyzers = []; + + /// + /// Whether to enable unsafe blocks. + /// + private readonly bool _allowUnsafeBlocks; + + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion _languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The source code to analyze. + /// Whether to enable unsafe blocks. + /// The language version to use to run the test. + public CSharpSuppressorTest( + string source, + bool allowUnsafeBlocks = true, + LanguageVersion languageVersion = LanguageVersion.CSharp13) + { + _allowUnsafeBlocks = allowUnsafeBlocks; + _languageVersion = languageVersion; + + TestCode = source; + TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + } + + /// + protected override IEnumerable GetDiagnosticAnalyzers() + { + return base.GetDiagnosticAnalyzers().Concat(_analyzers); + } + + /// + protected override CompilationOptions CreateCompilationOptions() + { + return new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: _allowUnsafeBlocks); + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(_languageVersion, DocumentationMode.Diagnose); + } + + /// + /// Adds a new analyzer to the set of analyzers to run on the input code. + /// + /// The type of analyzer to activate. + /// The current test instance. + public CSharpSuppressorTest WithAnalyzer( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] string assemblyQualifiedTypeName) + { + _analyzers.Add((DiagnosticAnalyzer)Activator.CreateInstance(Type.GetType(assemblyQualifiedTypeName)!)!); + + return this; + } + + /// + /// Specifies the diagnostics to enable. + /// + /// The set of diagnostics. + /// The current test instance. + public CSharpSuppressorTest WithSpecificDiagnostics(params DiagnosticResult[] diagnostics) + { + ImmutableDictionary diagnosticOptions = diagnostics.ToImmutableDictionary( + descriptor => descriptor.Id, + descriptor => descriptor.Severity.ToReportDiagnostic()); + + // Transform to enable the diagnostics + Solution EnableDiagnostics(Solution solution, ProjectId projectId) + { + CompilationOptions options = + solution.GetProject(projectId)?.CompilationOptions + ?? throw new InvalidOperationException("Compilation options missing."); + + return solution.WithProjectCompilationOptions( + projectId, + options.WithSpecificDiagnosticOptions(diagnosticOptions)); + } + + SolutionTransforms.Clear(); + SolutionTransforms.Add(EnableDiagnostics); + + return this; + } + + /// + /// Specifies the diagnostics that should be produced. + /// + /// The set of diagnostics. + /// The current test instance. + public CSharpSuppressorTest WithExpectedDiagnosticsResults(params DiagnosticResult[] diagnostics) + { + ExpectedDiagnostics.AddRange(diagnostics); + + return this; + } +} + +/// +/// Extensions for . +/// +file static class DiagnosticSeverityExtensions +{ + /// + /// Converts a value into a one. + /// + public static ReportDiagnostic ToReportDiagnostic(this DiagnosticSeverity severity) + { + return severity switch + { + DiagnosticSeverity.Hidden => ReportDiagnostic.Hidden, + DiagnosticSeverity.Info => ReportDiagnostic.Info, + DiagnosticSeverity.Warning => ReportDiagnostic.Warn, + DiagnosticSeverity.Error => ReportDiagnostic.Error, + _ => throw new InvalidEnumArgumentException(nameof(severity), (int)severity, typeof(DiagnosticSeverity)), + }; + } +} From c372d84423a2303798a96bf40e14be50acf376ab Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 27 Dec 2024 23:41:40 +0100 Subject: [PATCH 124/200] Fix diagnostic suppressor, add tests --- ...DependencyPropertyDeclarationSuppressor.cs | 2 +- .../Test_DiagnosticSuppressors.cs | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs index f85778f60..ce3875685 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Suppressors/StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor.cs @@ -46,7 +46,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); // Check that the target is effectively [static:] over a property declaration, which is the only case we are interested in - if (syntaxNode is AttributeTargetSpecifierSyntax { Parent: PropertyDeclarationSyntax propertyDeclaration, Identifier: SyntaxToken(SyntaxKind.StaticKeyword) }) + if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: PropertyDeclarationSyntax propertyDeclaration, Identifier: SyntaxToken(SyntaxKind.StaticKeyword) }) { SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs new file mode 100644 index 000000000..f52ab2a27 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DiagnosticSuppressors.cs @@ -0,0 +1,84 @@ +// 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.Threading.Tasks; +using CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_DiagnosticSuppressors +{ + private static readonly DiagnosticResult CS0658 = DiagnosticResult.CompilerWarning("CS0658"); + + [TestMethod] + [DataRow("get")] + [DataRow("with")] + [DataRow("readonly")] + [DataRow("propdp")] + public async Task StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor_OtherTarget_NotSuppressed(string target) + { + await new CSharpSuppressorTest( + $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + [{{target}}: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """) + .WithSpecificDiagnostics(CS0658) + .RunAsync(); + } + + [TestMethod] + public async Task StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor_NoTriggerAttribute_NotSuppressed() + { + await new CSharpSuppressorTest( + """ + using System; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [static: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """) + .WithSpecificDiagnostics(CS0658) + .RunAsync(); + } + + [TestMethod] + public async Task StaticAttributeListTargetOnGeneratedDependencyPropertyDeclarationSuppressor_ValidUse_Suppressed() + { + await new CSharpSuppressorTest( + """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + [static: Test] + public string? Name { get; set; } + } + + public class TestAttribute : Attribute; + """) + .WithSpecificDiagnostics(CS0658) + .RunAsync(); + } +} From e37d7dcc111626006a0f5bb1c274d734b229cfb4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 01:13:18 +0100 Subject: [PATCH 125/200] Add interleaved non-fixable properties, add tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 2 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 208 ++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 64d5a18fa..ee855ca3b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -531,7 +531,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // We are intentionally not handling combinations of nullable value types here. if (!SymbolEqualityComparer.Default.Equals(fieldFlags.PropertyType, pair.Key.Type)) { - return; + continue; } // Finally, check whether the field was valid (if so, we will have a valid location) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 0d2cf9c09..5e4f56916 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -1046,4 +1046,212 @@ public class TestAttribute(int X, string Y) : Attribute; await test.RunAsync(); } + + // Using 'object' for dependency properties is sometimes needed to work around an 'IReference' issue in some binding scenarios + [TestMethod] + public async Task MultipleProperties_WithInterspersedNonFixableProprty_HandlesAllPossibleProperties() + { + const string original = """ + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DisableAnimationProperty = DependencyProperty.Register( + nameof(DisableAnimation), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( + nameof(HorizontalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsHorizontalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsHorizontalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsVerticalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsVerticalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetScrollViewerProperty = DependencyProperty.Register( + nameof(TargetScrollViewer), + typeof(ScrollViewer), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( + nameof(VerticalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + public bool [|DisableAnimation|] + { + get => (bool)GetValue(DisableAnimationProperty); + set => SetValue(DisableAnimationProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + public double? HorizontalOffset + { + get => (double?)GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + public bool [|IsHorizontalOffsetRelative|] + { + get => (bool)GetValue(IsHorizontalOffsetRelativeProperty); + set => SetValue(IsHorizontalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + public bool [|IsVerticalOffsetRelative|] + { + get => (bool)GetValue(IsVerticalOffsetRelativeProperty); + set => SetValue(IsVerticalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets the target . + /// + public ScrollViewer? [|TargetScrollViewer|] + { + get => (ScrollViewer?)GetValue(TargetScrollViewerProperty); + set => SetValue(TargetScrollViewerProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + public double? VerticalOffset + { + get => (double?)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( + nameof(HorizontalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( + nameof(VerticalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:DisableAnimation|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + public double? HorizontalOffset + { + get => (double?)GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsHorizontalOffsetRelative|} { get; set; } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsVerticalOffsetRelative|} { get; set; } + + /// + /// Gets or sets the target . + /// + [GeneratedDependencyProperty] + public partial ScrollViewer? {|CS9248:TargetScrollViewer|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + public double? VerticalOffset + { + get => (double?)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From 21f36847e770e4d28664a08ea175b4f5c5dabbc7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 01:23:31 +0100 Subject: [PATCH 126/200] Add 'partial' for nested types too, add tests --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 17 +++--- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 8907683a8..5197300bb 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -319,14 +319,17 @@ private static void ConvertToPartialProperty( // Also remove the field declaration (it'll be generated now) syntaxEditor.RemoveNode(fieldDeclaration); - // Find the parent type for the property - TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; - - // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration). - // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. - if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + // Find the parent type for the property (we need to do this for all ancestor types, as the type might be bested) + for (TypeDeclarationSyntax? typeDeclaration = propertyDeclaration.FirstAncestor(); + typeDeclaration is not null; + typeDeclaration = typeDeclaration.FirstAncestor()) { - syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration). + // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + } } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 5e4f56916..b18a8db7c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -1254,4 +1254,60 @@ public double? VerticalOffset await test.RunAsync(); } + + [TestMethod] + public async Task SimpleProperty_NestedType_AddsAllRequiredPartialModifiers() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public class MyNestedObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyNestedObject), + typeMetadata: null); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public partial class MyNestedObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From 9dc67109aa9af32801bd1fe800bdeadc6171d3c0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 12:28:04 +0100 Subject: [PATCH 127/200] Fix codegen for 'bool' default values --- .../Extensions/IOperationExtensions.cs | 5 +- .../Extensions/WinRTExtensions.cs | 5 +- .../Test_DependencyPropertyGenerator.cs | 116 ++++++++++++++++++ 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs index a6d7ba672..fe9bc1a2e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs @@ -36,13 +36,14 @@ public static bool IsConstantValueDefault(this IOperation operation) return true; } - // Manually match for known primitive types + // Manually match for known primitive types (this should be kept in sync with 'IsWellKnownWinRTProjectedValueType') return (operation.Type.SpecialType, operation.ConstantValue.Value) switch { (SpecialType.System_Byte, default(byte)) or - (SpecialType.System_Char, default(char)) or + (SpecialType.System_SByte, default(sbyte)) or (SpecialType.System_Int16, default(short)) or (SpecialType.System_UInt16, default(ushort)) or + (SpecialType.System_Char, default(char)) or (SpecialType.System_Int32, default(int)) or (SpecialType.System_UInt32, default(uint)) or (SpecialType.System_Int64, default(long)) or diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs index 738cfac37..aa8776155 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -57,15 +57,16 @@ public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, b // Lastly, special case the well known primitive types if (symbol.SpecialType is - SpecialType.System_Int32 or SpecialType.System_Byte or SpecialType.System_SByte or SpecialType.System_Int16 or SpecialType.System_UInt16 or + SpecialType.System_Char or + SpecialType.System_Int32 or SpecialType.System_UInt32 or SpecialType.System_Int64 or SpecialType.System_UInt64 or - SpecialType.System_Char or + SpecialType.System_Boolean or SpecialType.System_Single or SpecialType.System_Double) { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 054bb8966..0d1d2b163 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4282,4 +4282,120 @@ public partial string? Name CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + + [TestMethod] + public void SingleProperty_Bool_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty] + public partial bool IsSelected { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty IsSelectedProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "IsSelected", + propertyType: typeof(bool), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial bool IsSelected + { + get + { + object? __boxedValue = GetValue(IsSelectedProperty); + + OnIsSelectedGet(ref __boxedValue); + + bool __unboxedValue = (bool)__boxedValue; + + OnIsSelectedGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnIsSelectedSet(ref value); + + object? __boxedValue = value; + + OnIsSelectedSet(ref __boxedValue); + + SetValue(IsSelectedProperty, __boxedValue); + + OnIsSelectedChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref bool propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref bool propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedChanged(bool newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } } From 109d34617cfd7102c470b86c9986440996b3fb8a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 12:35:37 +0100 Subject: [PATCH 128/200] Handle explicit 'null' callbacks in metadata --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 8 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 112 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index ee855ca3b..950b9c80b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -409,7 +409,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla else { // Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type - if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument] } objectCreationOperation) + if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument, ..] } objectCreationOperation) { return; } @@ -420,6 +420,12 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } + // If we have a second argument, a 'null' literal is the only supported value for it + if (objectCreationOperation.Arguments is not ([_] or [_, { Value.ConstantValue: { HasValue: true, Value: null } }])) + { + return; + } + // The argument should be a conversion operation (boxing) if (defaultValueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b18a8db7c..b338d875b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -1310,4 +1310,116 @@ public partial class MyNestedObject : DependencyObject await test.RunAsync(); } + + [TestMethod] + public async Task SimpleProperty_ExplicitNullCallbackArgument_IsHandledCorrectly1() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public class MyNestedObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyNestedObject), + typeMetadata: new PropertyMetadata(null, null)); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public partial class MyNestedObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_ExplicitNullCallbackArgument_IsHandledCorrectly2() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public class MyNestedObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyNestedObject), + typeMetadata: new PropertyMetadata("", null)); + + public string? [|Name|] + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public partial class MyNestedObject : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = "")] + public partial string? {|CS9248:Name|} { get; set; } + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From 8792538f649fa93da341d63a3859d69bdd383ec9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 12:46:56 +0100 Subject: [PATCH 129/200] Handle 'string.Empty' as default value --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 6 +++ .../Models/TypedConstantInfo.cs | 5 ++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 52 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 950b9c80b..1062760ed 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -466,6 +466,12 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } } } + else if (conversionOperation.Operand is IFieldReferenceOperation { Field: { ContainingType.SpecialType: SpecialType.System_String, Name: "Empty" } }) + { + // Special handling of the 'string.Empty' field. This is not a constant value, but we can still treat it as a constant, by just + // pretending this were the empty string literal instead. This way we can still support the property and convert to an attribute. + fieldFlags.DefaultValue = TypedConstantInfo.Primitive.String.Empty; + } else { // If we don't have a constant, check if it's some constant value we can forward. In this case, we diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs index 4ffb03a31..d42a2c543 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/TypedConstantInfo.cs @@ -70,6 +70,11 @@ public abstract record Primitive : TypedConstantInfo /// The input value. public sealed record String(string Value) : TypedConstantInfo { + /// + /// The shared instance for empty strings. + /// + public static String Empty { get; } = new(""); + /// public override string ToString() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b338d875b..b284bb9fc 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -321,6 +321,58 @@ public enum MyEnum { A } await test.RunAsync(); } + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_EmptyString() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(string.Empty)); + + public string [|Name|] + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "")] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + [TestMethod] public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace() { From 53f7436a0ccea220945b565c8672bc9cb37dd55a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 15:36:36 +0100 Subject: [PATCH 130/200] Add 'UseFieldDeclarationCorrectlyAnalyzer' and tests --- .../AnalyzerReleases.Shipped.md | 1 + .../UseFieldDeclarationCorrectlyAnalyzer.cs | 69 +++++++++++++++++++ ...endencyPropertyOnManualPropertyAnalyzer.cs | 1 - .../Diagnostics/DiagnosticDescriptors.cs | 13 ++++ .../Test_Analyzers.cs | 52 ++++++++++++++ 5 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 55f8ed3fc..edf5a5d5f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -26,3 +26,4 @@ WCTDP0016 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0020 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs new file mode 100644 index 000000000..d087bf650 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs @@ -0,0 +1,69 @@ +// 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.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning whenever a dependency property is declared as an incorrect field. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseFieldDeclarationCorrectlyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [IncorrectDependencyPropertyFieldDeclaration]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the 'DependencyProperty' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + IFieldSymbol fieldSymbol = (IFieldSymbol)context.Symbol; + + // We only care about fields with are of type 'DependencyProperty' + if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.Type, dependencyPropertySymbol)) + { + return; + } + + // Fields should always be public, static, readonly, and with nothing else on them + if (fieldSymbol is + { DeclaredAccessibility: not Accessibility.Public } or + { IsStatic: false } or + { IsRequired: true } or + { IsReadOnly: false } or + { IsVolatile: true } or + { NullableAnnotation: NullableAnnotation.Annotated }) + { + context.ReportDiagnostic(Diagnostic.Create( + IncorrectDependencyPropertyFieldDeclaration, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol)); + } + }, SymbolKind.Field); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 1062760ed..b7e9193d9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -66,7 +66,6 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(static context => { // Get the XAML mode to use diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index d86c378e5..d69f18ac4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -262,4 +262,17 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must have arguments using supported expressions.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)". + /// + public static readonly DiagnosticDescriptor IncorrectDependencyPropertyFieldDeclaration = new( + id: "WCTDP0020", + title: "Incorrect dependency property field declaration", + messageFormat: "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should be declared as 'public static readonly', and not be nullable.", + helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 30b0a2fb2..b69b0ac56 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1817,4 +1817,56 @@ public class TestAttribute(string P) : Attribute await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task UseFieldDeclarationCorrectlyAnalyzer_NotDependencyProperty_DoesNotWarn() + { + string source = $$""" + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + private static string TestProperty = "Blah"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationCorrectlyAnalyzer_ValidField_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty TestProperty = DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private static readonly DependencyProperty")] + [DataRow("public readonly DependencyProperty")] + [DataRow("public static DependencyProperty")] + [DataRow("public static volatile DependencyProperty")] + [DataRow("public static readonly DependencyProperty?")] + public async Task UseFieldDeclarationCorrectlyAnalyzer_Warns(string fieldDeclaration) + { + string source = $$""" + using Windows.UI.Xaml; + + #nullable enable + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} {|WCTDP0020:TestProperty|}; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From 59e43067253c518600fc147f3ea350bfbb8a6d4c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 16:19:35 +0100 Subject: [PATCH 131/200] Add 'UseFieldDeclarationCorrectlyCodeFixer' and tests --- .../UseFieldDeclarationCorrectlyCodeFixer.cs | 91 +++++++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 2 +- .../Diagnostics/DiagnosticDescriptors.cs | 7 +- ...t_UseFieldDeclarationCorrectlyCodeFixer.cs | 242 ++++++++++++++++++ 4 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs new file mode 100644 index 000000000..4688d3048 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs @@ -0,0 +1,91 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A code fixer that updates field declarations to ensure they follow the recommended rules for dependency properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UseFieldDeclarationCorrectlyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = [IncorrectDependencyPropertyFieldDeclarationId]; + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + 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); + + // Get the property declaration and the field declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { } fieldDeclaration) + { + // Register the code fix to update the field to be correctly declared + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare dependency property field correctly", + createChangedDocument: token => FixDependencyPropertyFieldDeclaration(context.Document, root, fieldDeclaration), + equivalenceKey: "Declare dependency property field correctly"), + diagnostic); + } + } + + /// + /// Applies the code fix to a target field declaration and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The to update. + /// An updated document with the applied code fix. + private static async Task FixDependencyPropertyFieldDeclaration(Document document, SyntaxNode root, FieldDeclarationSyntax fieldDeclaration) + { + await Task.CompletedTask; + + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + // We use the lambda overload mostly for convenient, so we can easily get a generator to use + syntaxEditor.ReplaceNode(fieldDeclaration, (node, generator) => + { + // Update the field to ensure it's declared as 'public static readonly' + node = generator.WithAccessibility(node, Accessibility.Public); + node = generator.WithModifiers(node, DeclarationModifiers.Static | DeclarationModifiers.ReadOnly); + + // If the type is declared as nullable, unwrap it and remove the annotation. + // We need to make sure to carry the space after the element type. When the + // type is nullable, that space is attached to the question mark token. + if (((FieldDeclarationSyntax)node).Declaration is { Type: NullableTypeSyntax { ElementType: { } fieldElementType } nullableType } variableDeclaration) + { + TypeSyntax typeDeclaration = fieldElementType.WithTrailingTrivia(nullableType.QuestionToken.TrailingTrivia); + + node = ((FieldDeclarationSyntax)node).WithDeclaration(variableDeclaration.WithType(typeDeclaration)); + } + + return node; + }); + + // Create the new document with the single change + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 5197300bb..83afc96b8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -182,7 +182,7 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis } /// - /// Applies the code fix to a target identifier and returns an updated document. + /// Applies the code fix to a target property declaration and returns an updated document. /// /// The original document being fixed. /// The instance for the current compilation. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index d69f18ac4..71093bb77 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -16,6 +16,11 @@ internal static class DiagnosticDescriptors /// public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDP0017"; + /// + /// The diagnostic id for . + /// + public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDP0020"; + /// /// "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)". /// @@ -267,7 +272,7 @@ internal static class DiagnosticDescriptors /// "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)". /// public static readonly DiagnosticDescriptor IncorrectDependencyPropertyFieldDeclaration = new( - id: "WCTDP0020", + id: IncorrectDependencyPropertyFieldDeclarationId, title: "Incorrect dependency property field declaration", messageFormat: "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)", category: typeof(DependencyPropertyGenerator).FullName, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs new file mode 100644 index 000000000..09e01500b --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs @@ -0,0 +1,242 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCorrectlyAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCorrectlyCodeFixer>; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_UseFieldDeclarationCorrectlyCodeFixer +{ + [TestMethod] + [DataRow("private static readonly DependencyProperty")] + [DataRow("public readonly DependencyProperty")] + [DataRow("public static DependencyProperty")] + [DataRow("public static volatile DependencyProperty")] + [DataRow("public static readonly DependencyProperty?")] + public async Task SingleField(string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} [|TestProperty|]; + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty TestProperty; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("private static readonly DependencyProperty")] + [DataRow("public readonly DependencyProperty")] + [DataRow("public static DependencyProperty")] + [DataRow("public static volatile DependencyProperty")] + [DataRow("public static readonly DependencyProperty?")] + public async Task SingleField_WithInitializer(string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} [|TestProperty|] = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty TestProperty = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleFields() + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + private static readonly DependencyProperty [|Test1Property|]; + public readonly DependencyProperty [|Test2Property|]; + public static DependencyProperty [|Test3Property|]; + public static readonly DependencyProperty Test4Property; + public static volatile DependencyProperty [|Test5Property|]; + public static readonly DependencyProperty? [|Test6Property|]; + public static readonly DependencyProperty Test7Property; + public static readonly DependencyProperty Test8Property; + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty Test1Property; + public static readonly DependencyProperty Test2Property; + public static readonly DependencyProperty Test3Property; + public static readonly DependencyProperty Test4Property; + public static readonly DependencyProperty Test5Property; + public static readonly DependencyProperty Test6Property; + public static readonly DependencyProperty Test7Property; + public static readonly DependencyProperty Test8Property; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleFields_WithInitializers() + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + private static readonly DependencyProperty [|Test1Property|] = DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public readonly DependencyProperty [|Test2Property|] = DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test3Property|]; + public static readonly DependencyProperty Test4Property = DependencyProperty.Register("Test4", typeof(string), typeof(MyObject), null); + public static volatile DependencyProperty [|Test5Property|]; + public static readonly DependencyProperty? [|Test6Property|] = DependencyProperty.Register("Test6", typeof(string), typeof(MyObject), null); + public static readonly DependencyProperty Test7Property; + public static readonly DependencyProperty Test8Property = DependencyProperty.Register( + "Test8", + typeof(string), + typeof(MyObject), + null); + } + """; + + const string @fixed = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty Test1Property = DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test2Property = DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test3Property; + public static readonly DependencyProperty Test4Property = DependencyProperty.Register("Test4", typeof(string), typeof(MyObject), null); + public static readonly DependencyProperty Test5Property; + public static readonly DependencyProperty Test6Property = DependencyProperty.Register("Test6", typeof(string), typeof(MyObject), null); + public static readonly DependencyProperty Test7Property; + public static readonly DependencyProperty Test8Property = DependencyProperty.Register( + "Test8", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } +} From 618e6bb2fd9c62117072d3497404cb561061c153 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 16:59:25 +0100 Subject: [PATCH 132/200] Add 'UseFieldDeclarationAnalyzer' and tests --- .../AnalyzerReleases.Shipped.md | 1 + .../Constants/WellKnownPropertyNames.cs | 5 + .../Analyzers/UseFieldDeclarationAnalyzer.cs | 103 ++++++++++++++++++ .../UseFieldDeclarationCorrectlyAnalyzer.cs | 3 +- .../Diagnostics/DiagnosticDescriptors.cs | 18 +++ .../Extensions/ISymbolExtensions.cs | 38 +++++++ .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 34 ++++++ .../Test_Analyzers.cs | 89 ++++++++++++++- ....WinUI.DependencyPropertyGenerator.targets | 3 +- 9 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index edf5a5d5f..8b88e411d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -27,3 +27,4 @@ WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0020 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0021 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs index 7d88ca86a..f5f81c523 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs @@ -13,4 +13,9 @@ internal static class WellKnownPropertyNames /// The MSBuild property to control the XAML mode. /// public const string DependencyPropertyGeneratorUseWindowsUIXaml = nameof(DependencyPropertyGeneratorUseWindowsUIXaml); + + /// + /// The MSBuild property to control whether the project is a WinRT component. + /// + public const string CsWinRTComponent = nameof(CsWinRTComponent); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs new file mode 100644 index 000000000..845ad0b7d --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs @@ -0,0 +1,103 @@ +// 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.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning whenever a dependency property is declared as a property. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseFieldDeclarationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DependencyPropertyFieldDeclaration]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the XAML mode to use + bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); + + // Get the 'DependencyProperty' symbol + if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol) + { + return; + } + + // Check whether the current project is a WinRT component (modern .NET uses CsWinRT, legacy .NET produces .winmd files directly) + bool isWinRTComponent = + context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.CsWinRTComponent) || + context.Compilation.Options.OutputKind is OutputKind.WindowsRuntimeMetadata; + + context.RegisterSymbolAction(context => + { + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + + // We only care about properties which are of type 'DependencyProperty' + if (!SymbolEqualityComparer.Default.Equals(propertySymbol.Type, dependencyPropertySymbol)) + { + return; + } + + // If the property is an explicit interface implementation, allow it + if (propertySymbol.ExplicitInterfaceImplementations.Length > 0) + { + return; + } + + // Next, make sure this property isn't (implicitly) implementing any interface properties. + // If that's the case, we'll also allow it, as otherwise fixing this would break things. + foreach (INamedTypeSymbol interfaceSymbol in propertySymbol.ContainingType.AllInterfaces) + { + // Go over all properties (we can filter to just those with the same name) in each interface + foreach (IPropertySymbol interfacePropertySymbol in interfaceSymbol.GetMembers(propertySymbol.Name).OfType()) + { + // The property must have the same type to possibly be an interface implementation + if (!SymbolEqualityComparer.Default.Equals(interfacePropertySymbol.Type, propertySymbol.Type)) + { + continue; + } + + // If the property is not implemented at all, ignore it + if (propertySymbol.ContainingType.FindImplementationForInterfaceMember(interfacePropertySymbol) is not IPropertySymbol implementationSymbol) + { + continue; + } + + // If the current property is the one providing the implementation, then we allow it and stop here + if (SymbolEqualityComparer.Default.Equals(implementationSymbol, propertySymbol)) + { + return; + } + } + } + + // Make an exception for WinRT components: in this case declaring properties is valid, as they're needed for WinRT + if (isWinRTComponent && propertySymbol.GetEffectiveAccessibility() is Accessibility.Public) + { + return; + } + + // At this point, we know for sure the property isn't valid, so emit a diagnostic + context.ReportDiagnostic(Diagnostic.Create( + DependencyPropertyFieldDeclaration, + propertySymbol.Locations.First(), + propertySymbol)); + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs index d087bf650..1acf9b2da 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs @@ -27,7 +27,6 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterCompilationStartAction(static context => { // Get the XAML mode to use @@ -43,7 +42,7 @@ public override void Initialize(AnalysisContext context) { IFieldSymbol fieldSymbol = (IFieldSymbol)context.Symbol; - // We only care about fields with are of type 'DependencyProperty' + // We only care about fields which are of type 'DependencyProperty' if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.Type, dependencyPropertySymbol)) { return; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 71093bb77..9d1b5b5bb 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -21,6 +21,11 @@ internal static class DiagnosticDescriptors /// public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDP0020"; + /// + /// The diagnostic id for . + /// + public const string DependencyPropertyFieldDeclarationId = "WCTDP0021"; + /// /// "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)". /// @@ -280,4 +285,17 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "All dependency property fields should be declared as 'public static readonly', and not be nullable.", helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property"); + + /// + /// "The property '{0}' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types)". + /// + public static readonly DiagnosticDescriptor DependencyPropertyFieldDeclaration = new( + id: DependencyPropertyFieldDeclarationId, + title: "Dependency property declared as a property", + messageFormat: "The property '{0}' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types.", + helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs index c49e99c1c..df1e8d75b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs @@ -91,4 +91,42 @@ public static bool TryGetAttributeWithAnyType(this ISymbol symbol, ImmutableArra return false; } + + /// + /// Calculates the effective accessibility for a given symbol. + /// + /// The instance to check. + /// The effective accessibility for . + public static Accessibility GetEffectiveAccessibility(this ISymbol symbol) + { + // Start by assuming it's visible + Accessibility visibility = Accessibility.Public; + + // Handle special cases + switch (symbol.Kind) + { + case SymbolKind.Alias: return Accessibility.Private; + case SymbolKind.Parameter: return GetEffectiveAccessibility(symbol.ContainingSymbol); + case SymbolKind.TypeParameter: return Accessibility.Private; + } + + // Traverse the symbol hierarchy to determine the effective accessibility + while (symbol is not null && symbol.Kind != SymbolKind.Namespace) + { + switch (symbol.DeclaredAccessibility) + { + case Accessibility.NotApplicable: + case Accessibility.Private: + return Accessibility.Private; + case Accessibility.Internal: + case Accessibility.ProtectedAndInternal: + visibility = Accessibility.Internal; + break; + } + + symbol = symbol.ContainingSymbol; + } + + return visibility; + } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index 441d35d82..071190a59 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -2,6 +2,9 @@ // 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.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.WinUI; @@ -9,6 +12,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis; using Windows.Foundation; using Windows.UI.ViewManagement; @@ -59,4 +63,34 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe return test.RunAsync(CancellationToken.None); } + + /// + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, (string PropertyName, object PropertyValue)[] editorconfig) + { + CSharpAnalyzerTest test = new(languageVersion) { TestCode = source }; + + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)); + + // Add any editorconfig properties, if present + if (editorconfig.Length > 0) + { + test.SolutionTransforms.Add((solution, projectId) => + solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId), + "DependencyPropertyGenerator.editorconfig", + SourceText.From($""" + is_global = true + {string.Join(Environment.NewLine, editorconfig.Select(static p => $"build_property.{p.PropertyName} = {p.PropertyValue}"))} + """, + Encoding.UTF8), + filePath: "/DependencyPropertyGenerator.editorconfig")); + } + + return test.RunAsync(CancellationToken.None); + } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index b69b0ac56..cf03d3f76 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1821,7 +1821,7 @@ public class TestAttribute(string P) : Attribute [TestMethod] public async Task UseFieldDeclarationCorrectlyAnalyzer_NotDependencyProperty_DoesNotWarn() { - string source = $$""" + const string source = """ using Windows.UI.Xaml; public class MyObject : DependencyObject @@ -1869,4 +1869,91 @@ public class MyObject : DependencyObject await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_NotDependencyProperty_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static string TestProperty => "Blah"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_ExplicitInterfaceImplementation_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyObject + { + static DependencyProperty IMyObject.TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + + public interface IMyObject + { + static abstract DependencyProperty TestProperty { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_ImplicitInterfaceImplementation_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyObject + { + public static DependencyProperty TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + + public interface IMyObject + { + static abstract DependencyProperty TestProperty { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_WinRTComponent_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static DependencyProperty TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13, editorconfig: [("CsWinRTComponent", true)]); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_NormalProperty_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static DependencyProperty {|WCTDP0021:Test1Property|} => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); + public static DependencyProperty {|WCTDP0021:Test2Property|} { get; } = DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); + public DependencyProperty {|WCTDP0021:Test3Property|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 612e2bd54..924a54c6e 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -13,9 +13,10 @@ false - + + From df3c72d332756072ffeb07d9bf4c522ddd0e6fb7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 17:59:55 +0100 Subject: [PATCH 133/200] Add 'UseFieldDeclarationCodeFixer' and tests --- .../UseFieldDeclarationCodeFixer.cs | 171 ++++++++++++++ .../UseFieldDeclarationCorrectlyCodeFixer.cs | 2 +- .../Test_Analyzers.cs | 41 ++++ .../Test_UseFieldDeclarationCodeFixer.cs | 218 ++++++++++++++++++ ...t_UseFieldDeclarationCorrectlyCodeFixer.cs | 2 +- 5 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs new file mode 100644 index 000000000..e75e8ed0e --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs @@ -0,0 +1,171 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A code fixer that updates property declarations to be fields instead, for dependency properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UseFieldDeclarationCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = [DependencyPropertyFieldDeclarationId]; + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + 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); + + // Get the property declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration) + { + // We only support this code fix for static properties without modifiers and attributes + if (!IsCodeFixSupportedForPropertyDeclaration(propertyDeclaration)) + { + return; + } + + // We can now register the code fix to convert the property into a field + context.RegisterCodeFix( + CodeAction.Create( + title: "Declare dependency property as field", + createChangedDocument: token => ConvertDependencyPropertyToFieldDeclaration(context.Document, root, propertyDeclaration), + equivalenceKey: "Declare dependency property as field"), + diagnostic); + } + } + + /// + /// Checks whether the code fixer can be applied to a target property declaration. + /// + /// The to update. + /// Whether the code fixer can be applied to . + private static bool IsCodeFixSupportedForPropertyDeclaration(PropertyDeclarationSyntax propertyDeclaration) + { + // We don't support properties with attributes, as those might not work on fields and need special handling + if (propertyDeclaration.AttributeLists.Count > 0) + { + return false; + } + + bool isStatic = false; + + foreach (SyntaxToken modifier in propertyDeclaration.Modifiers) + { + // Accessibility modifiers are allowed (the property will however become public) + if (SyntaxFacts.IsAccessibilityModifier(modifier.Kind())) + { + continue; + } + + // Track whether the property is static + if (modifier.IsKind(SyntaxKind.StaticKeyword)) + { + isStatic = true; + + continue; + } + + // If the property is abstract or an override, or other weird things (which shouldn't really happen), we don't support it + if (modifier.Kind() is SyntaxKind.AbstractKeyword or SyntaxKind.OverrideKeyword or SyntaxKind.PartialKeyword or SyntaxKind.ExternKeyword) + { + return false; + } + } + + // We don't support fixing instance properties automatically, as that might break code + if (!isStatic) + { + return false; + } + + // Properties with an expression body are supported and will be converted to field initializers + if (propertyDeclaration.ExpressionBody is not null) + { + return true; + } + + // The property must have at least an accessor + if (propertyDeclaration.AccessorList is not { Accessors: { Count: > 0 } } accessorList) + { + return false; + } + + // One of the accessors must be a getter + if (!accessorList.Accessors.Any(accessor => accessor.IsKind(SyntaxKind.GetAccessorDeclaration))) + { + return false; + } + + return true; + } + + /// + /// Applies the code fix to a target property declaration and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The to update. + /// An updated document with the applied code fix. + private static async Task ConvertDependencyPropertyToFieldDeclaration(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration) + { + await Task.CompletedTask; + + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + syntaxEditor.ReplaceNode(propertyDeclaration, (node, generator) => + { + // If the property had an initializer, carry that over + ExpressionSyntax? initializerExpression = propertyDeclaration switch + { + { ExpressionBody.Expression: { } arrowExpression } => arrowExpression, + { Initializer.Value: { } equalsExpression } => equalsExpression, + _ => null + }; + + // Create the field declaration and make it 'public static readonly' (same as the other analyzer) + SyntaxNode updatedNode = generator.FieldDeclaration( + name: propertyDeclaration.Identifier.Text, + type: propertyDeclaration.Type, + accessibility: Accessibility.Public, + modifiers: DeclarationModifiers.Static | DeclarationModifiers.ReadOnly, + initializer: initializerExpression); + + // Keep the 'new' modifier, if needed + if (propertyDeclaration.Modifiers.Any(SyntaxKind.NewKeyword)) + { + updatedNode = generator.WithModifiers(updatedNode, generator.GetModifiers(updatedNode).WithIsNew(true)); + } + + return updatedNode.WithTriviaFrom(propertyDeclaration); + }); + + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs index 4688d3048..4ad19f203 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs @@ -39,7 +39,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - // Get the property declaration and the field declaration from the target diagnostic + // Get the the field declaration from the target diagnostic if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { } fieldDeclaration) { // Register the code fix to update the field to be correctly declared diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index cf03d3f76..9ccbd559d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1940,6 +1940,47 @@ public class MyObject : DependencyObject await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13, editorconfig: [("CsWinRTComponent", true)]); } + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_UnsupportedModifier_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public abstract class MyObject : MyBase + { + public DependencyProperty Test1Property => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); + public virtual DependencyProperty Test2Property => DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); + public abstract DependencyProperty Test3Property { get; } + public override DependencyProperty BaseProperty => DependencyProperty.Register("Base", typeof(string), typeof(MyObject), null); + } + + public abstract class MyBase : DependencyObject + { + public abstract DependencyProperty BaseProperty { get; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseFieldDeclarationAnalyzer_WithNoSetter_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public static DependencyProperty TestProperty + { + set { } + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task UseFieldDeclarationAnalyzer_NormalProperty_Warns() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs new file mode 100644 index 000000000..3cf59ab03 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs @@ -0,0 +1,218 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCodeFixer>; +using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationAnalyzer, + CommunityToolkit.GeneratedDependencyProperty.UseFieldDeclarationCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.GeneratedDependencyProperty.Tests; + +[TestClass] +public class Test_UseFieldDeclarationCodeFixer +{ + [TestMethod] + [DataRow("private static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static new DependencyProperty", "public new static readonly DependencyProperty")] + public async Task SingleProperty(string propertyDeclaration, string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{propertyDeclaration}} [|TestProperty|] { get; } = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + string @fixed = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} TestProperty = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("private static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static DependencyProperty", "public static readonly DependencyProperty")] + [DataRow("public static new DependencyProperty", "public new static readonly DependencyProperty")] + public async Task SingleProperty_WithExpressionBody(string propertyDeclaration, string fieldDeclaration) + { + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{propertyDeclaration}} [|TestProperty|] => DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + string @fixed = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + {{fieldDeclaration}} TestProperty = DependencyProperty.Register( + "Test", + typeof(string), + typeof(MyObject), + null); + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task MultipleProperties_WithInitializers() + { + const string original = """ + using System; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + private static DependencyProperty [|Test1Property|] => DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test2Property|] => DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test3Property|] { get; } = DependencyProperty.Register( + "Test3", + typeof(string), + typeof(MyObject), + null); + + public DependencyProperty [|Test4Property|] => DependencyProperty.Register( + "Test4", + typeof(string), + typeof(MyObject), + null); + + public static DependencyProperty [|Test5Property|] { get; } + public virtual DependencyProperty [|Test6Property|] { get; } + + [Test] + public static DependencyProperty [|Test7Property|] { get; } + } + + public class TestAttribute : Attribute; + """; + + const string @fixed = """ + using System; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty Test1Property = DependencyProperty.Register( + "Test1", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test2Property = DependencyProperty.Register( + "Test2", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test3Property = DependencyProperty.Register( + "Test3", + typeof(string), + typeof(MyObject), + null); + + public DependencyProperty Test4Property => DependencyProperty.Register( + "Test4", + typeof(string), + typeof(MyObject), + null); + + public static readonly DependencyProperty Test5Property; + public virtual DependencyProperty Test6Property { get; } + + [Test] + public static DependencyProperty [|Test7Property|] { get; } + } + + public class TestAttribute : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + test.FixedState.ExpectedDiagnostics.AddRange( + [ + // /0/Test0.cs(26,31): warning WCTDP0021: The property 'MyApp.MyObject.Test4Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) + CSharpCodeFixVerifier.Diagnostic().WithSpan(26, 31, 26, 44).WithArguments("MyApp.MyObject.Test4Property"), + + // /0/Test0.cs(33,39): warning WCTDP0021: The property 'MyApp.MyObject.Test6Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) + CSharpCodeFixVerifier.Diagnostic().WithSpan(33, 39, 33, 52).WithArguments("MyApp.MyObject.Test6Property"), + + // /0/Test0.cs(36,38): warning WCTDP0021: The property 'MyApp.MyObject.Test7Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) + CSharpCodeFixVerifier.Diagnostic().WithSpan(36, 38, 36, 51).WithArguments("MyApp.MyObject.Test7Property") + ]); + + await test.RunAsync(); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs index 09e01500b..afd3cb364 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs @@ -163,7 +163,7 @@ public class MyObject : DependencyObject [TestMethod] public async Task MultipleFields_WithInitializers() { - string original = $$""" + const string original = """ using Windows.UI.Xaml; #nullable enable From 0d01d5cf37d861de225b0de0e39c045224393390 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 28 Dec 2024 21:25:44 +0100 Subject: [PATCH 134/200] Handle nested enum types in code fixer --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 38 ++++-- ...endencyPropertyOnManualPropertyAnalyzer.cs | 45 +++++-- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 118 ++++++++++++++++++ 3 files changed, 177 insertions(+), 24 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 83afc96b8..656a76974 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -60,6 +60,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) // Retrieve the properties passed by the analyzer string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeFullyQualifiedMetadataNamePropertyName]; SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); @@ -80,7 +81,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) root, propertyDeclaration, fieldDeclaration, - defaultValue), + defaultValue, + defaultValueTypeFullyQualifiedMetadataName), equivalenceKey: "Use a partial property"), diagnostic); } @@ -122,12 +124,14 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList( /// The original document being fixed. /// The instance for the current compilation. /// The expression for the default value of the property, if present + /// The fully qualified metadata name of the default value, if present. /// The updated attribute syntax. private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList( Document document, SemanticModel semanticModel, AttributeListSyntax generatedDependencyPropertyAttributeList, - string? defaultValueExpression) + string? defaultValueExpression, + string? defaultValueTypeFullyQualifiedMetadataName) { // If we do have a default value expression, set it in the attribute. // We extract the generated attribute so we can add the new argument. @@ -139,18 +143,19 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed' if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) { - string fullyQualifiedTypeName = expressionSyntax.ToFullString(); + string fullyQualifiedMetadataName = defaultValueTypeFullyQualifiedMetadataName ?? expressionSyntax.ToFullString(); - // Ensure we strip the global prefix, if present (it should always be present) - if (fullyQualifiedTypeName.StartsWith("global::")) + // Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name). + // Note that using the fully qualified type name is just a fallback, as we should always have the metadata name. + if (fullyQualifiedMetadataName.StartsWith("global::")) { - fullyQualifiedTypeName = fullyQualifiedTypeName["global::".Length..]; + fullyQualifiedMetadataName = fullyQualifiedMetadataName["global::".Length..]; } // Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not // a fully qualified type name. However, for virtually all cases for enum types, the two should match. // That is, they will be the same if the type is not nested, and not generic, which is what we expect. - if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedTypeName) is INamedTypeSymbol enumTypeSymbol) + if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) is INamedTypeSymbol enumTypeSymbol) { SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); @@ -190,6 +195,7 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis /// The for the property being updated. /// The for the declared property to remove. /// The expression for the default value of the property, if present + /// The fully qualified metadata name of the default value, if present. /// An updated document with the applied code fix, and being replaced with a partial property. private static async Task ConvertToPartialProperty( Document document, @@ -197,7 +203,8 @@ private static async Task ConvertToPartialProperty( SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, - string? defaultValueExpression) + string? defaultValueExpression, + string? defaultValueTypeFullyQualifiedMetadataName) { await Task.CompletedTask; @@ -217,7 +224,8 @@ private static async Task ConvertToPartialProperty( fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, - defaultValueExpression); + defaultValueExpression, + defaultValueTypeFullyQualifiedMetadataName); RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor); @@ -235,6 +243,7 @@ private static async Task ConvertToPartialProperty( /// The with the attribute to add. /// The instance to use. /// The expression for the default value of the property, if present + /// The fully qualified metadata name of the default value, if present. /// An updated document with the applied code fix, and being replaced with a partial property. private static void ConvertToPartialProperty( Document document, @@ -243,7 +252,8 @@ private static void ConvertToPartialProperty( FieldDeclarationSyntax fieldDeclaration, AttributeListSyntax generatedDependencyPropertyAttributeList, SyntaxEditor syntaxEditor, - string? defaultValueExpression) + string? defaultValueExpression, + string? defaultValueTypeFullyQualifiedMetadataName) { // Replace the property with the partial property using the attribute. Note that it's important to use the // lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and @@ -258,7 +268,8 @@ private static void ConvertToPartialProperty( document, semanticModel, generatedDependencyPropertyAttributeList, - defaultValueExpression); + defaultValueExpression, + defaultValueTypeFullyQualifiedMetadataName); // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -459,6 +470,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider // Retrieve the properties passed by the analyzer string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; + string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeFullyQualifiedMetadataNamePropertyName]; + ConvertToPartialProperty( document, @@ -467,7 +480,8 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, - defaultValue); + defaultValue, + defaultValueTypeFullyQualifiedMetadataName); fieldDeclarations.Add(fieldDeclaration); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index b7e9193d9..b0468b88a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -57,6 +57,11 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia /// public const string DefaultValuePropertyName = "DefaultValue"; + /// + /// The property name for the fully qualified metadata name of the default value, if present. + /// + public const string DefaultValueTypeFullyQualifiedMetadataNamePropertyName = "DefaultValueTypeFullyQualifiedMetadataName"; + /// public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; @@ -449,19 +454,27 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } else if (TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) { - // We have found a valid constant. As an optimization, we check whether the constant was the value - // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the - // case, the XAML infrastructure can default that values automatically, meaning we can skip the - // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. - if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType && - operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + // We have found a valid constant. If it's an enum type, we have a couple special cases to handle. + if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType) { - // Before actually enabling the optimization, validate that the default value is actually - // the same as the default value of the enum (ie. the value of its first declared field). - if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) && - conversionOperation.Operand.ConstantValue.Value == defaultValue) + // As an optimization, we check whether the constant was the value + // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the + // case, the XAML infrastructure can default that values automatically, meaning we can skip the + // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. + if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + { + // Before actually enabling the optimization, validate that the default value is actually + // the same as the default value of the enum (ie. the value of its first declared field). + if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) && + conversionOperation.Operand.ConstantValue.Value == defaultValue) + { + fieldFlags.DefaultValue = null; + } + } + else if (operandType.ContainingType is not null) { - fieldFlags.DefaultValue = null; + // If the enum is nested, we need to also + fieldFlags.DefaultValueTypeFullyQualifiedMetadataName = operandType.GetFullyQualifiedMetadataName(); } } } @@ -552,7 +565,9 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), [fieldLocation], - ImmutableDictionary.Create().Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()), + ImmutableDictionary.Create() + .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) + .Add(DefaultValueTypeFullyQualifiedMetadataNamePropertyName, fieldFlags.DefaultValueTypeFullyQualifiedMetadataName), pair.Key)); } } @@ -573,6 +588,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyName = null; fieldFlags.PropertyType = null; fieldFlags.DefaultValue = null; + fieldFlags.DefaultValueTypeFullyQualifiedMetadataName = null; fieldFlags.FieldLocation = null; fieldFlagsStack.Push(fieldFlags); @@ -647,6 +663,11 @@ private sealed class FieldFlags /// public TypedConstantInfo? DefaultValue; + /// + /// The fully qualified metadata name of the default value, if needed. + /// + public string? DefaultValueTypeFullyQualifiedMetadataName; + /// /// The location of the target field being initialized. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b284bb9fc..5dc851497 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -373,6 +373,124 @@ public partial class MyControl : Control await test.RunAsync(); } + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_NestedEnumType() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(MyContainingType.MyEnum), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(MyContainingType.MyEnum.B)); + + public MyContainingType.MyEnum [|Name|] + { + get => (MyContainingType.MyEnum)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = MyContainingType.MyEnum.B)] + public partial MyContainingType.MyEnum {|CS9248:Name|} { get; set; } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithExplicitValue_NestedEnumType_WithUsingStatic() + { + const string original = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + using static MyApp.MyContainingType; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(MyEnum), + ownerType: typeof(MyControl), + typeMetadata: new PropertyMetadata(MyEnum.B)); + + public MyEnum [|Name|] + { + get => (MyEnum)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + using static MyApp.MyContainingType; + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = MyEnum.B)] + public partial MyEnum {|CS9248:Name|} { get; set; } + } + + public class MyContainingType + { + public enum MyEnum { A, B } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + [TestMethod] public async Task SimpleProperty_WithExplicitValue_NotDefault_AddsNamespace() { From 922b01620e930a77592436781c2d2e48c7a392ff Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 29 Dec 2024 00:34:46 +0100 Subject: [PATCH 135/200] Don't warn on instance fields and properties Also fix some other misc bugs, eg. around trivia --- .../UseFieldDeclarationCodeFixer.cs | 18 +---------- .../UseFieldDeclarationCorrectlyCodeFixer.cs | 5 ++- .../Analyzers/UseFieldDeclarationAnalyzer.cs | 12 +++++++ .../UseFieldDeclarationCorrectlyAnalyzer.cs | 8 ++++- .../Test_Analyzers.cs | 5 ++- .../Test_UseFieldDeclarationCodeFixer.cs | 32 ++++--------------- ...t_UseFieldDeclarationCorrectlyCodeFixer.cs | 16 ++++------ 7 files changed, 38 insertions(+), 58 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs index e75e8ed0e..80b5ffc19 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCodeFixer.cs @@ -74,8 +74,6 @@ private static bool IsCodeFixSupportedForPropertyDeclaration(PropertyDeclaration return false; } - bool isStatic = false; - foreach (SyntaxToken modifier in propertyDeclaration.Modifiers) { // Accessibility modifiers are allowed (the property will however become public) @@ -84,14 +82,6 @@ private static bool IsCodeFixSupportedForPropertyDeclaration(PropertyDeclaration continue; } - // Track whether the property is static - if (modifier.IsKind(SyntaxKind.StaticKeyword)) - { - isStatic = true; - - continue; - } - // If the property is abstract or an override, or other weird things (which shouldn't really happen), we don't support it if (modifier.Kind() is SyntaxKind.AbstractKeyword or SyntaxKind.OverrideKeyword or SyntaxKind.PartialKeyword or SyntaxKind.ExternKeyword) { @@ -99,12 +89,6 @@ private static bool IsCodeFixSupportedForPropertyDeclaration(PropertyDeclaration } } - // We don't support fixing instance properties automatically, as that might break code - if (!isStatic) - { - return false; - } - // Properties with an expression body are supported and will be converted to field initializers if (propertyDeclaration.ExpressionBody is not null) { @@ -112,7 +96,7 @@ private static bool IsCodeFixSupportedForPropertyDeclaration(PropertyDeclaration } // The property must have at least an accessor - if (propertyDeclaration.AccessorList is not { Accessors: { Count: > 0 } } accessorList) + if (propertyDeclaration.AccessorList is not { Accessors.Count: > 0 } accessorList) { return false; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs index 4ad19f203..5e750ab4b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseFieldDeclarationCorrectlyCodeFixer.cs @@ -68,6 +68,9 @@ private static async Task FixDependencyPropertyFieldDeclaration(Docume // We use the lambda overload mostly for convenient, so we can easily get a generator to use syntaxEditor.ReplaceNode(fieldDeclaration, (node, generator) => { + // Keep the original node to get the trivia back from it + SyntaxNode originalNode = node; + // Update the field to ensure it's declared as 'public static readonly' node = generator.WithAccessibility(node, Accessibility.Public); node = generator.WithModifiers(node, DeclarationModifiers.Static | DeclarationModifiers.ReadOnly); @@ -82,7 +85,7 @@ private static async Task FixDependencyPropertyFieldDeclaration(Docume node = ((FieldDeclarationSyntax)node).WithDeclaration(variableDeclaration.WithType(typeDeclaration)); } - return node; + return node.WithTriviaFrom(originalNode); }); // Create the new document with the single change diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs index 845ad0b7d..0e0956524 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationAnalyzer.cs @@ -47,6 +47,18 @@ public override void Initialize(AnalysisContext context) { IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + // Ignore instance properties (same as in the other analyzer), and interface members + if (propertySymbol is { IsStatic: false } or { ContainingType.TypeKind: TypeKind.Interface }) + { + return; + } + + // Also ignore properties that are write-only (there can't really be normal dependency properties) + if (propertySymbol.IsWriteOnly) + { + return; + } + // We only care about properties which are of type 'DependencyProperty' if (!SymbolEqualityComparer.Default.Equals(propertySymbol.Type, dependencyPropertySymbol)) { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs index 1acf9b2da..2b6394afd 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs @@ -42,6 +42,13 @@ public override void Initialize(AnalysisContext context) { IFieldSymbol fieldSymbol = (IFieldSymbol)context.Symbol; + // Ignore instance fields, to reduce false positives. There might be edge cases + // where property instances are used as state, and that's technically not invalid. + if (!fieldSymbol.IsStatic) + { + return; + } + // We only care about fields which are of type 'DependencyProperty' if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.Type, dependencyPropertySymbol)) { @@ -51,7 +58,6 @@ public override void Initialize(AnalysisContext context) // Fields should always be public, static, readonly, and with nothing else on them if (fieldSymbol is { DeclaredAccessibility: not Accessibility.Public } or - { IsStatic: false } or { IsRequired: true } or { IsReadOnly: false } or { IsVolatile: true } or diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 9ccbd559d..58996f02e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1850,7 +1850,6 @@ public class MyObject : DependencyObject [TestMethod] [DataRow("private static readonly DependencyProperty")] - [DataRow("public readonly DependencyProperty")] [DataRow("public static DependencyProperty")] [DataRow("public static volatile DependencyProperty")] [DataRow("public static readonly DependencyProperty?")] @@ -1964,7 +1963,7 @@ public abstract class MyBase : DependencyObject } [TestMethod] - public async Task UseFieldDeclarationAnalyzer_WithNoSetter_DoesNotWarn() + public async Task UseFieldDeclarationAnalyzer_WithNoGetter_DoesNotWarn() { const string source = """ using Windows.UI.Xaml; @@ -1991,7 +1990,7 @@ public class MyObject : DependencyObject { public static DependencyProperty {|WCTDP0021:Test1Property|} => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); public static DependencyProperty {|WCTDP0021:Test2Property|} { get; } = DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); - public DependencyProperty {|WCTDP0021:Test3Property|} { get; set; } + public static DependencyProperty {|WCTDP0021:Test3Property|} { get; set; } } """; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs index 3cf59ab03..10aaa50b4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs @@ -137,17 +137,10 @@ public class MyObject : DependencyObject typeof(MyObject), null); - public DependencyProperty [|Test4Property|] => DependencyProperty.Register( - "Test4", - typeof(string), - typeof(MyObject), - null); - - public static DependencyProperty [|Test5Property|] { get; } - public virtual DependencyProperty [|Test6Property|] { get; } + public static DependencyProperty [|Test4Property|] { get; } [Test] - public static DependencyProperty [|Test7Property|] { get; } + public static DependencyProperty [|Test5Property|] { get; } } public class TestAttribute : Attribute; @@ -179,17 +172,10 @@ public class MyObject : DependencyObject typeof(MyObject), null); - public DependencyProperty Test4Property => DependencyProperty.Register( - "Test4", - typeof(string), - typeof(MyObject), - null); - - public static readonly DependencyProperty Test5Property; - public virtual DependencyProperty Test6Property { get; } + public static readonly DependencyProperty Test4Property; [Test] - public static DependencyProperty [|Test7Property|] { get; } + public static DependencyProperty Test5Property { get; } } public class TestAttribute : Attribute; @@ -203,14 +189,8 @@ public class TestAttribute : Attribute; test.FixedState.ExpectedDiagnostics.AddRange( [ - // /0/Test0.cs(26,31): warning WCTDP0021: The property 'MyApp.MyObject.Test4Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) - CSharpCodeFixVerifier.Diagnostic().WithSpan(26, 31, 26, 44).WithArguments("MyApp.MyObject.Test4Property"), - - // /0/Test0.cs(33,39): warning WCTDP0021: The property 'MyApp.MyObject.Test6Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) - CSharpCodeFixVerifier.Diagnostic().WithSpan(33, 39, 33, 52).WithArguments("MyApp.MyObject.Test6Property"), - - // /0/Test0.cs(36,38): warning WCTDP0021: The property 'MyApp.MyObject.Test7Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) - CSharpCodeFixVerifier.Diagnostic().WithSpan(36, 38, 36, 51).WithArguments("MyApp.MyObject.Test7Property") + // /0/Test0.cs(29,38): warning WCTDP0021: The property 'MyApp.MyObject.Test5Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) + CSharpCodeFixVerifier.Diagnostic().WithSpan(29, 38, 29, 51).WithArguments("MyApp.MyObject.Test5Property") ]); await test.RunAsync(); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs index afd3cb364..d31d3ad70 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCorrectlyCodeFixer.cs @@ -16,7 +16,6 @@ public class Test_UseFieldDeclarationCorrectlyCodeFixer { [TestMethod] [DataRow("private static readonly DependencyProperty")] - [DataRow("public readonly DependencyProperty")] [DataRow("public static DependencyProperty")] [DataRow("public static volatile DependencyProperty")] [DataRow("public static readonly DependencyProperty?")] @@ -59,7 +58,6 @@ public class MyObject : DependencyObject [TestMethod] [DataRow("private static readonly DependencyProperty")] - [DataRow("public readonly DependencyProperty")] [DataRow("public static DependencyProperty")] [DataRow("public static volatile DependencyProperty")] [DataRow("public static readonly DependencyProperty?")] @@ -121,13 +119,12 @@ namespace MyApp; public class MyObject : DependencyObject { private static readonly DependencyProperty [|Test1Property|]; - public readonly DependencyProperty [|Test2Property|]; - public static DependencyProperty [|Test3Property|]; - public static readonly DependencyProperty Test4Property; - public static volatile DependencyProperty [|Test5Property|]; - public static readonly DependencyProperty? [|Test6Property|]; + public static DependencyProperty [|Test2Property|]; + public static readonly DependencyProperty Test3Property; + public static volatile DependencyProperty [|Test4Property|]; + public static readonly DependencyProperty? [|Test5Property|]; + public static readonly DependencyProperty Test6Property; public static readonly DependencyProperty Test7Property; - public static readonly DependencyProperty Test8Property; } """; @@ -147,7 +144,6 @@ public class MyObject : DependencyObject public static readonly DependencyProperty Test5Property; public static readonly DependencyProperty Test6Property; public static readonly DependencyProperty Test7Property; - public static readonly DependencyProperty Test8Property; } """; @@ -178,7 +174,7 @@ public class MyObject : DependencyObject typeof(MyObject), null); - public readonly DependencyProperty [|Test2Property|] = DependencyProperty.Register( + internal static volatile DependencyProperty [|Test2Property|] = DependencyProperty.Register( "Test2", typeof(string), typeof(MyObject), From dff25ef0d67e8346f6067290ac8dc46303161c9f Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Sun, 29 Dec 2024 01:31:44 +0100 Subject: [PATCH 136/200] Fix nested enum test failure --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 56 +++++++++++++------ ...endencyPropertyOnManualPropertyAnalyzer.cs | 16 +++--- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 656a76974..04e489815 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -60,7 +60,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) // Retrieve the properties passed by the analyzer string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; - string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeFullyQualifiedMetadataNamePropertyName]; + string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); @@ -82,7 +82,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) propertyDeclaration, fieldDeclaration, defaultValue, - defaultValueTypeFullyQualifiedMetadataName), + defaultValueTypeReferenceId), equivalenceKey: "Use a partial property"), diagnostic); } @@ -131,7 +131,7 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis SemanticModel semanticModel, AttributeListSyntax generatedDependencyPropertyAttributeList, string? defaultValueExpression, - string? defaultValueTypeFullyQualifiedMetadataName) + string? defaultValueTypeReferenceId) { // If we do have a default value expression, set it in the attribute. // We extract the generated attribute so we can add the new argument. @@ -140,13 +140,34 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis { ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); - // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed' - if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'. + // We have two cases to handle, which require different logic to ensure the correct tree is always generated: + // - For nested enum types, we'll have a reference id. In this case, we manually insert annotations. + // - For normal enum member accesses, we resolve the type and then construct the tree from that expression. + if (defaultValueTypeReferenceId is not null) { - string fullyQualifiedMetadataName = defaultValueTypeFullyQualifiedMetadataName ?? expressionSyntax.ToFullString(); + // Here we're relying on the special 'SymbolId' annotation, which is used internally by Roslyn to track + // necessary imports for type expressions. We need to rely on this implementation detail here because + // there is no public API to correctly produce a tree for an enum member access on a nested type. + // This internal detail is one that many generators take a dependency on already, so it's safe-ish. + parsedExpression = parsedExpression.WithAdditionalAnnotations( + Simplifier.Annotation, + Simplifier.AddImportsAnnotation, + new SyntaxAnnotation("SymbolId", defaultValueTypeReferenceId)); + + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute argument to insert + SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", parsedExpression); + + // Actually add the argument to the existing attribute syntax + return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); + } + else if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + { + string fullyQualifiedMetadataName = expressionSyntax.ToFullString(); - // Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name). - // Note that using the fully qualified type name is just a fallback, as we should always have the metadata name. + // Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name) if (fullyQualifiedMetadataName.StartsWith("global::")) { fullyQualifiedMetadataName = fullyQualifiedMetadataName["global::".Length..]; @@ -165,11 +186,10 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis // Create the member access expression for the target enum type SyntaxNode enumMemberAccessExpressionSyntax = syntaxGenerator.MemberAccessExpression(enumTypeSyntax, memberName); - // Create the attribute argument to insert - SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax); - - // Actually add the argument to the existing attribute syntax - return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); + // Create the attribute argument, like in the previous case + return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax)]); } } @@ -204,7 +224,7 @@ private static async Task ConvertToPartialProperty( PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, string? defaultValueExpression, - string? defaultValueTypeFullyQualifiedMetadataName) + string? defaultValueTypeReferenceId) { await Task.CompletedTask; @@ -225,7 +245,7 @@ private static async Task ConvertToPartialProperty( generatedDependencyPropertyAttributeList, syntaxEditor, defaultValueExpression, - defaultValueTypeFullyQualifiedMetadataName); + defaultValueTypeReferenceId); RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor); @@ -253,7 +273,7 @@ private static void ConvertToPartialProperty( AttributeListSyntax generatedDependencyPropertyAttributeList, SyntaxEditor syntaxEditor, string? defaultValueExpression, - string? defaultValueTypeFullyQualifiedMetadataName) + string? defaultValueTypeReferenceId) { // Replace the property with the partial property using the attribute. Note that it's important to use the // lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and @@ -269,7 +289,7 @@ private static void ConvertToPartialProperty( semanticModel, generatedDependencyPropertyAttributeList, defaultValueExpression, - defaultValueTypeFullyQualifiedMetadataName); + defaultValueTypeReferenceId); // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -470,7 +490,7 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider // Retrieve the properties passed by the analyzer string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; - string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeFullyQualifiedMetadataNamePropertyName]; + string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; ConvertToPartialProperty( diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index b0468b88a..501b8e44e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -60,7 +60,7 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia /// /// The property name for the fully qualified metadata name of the default value, if present. /// - public const string DefaultValueTypeFullyQualifiedMetadataNamePropertyName = "DefaultValueTypeFullyQualifiedMetadataName"; + public const string DefaultValueTypeReferenceIdPropertyName = "DefaultValueTypeReferenceId"; /// public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; @@ -473,8 +473,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } else if (operandType.ContainingType is not null) { - // If the enum is nested, we need to also - fieldFlags.DefaultValueTypeFullyQualifiedMetadataName = operandType.GetFullyQualifiedMetadataName(); + // If the enum is nested, we need to also track the type symbol specifically, as the fully qualified + // expression we'd be using otherwise would not be the same as the metadata name, and resolving the + // enum type symbol from that in the code fixer would fail. This is an edge case, but it can happen. + fieldFlags.DefaultValueTypeReferenceId = DocumentationCommentId.CreateReferenceId(operandType); } } } @@ -567,7 +569,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla [fieldLocation], ImmutableDictionary.Create() .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) - .Add(DefaultValueTypeFullyQualifiedMetadataNamePropertyName, fieldFlags.DefaultValueTypeFullyQualifiedMetadataName), + .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId), pair.Key)); } } @@ -588,7 +590,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyName = null; fieldFlags.PropertyType = null; fieldFlags.DefaultValue = null; - fieldFlags.DefaultValueTypeFullyQualifiedMetadataName = null; + fieldFlags.DefaultValueTypeReferenceId = null; fieldFlags.FieldLocation = null; fieldFlagsStack.Push(fieldFlags); @@ -664,9 +666,9 @@ private sealed class FieldFlags public TypedConstantInfo? DefaultValue; /// - /// The fully qualified metadata name of the default value, if needed. + /// The documentation comment reference id for type of the default value, if needed. /// - public string? DefaultValueTypeFullyQualifiedMetadataName; + public string? DefaultValueTypeReferenceId; /// /// The location of the target field being initialized. From 1b607b56d8a7c88a9a5aa31831cf40cdc84664a7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 29 Dec 2024 16:13:58 +0100 Subject: [PATCH 137/200] Fix UWP XAML checks for legacy UWP --- ...mmunityToolkit.WinUI.DependencyPropertyGenerator.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets index 924a54c6e..00a35e6a2 100644 --- a/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets +++ b/components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets @@ -1,10 +1,10 @@ - + - true + true true - false + false From b8a7824bbf2f46271c53b0678e2908abb6660213 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 29 Dec 2024 16:16:02 +0100 Subject: [PATCH 138/200] Don't suggest using the generator on C# < 13 --- ...eGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 501b8e44e..242c34964 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -73,6 +73,13 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(static context => { + // If the language is not at least C#, the generator can't be used, so we shouldn't emit warnings. + // If we did so, users wouldn't be able to fix them anyway without bumping the language first. + if (!context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)) + { + return; + } + // Get the XAML mode to use bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml); From 28ab171eef5447e1f2dcc64bad50338c46dfc7d2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 30 Dec 2024 01:42:58 +0100 Subject: [PATCH 139/200] Preserve named constants in code fixer --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 67 ++++++++++---- ...endencyPropertyOnManualPropertyAnalyzer.cs | 16 +++- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 90 +++++++++++++++++++ 3 files changed, 154 insertions(+), 19 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 04e489815..99c25db88 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -46,8 +46,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) Diagnostic diagnostic = context.Diagnostics[0]; TextSpan diagnosticSpan = context.Span; - // We can only possibly fix diagnostics with an additional location - if (diagnostic.AdditionalLocations is not [{ } fieldLocation]) + // We always expect the field location to be the first additional location, this must be present + if (diagnostic.AdditionalLocations is not [{ } fieldLocation, ..]) { return; } @@ -62,6 +62,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; + // Get any additional locations, if available + Location? defaultValueExpressionLocation = diagnostic.AdditionalLocations.ElementAtOrDefault(1); + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); // Get the property declaration and the field declaration from the target diagnostic @@ -82,7 +85,8 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) propertyDeclaration, fieldDeclaration, defaultValue, - defaultValueTypeReferenceId), + defaultValueTypeReferenceId, + defaultValueExpressionLocation), equivalenceKey: "Use a partial property"), diagnostic); } @@ -123,21 +127,38 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList( /// /// The original document being fixed. /// The instance for the current compilation. + /// The original tree root belonging to the current document. /// The expression for the default value of the property, if present - /// The fully qualified metadata name of the default value, if present. + /// The documentation comment reference id for type of the default value, if present. + /// The location for the default value, if available. /// The updated attribute syntax. private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeList( Document document, SemanticModel semanticModel, + SyntaxNode root, AttributeListSyntax generatedDependencyPropertyAttributeList, string? defaultValueExpression, - string? defaultValueTypeReferenceId) + string? defaultValueTypeReferenceId, + Location? defaultValueExpressionLocation) { // If we do have a default value expression, set it in the attribute. // We extract the generated attribute so we can add the new argument. // It's important to reuse it, as it has the "add usings" annotation. if (defaultValueExpression is not null) { + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Special case if we have a location for the original expression, and we can resolve the node. + // In this case, we want to just carry that over with no changes (this is used for named constants). + // See notes below for how this method is constructing the new attribute argument to insert. + if (defaultValueExpressionLocation is not null && + root.FindNode(defaultValueExpressionLocation.SourceSpan) is ArgumentSyntax { Expression: { } defaultValueOriginalExpression }) + { + return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", defaultValueOriginalExpression)]); + } + ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'. @@ -155,8 +176,6 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis Simplifier.AddImportsAnnotation, new SyntaxAnnotation("SymbolId", defaultValueTypeReferenceId)); - SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); - // Create the attribute argument to insert SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", parsedExpression); @@ -178,8 +197,6 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis // That is, they will be the same if the type is not nested, and not generic, which is what we expect. if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) is INamedTypeSymbol enumTypeSymbol) { - SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); - // Create the identifier syntax for the enum type, with the right annotations SyntaxNode enumTypeSyntax = syntaxGenerator.TypeExpression(enumTypeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); @@ -215,7 +232,8 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis /// The for the property being updated. /// The for the declared property to remove. /// The expression for the default value of the property, if present - /// The fully qualified metadata name of the default value, if present. + /// The documentation comment reference id for type of the default value, if present. + /// The location for the default value, if available. /// An updated document with the applied code fix, and being replaced with a partial property. private static async Task ConvertToPartialProperty( Document document, @@ -224,7 +242,8 @@ private static async Task ConvertToPartialProperty( PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, string? defaultValueExpression, - string? defaultValueTypeReferenceId) + string? defaultValueTypeReferenceId, + Location? defaultValueExpressionLocation) { await Task.CompletedTask; @@ -240,12 +259,14 @@ private static async Task ConvertToPartialProperty( ConvertToPartialProperty( document, semanticModel, + root, propertyDeclaration, fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, defaultValueExpression, - defaultValueTypeReferenceId); + defaultValueTypeReferenceId, + defaultValueExpressionLocation); RemoveLeftoverLeadingEndOfLines([fieldDeclaration], syntaxEditor); @@ -258,22 +279,26 @@ private static async Task ConvertToPartialProperty( /// /// The original document being fixed. /// The instance for the current compilation. + /// The original tree root belonging to the current document. /// The for the property being updated. /// The for the declared property to remove. /// The with the attribute to add. /// The instance to use. /// The expression for the default value of the property, if present - /// The fully qualified metadata name of the default value, if present. + /// The documentation comment reference id for type of the default value, if present. + /// The location for the default value, if available. /// An updated document with the applied code fix, and being replaced with a partial property. private static void ConvertToPartialProperty( Document document, SemanticModel semanticModel, + SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, AttributeListSyntax generatedDependencyPropertyAttributeList, SyntaxEditor syntaxEditor, string? defaultValueExpression, - string? defaultValueTypeReferenceId) + string? defaultValueTypeReferenceId, + Location? defaultValueExpressionLocation) { // Replace the property with the partial property using the attribute. Note that it's important to use the // lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and @@ -287,9 +312,11 @@ private static void ConvertToPartialProperty( generatedDependencyPropertyAttributeList = UpdateGeneratedDependencyPropertyAttributeList( document, semanticModel, + root, generatedDependencyPropertyAttributeList, defaultValueExpression, - defaultValueTypeReferenceId); + defaultValueTypeReferenceId, + defaultValueExpressionLocation); // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -482,7 +509,7 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider } // Also check that we can find the target field to remove - if (diagnostic.AdditionalLocations is not [{ } fieldLocation] || + if (diagnostic.AdditionalLocations is not [{ } fieldLocation, ..] || root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) { continue; @@ -490,18 +517,22 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider // Retrieve the properties passed by the analyzer string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; - string? defaultValueTypeFullyQualifiedMetadataName = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; + string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; + // Get any additional locations, if available + Location? defaultValueExpressionLocation = diagnostic.AdditionalLocations.ElementAtOrDefault(1); ConvertToPartialProperty( document, semanticModel, + root, propertyDeclaration, fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, defaultValue, - defaultValueTypeFullyQualifiedMetadataName); + defaultValueTypeReferenceId, + defaultValueExpressionLocation); fieldDeclarations.Add(fieldDeclaration); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 242c34964..455260b25 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -486,6 +486,14 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.DefaultValueTypeReferenceId = DocumentationCommentId.CreateReferenceId(operandType); } } + + // Special case for named constants: in this case we want to carry the whole expression over, rather + // than serializing the constant value itself. This preserves the actual fields being referenced. + // We skip enum fields, as those are not named constants. Those will be handled by the logic above. + if (conversionOperation.Operand is IFieldReferenceOperation { Field: { IsConst: true, ContainingType.TypeKind: not TypeKind.Enum } }) + { + fieldFlags.DefaultValueExpressionLocation = conversionOperation.Operand.Syntax.GetLocation(); + } } else if (conversionOperation.Operand is IFieldReferenceOperation { Field: { ContainingType.SpecialType: SpecialType.System_String, Name: "Empty" } }) { @@ -573,7 +581,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla context.ReportDiagnostic(Diagnostic.Create( UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), - [fieldLocation], + ((Location?[])[fieldLocation, fieldFlags.DefaultValueExpressionLocation]).OfType(), ImmutableDictionary.Create() .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId), @@ -598,6 +606,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyType = null; fieldFlags.DefaultValue = null; fieldFlags.DefaultValueTypeReferenceId = null; + fieldFlags.DefaultValueExpressionLocation = null; fieldFlags.FieldLocation = null; fieldFlagsStack.Push(fieldFlags); @@ -677,6 +686,11 @@ private sealed class FieldFlags /// public string? DefaultValueTypeReferenceId; + /// + /// The location for the default value, if available. + /// + public Location? DefaultValueExpressionLocation; + /// /// The location of the target field being initialized. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 5dc851497..b948bdb3f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -544,6 +544,96 @@ public partial class MyControl : Control await test.RunAsync(); } + [TestMethod] + [DataRow("int", "MyContainer1.X")] + [DataRow("float", "MyContainer1.Y")] + [DataRow("string", "MyContainer1.S")] + [DataRow("MyContainer1.MyContainer2.NestedEnum", "MyContainer1.E1")] + [DataRow("MyContainer1.MyEnum1", "MyContainer1.E2")] + [DataRow("MyEnum3", "MyContainer1.E3")] + [DataRow("int", "MyContainer1.MyContainer2.X")] + [DataRow("float", "MyContainer1.MyContainer2.Y")] + [DataRow("string", "MyContainer1.MyContainer2.S")] + [DataRow("MyContainer1.MyContainer2.NestedEnum", "MyContainer1.MyContainer2.E1")] + [DataRow("MyContainer1.MyEnum1", "MyContainer1.MyContainer2.E2")] + [DataRow("MyEnum3", "MyContainer1.MyContainer2.E3")] + public async Task SimpleProperty_WithExplicitValue_NamedConstant(string propertyType, string defaultValue) + { + const string types = """ + public class MyContainer1 + { + public const int X = 42; + public const float Y = 3.14f; + public const string S = "Test"; + public const MyContainer2.NestedEnum E1 = MyContainer2.NestedEnum.B; + public const MyEnum1 E2 = MyEnum1.B; + public const MyEnum3 E3 = MyEnum3.B; + + public enum MyEnum1 { A, B }; + + public struct MyContainer2 + { + public const int X = 42; + public const float Y = 3.14f; + public const string S = "Test"; + public const NestedEnum E1 = NestedEnum.B; + public const MyEnum1 E2 = MyEnum1.B; + public const MyEnum3 E3 = MyEnum3.B; + + public enum NestedEnum { A, B }; + } + } + + public enum MyEnum3 { A, B } + """; + + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValue}})); + + public {{propertyType}} [|Name|] + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + {{types}} + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValue}})] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + + {{types}} + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + [TestMethod] [DataRow("[A]", "[static: A]")] [DataRow("""[Test(42, "Hello")]""", """[static: Test(42, "Hello")]""")] From 21c11bfd4fd9a2745b64bac595738995c9f929a4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 14:56:31 +0100 Subject: [PATCH 140/200] Add 'PropertyType' option to attribute --- .../DependencyPropertyGenerator.Execute.cs | 42 ++++++++++++++++++- .../DependencyPropertyGenerator.cs | 9 +++- .../Models/DependencyPropertyInfo.cs | 2 + .../GeneratedDependencyPropertyAttribute.cs | 21 +++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index 1773b993f..ce49568d9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -236,6 +236,43 @@ public static bool TryGetAccessibilityModifiers( return true; } + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// + /// The input instance. + /// The input that triggered the annotation. + /// The type name for the generated property (without nullability annotations). + /// The type name for the generated property, including nullability annotations. + /// The type name for the metadata declaration of the property, if explicitly set. + public static void GetPropertyTypes( + IPropertySymbol propertySymbol, + AttributeData attributeData, + out string typeName, + out string typeNameWithNullabilityAnnotations, + out string? metadataTypeName) + { + // These type names are always present and directly derived from the property type + typeName = propertySymbol.Type.GetFullyQualifiedName(); + typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + + // Check if the user has specified an explicit property type to use in metadata + if (attributeData.TryGetNamedArgument("PropertyType", out TypedConstant propertyType)) + { + // Also make sure we do have a type. We don't need to perform additional validation here, since + // the resulting code will always compile even if the type isn't actually compatible. We can do + // that validation just from an analizer, and emit warnings if the requested type is incorrect. + if (propertyType is { Kind: TypedConstantKind.Type, IsNull: false, Value: ITypeSymbol typeSymbol }) + { + metadataTypeName = typeSymbol.GetFullyQualifiedName(); + + return; + } + } + + // By default, we'll just match the declared property type + metadataTypeName = null; + } + /// /// Gets the default value to use to initialize the generated property, if explicitly specified. /// @@ -589,10 +626,13 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) writer.WriteLine($"[{attributeInfo}]"); } + // Use the explicitly requested type name, if present, or the declared property type otherwise + string propertyType = propertyInfo.MetadataTypeName ?? propertyInfo.TypeName; + writer.Write($$""" public static readonly global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}} {{propertyInfo.PropertyName}}Property = global::{{WellKnownTypeNames.DependencyProperty(propertyInfo.UseWindowsUIXaml)}}.Register( name: "{{propertyInfo.PropertyName}}", - propertyType: typeof({{propertyInfo.TypeName}}), + propertyType: typeof({{propertyType}}), ownerType: typeof({{typeQualifiedName}}), typeMetadata: """, isMultiline: true); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index 4abd428e5..a099ddfc4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -89,8 +89,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - string typeName = propertySymbol.Type.GetFullyQualifiedName(); - string typeNameWithNullabilityAnnotations = propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); + Execute.GetPropertyTypes( + propertySymbol, + context.Attributes[0], + out string typeName, + out string typeNameWithNullabilityAnnotations, + out string? metadataTypeName); token.ThrowIfCancellationRequested(); @@ -144,6 +148,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) SetterAccessibility: setterAccessibility, TypeName: typeName, TypeNameWithNullabilityAnnotations: typeNameWithNullabilityAnnotations, + MetadataTypeName: metadataTypeName, DefaultValue: defaultValue, IsReferenceTypeOrUnconstraindTypeParameter: isReferenceTypeOrUnconstraindTypeParameter, IsLocalCachingEnabled: isLocalCachingEnabled, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs index a53c35f6f..6731d3c31 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -18,6 +18,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// The accessibility of the accessor, if available. /// The type name for the generated property (without nullability annotations). /// The type name for the generated property, including nullability annotations. +/// The type name for the metadata declaration of the property, if explicitly set (otherwise, will be used). /// The default value to set the generated property to. /// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether local caching should be used for the property value. @@ -35,6 +36,7 @@ internal sealed record DependencyPropertyInfo( Accessibility SetterAccessibility, string TypeName, string TypeNameWithNullabilityAnnotations, + string? MetadataTypeName, DependencyPropertyDefaultValue DefaultValue, bool IsReferenceTypeOrUnconstraindTypeParameter, bool IsLocalCachingEnabled, diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs index 368fe5fc1..05bfa86b5 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -41,7 +41,7 @@ namespace CommunityToolkit.WinUI; public sealed class GeneratedDependencyPropertyAttribute : Attribute { /// - /// Gets a value indicating the default value to set for the property. + /// Gets or sets a value indicating the default value to set for the property. /// /// /// @@ -79,4 +79,23 @@ public sealed class GeneratedDependencyPropertyAttribute : Attribute /// properties might also be set outside of the partial property implementation, meaning caching would be invalid. /// public bool IsLocalCacheEnabled { get; init; } = false; + + /// + /// Gets or sets the type to use to register the property in metadata. The default value will exactly match the property type. + /// + /// + /// + /// This property allows customizing the property type in metadata, in advanced scenarios. For instance, it can be used to define + /// properties of a type (e.g. ) as just using in metadata. + /// This allows working around some issues primarily around classic (reflection-based) binding in XAML. + /// + /// + /// This property should only be set when actually required (e.g. to ensure a specific scenario can work). The default behavior + /// (i.e. the property type in metadata matching the declared property type) should work correctly in the vast majority of cases. + /// + /// +#if NET8_0_OR_GREATER + [DisallowNull] +#endif + public Type? PropertyType { get; init; } = null!; } From 454ae8fdd764e7e99e8c6fae924c8b210a7ebb2a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 15:05:24 +0100 Subject: [PATCH 141/200] Add unit tests for new option --- .../Test_DependencyPropertyGenerator.cs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 0d1d2b163..af2fe64a5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4398,4 +4398,136 @@ public partial bool IsSelected CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + + [TestMethod] + [DataRow("bool", "null", "bool", "object")] + [DataRow("bool", "bool", "bool", "object")] + [DataRow("bool", "object", "object", "object")] + [DataRow("bool?", "null", "bool?", "object?")] + [DataRow("bool?", "bool?", "bool?", "object?")] + [DataRow("bool?", "object", "object", "object?")] + [DataRow("bool?", "bool", "bool", "object?")] + [DataRow("string?", "null", "string", "object?")] + [DataRow("string?", "string", "string", "object?")] + [DataRow("string?", "object", "object", "object?")] + public void SingleProperty_WithCustomMetadataType_WithNoCaching( + string declaredType, + string propertyType, + string generatedPropertyType, + string boxedType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyControl : DependencyObject + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public partial {{declaredType}} IsSelected { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyControl + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty IsSelectedProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "IsSelected", + propertyType: typeof({{generatedPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{declaredType}} IsSelected + { + get + { + object? __boxedValue = GetValue(IsSelectedProperty); + + OnIsSelectedGet(ref __boxedValue); + + {{declaredType}} __unboxedValue = ({{declaredType}})__boxedValue; + + OnIsSelectedGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnIsSelectedSet(ref value); + + object? __boxedValue = value; + + OnIsSelectedSet(ref __boxedValue); + + SetValue(IsSelectedProperty, __boxedValue); + + OnIsSelectedChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{declaredType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{declaredType}} propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedChanged({{declaredType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); + } } From 9676746d68f4bb746cf450f31a6ef1c715f486a9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 16:38:29 +0100 Subject: [PATCH 142/200] Add 'IsNullableValueType' extensions, code tweaks --- ...ropertyDefaultValueCallbackTypeAnalyzer.cs | 11 +------- ...InvalidPropertyDefaultValueTypeAnalyzer.cs | 2 +- ...endencyPropertyOnManualPropertyAnalyzer.cs | 2 +- .../Extensions/ITypeSymbolExtensions.cs | 26 +++++++++++++++++++ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs index b15719f1f..945e7e97e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -146,16 +146,7 @@ public static bool IsDefaultValueCallbackValid(IPropertySymbol propertySymbol, I return true; } - bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; - // Otherwise, try to see if the return is the type argument of a nullable value type - if (isNullableValueType && - methodSymbol.ReturnType.TypeKind is TypeKind.Struct && - SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0], methodSymbol.ReturnType)) - { - return true; - } - - return false; + return propertySymbol.Type.IsNullableValueTypeWithUnderlyingType(methodSymbol.ReturnType); } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs index 485819482..32967611e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -68,7 +68,7 @@ public override void Initialize(AnalysisContext context) return; } - bool isNullableValueType = propertySymbol.Type is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + bool isNullableValueType = propertySymbol.Type.IsNullableValueType(); bool isNullableType = !propertySymbol.Type.IsValueType || isNullableValueType; // If the value is 'null', handle all possible cases: diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 455260b25..b1ed4b073 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -443,7 +443,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - bool isNullableValueType = propertyTypeSymbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + bool isNullableValueType = propertyTypeSymbol.IsNullableValueType(); // Check whether the value is a default constant value. If it is, then the property is valid (no explicit value). // We need to special case nullable value types, as the default value for the underlying type is not the actual default. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index c7306082b..c8d00e3dd 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -24,6 +24,32 @@ public static bool IsDefaultValueNull(this ITypeSymbol symbol) return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; } + /// + /// Checks whether a given type symbol represents some type. + /// + /// The input instance to check. + /// Whether represents some type. + public static bool IsNullableValueType(this ITypeSymbol symbol) + { + return symbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; + } + + /// + /// Checks whether a given type symbol represents a type with a specific underlying type. + /// + /// The input instance to check. + /// The underlyign type to check. + /// Whether represents a type with a specific underlying type. + public static bool IsNullableValueTypeWithUnderlyingType(this ITypeSymbol symbol, ITypeSymbol underlyingType) + { + if (!IsNullableValueType(symbol)) + { + return false; + } + + return SymbolEqualityComparer.Default.Equals(((INamedTypeSymbol)symbol).TypeArguments[0], underlyingType); + } + /// /// Tries to get the default value of a given enum type. /// From ce6246ef19efad0439261b86ee92f91ceb599159 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 16:38:54 +0100 Subject: [PATCH 143/200] Add 'ExplicitPropertyMetadataTypeAnalyzer' and tests --- .../AnalyzerReleases.Shipped.md | 2 + .../ExplicitPropertyMetadataTypeAnalyzer.cs | 93 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 26 ++++++ .../Test_Analyzers.cs | 89 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 8b88e411d..4acea5e84 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -28,3 +28,5 @@ WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | WCTDP0020 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0021 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0022 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0023 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs new file mode 100644 index 000000000..ca1ca3acb --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs @@ -0,0 +1,93 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates diagnostics when using [GeneratedDependencyProperty] with 'PropertyType' set incorrectly. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ExplicitPropertyMetadataTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + UnnecessaryDependencyPropertyExplicitMetadataType, + IncompatibleDependencyPropertyExplicitMetadataType + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // If an explicit property type isn't set, there's nothing to do + if (!attributeData.TryGetNamedArgument("PropertyType", out TypedConstant propertyType)) + { + return; + } + + // Special case for 'null': this will already warn due to nullability annotations, nothing more to do here either. + // This will also catch invalid types, which Roslyn will already emit errors for (so we can ignore them as well). + if (propertyType is not { Kind: TypedConstantKind.Type, IsNull: false, Value: ITypeSymbol typeSymbol }) + { + return; + } + + // If the explicit type matches the property type, then it's unnecessary, so we can warn and stop here + if (SymbolEqualityComparer.Default.Equals(propertySymbol.Type, typeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + UnnecessaryDependencyPropertyExplicitMetadataType, + attributeData.GetLocation(), + propertySymbol, + propertySymbol.Type)); + + return; + } + + // If the explicit type is not compatible (i.e. there's no implicit conversion and the type is not the underlying nullable type), emit an error. + // We also emit the same diagnostic if the explicit type is the nullable version of the property type, as that is not a supported scenario. + if (typeSymbol.IsNullableValueTypeWithUnderlyingType(propertySymbol.Type) || + (!context.Compilation.HasImplicitConversion(propertySymbol.Type, typeSymbol)) && + !propertySymbol.Type.IsNullableValueTypeWithUnderlyingType(typeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + IncompatibleDependencyPropertyExplicitMetadataType, + attributeData.GetLocation(), + propertySymbol, + typeSymbol, + propertySymbol.Type)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 9d1b5b5bb..b4bee3710 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -298,4 +298,30 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "All dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types.", helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)". + /// + public static readonly DiagnosticDescriptor UnnecessaryDependencyPropertyExplicitMetadataType = new( + id: "WCTDP0022", + title: "Unnecessary dependency property explicit metadata type", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' should only do so when the explicit type would not match the declared property type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)". + /// + public static readonly DiagnosticDescriptor IncompatibleDependencyPropertyExplicitMetadataType = new( + id: "WCTDP0023", + title: "Incompatible dependency property explicit metadata type", + messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' must do so with a type that is compatible with the declared property type.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 58996f02e..85d6ea9a0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1996,4 +1996,93 @@ public class MyObject : DependencyObject await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + + [TestMethod] + public async Task ExplicitPropertyMetadataTypeAnalyzer_NoAttribute_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + public string? Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("string", "object")] + [DataRow("MyObject", "DependencyObject")] + [DataRow("MyObject", "IMyInterface")] + [DataRow("double?", "object")] + [DataRow("double?", "double")] + public async Task ExplicitPropertyMetadataTypeAnalyzer_ValidExplicitType_Warns(string declaredType, string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyInterface + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public {{declaredType}} Name { get; set; } + } + + public interface IMyInterface; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object")] + [DataRow("string")] + [DataRow("MyObject")] + [DataRow("DependencyObject")] + [DataRow("IMyInterface")] + [DataRow("double?")] + [DataRow("double")] + public async Task ExplicitPropertyMetadataTypeAnalyzer_SameType_Warns(string type) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject, IMyInterface + { + [{|WCTDP0022:GeneratedDependencyProperty(PropertyType = typeof({{type}}))|}] + public {{type}} Name { get; set; } + } + + public interface IMyInterface; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("object", "string")] + [DataRow("DependencyObject", "MyObject")] + [DataRow("MyObject", "IMyInterface")] + [DataRow("double", "double?")] + [DataRow("double?", "IMyInterface")] + public async Task ExplicitPropertyMetadataTypeAnalyzer_IncompatibleType_Warns(string declaredType, string propertyType) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + public class MyObject : DependencyObject + { + [{|WCTDP0023:GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))|}] + public {{declaredType}} Name { get; set; } + } + + public interface IMyInterface; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } } From e1e1c512778ab267b29bbee2222b4791a0b331c4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 16:50:59 +0100 Subject: [PATCH 144/200] Handle invalid boxing conversions --- .../ExplicitPropertyMetadataTypeAnalyzer.cs | 17 +++++++++++++++-- .../Test_Analyzers.cs | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs index ca1ca3acb..450ffb83f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs @@ -76,9 +76,22 @@ public override void Initialize(AnalysisContext context) // If the explicit type is not compatible (i.e. there's no implicit conversion and the type is not the underlying nullable type), emit an error. // We also emit the same diagnostic if the explicit type is the nullable version of the property type, as that is not a supported scenario. - if (typeSymbol.IsNullableValueTypeWithUnderlyingType(propertySymbol.Type) || + bool isPropertyTypeIncompatible = + typeSymbol.IsNullableValueTypeWithUnderlyingType(propertySymbol.Type) || (!context.Compilation.HasImplicitConversion(propertySymbol.Type, typeSymbol)) && - !propertySymbol.Type.IsNullableValueTypeWithUnderlyingType(typeSymbol)) + !propertySymbol.Type.IsNullableValueTypeWithUnderlyingType(typeSymbol); + + // Special case: we want to also block incompatible assignments that would have an implicit conversion (eg. 'float' -> 'double') + if (propertySymbol.Type.IsValueType && + typeSymbol.IsValueType && + !propertySymbol.Type.IsNullableValueType() && + !typeSymbol.IsNullableValueType() && + !SymbolEqualityComparer.Default.Equals(propertySymbol.Type, typeSymbol)) + { + isPropertyTypeIncompatible = true; + } + + if (isPropertyTypeIncompatible) { context.ReportDiagnostic(Diagnostic.Create( IncompatibleDependencyPropertyExplicitMetadataType, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 85d6ea9a0..4eb2a7c2d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2068,6 +2068,8 @@ public interface IMyInterface; [DataRow("MyObject", "IMyInterface")] [DataRow("double", "double?")] [DataRow("double?", "IMyInterface")] + [DataRow("double", "float")] + [DataRow("float", "double")] public async Task ExplicitPropertyMetadataTypeAnalyzer_IncompatibleType_Warns(string declaredType, string propertyType) { string source = $$""" From 7d0b5d98ad77bff5d996464c79ed915536ac1bcf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 16:57:21 +0100 Subject: [PATCH 145/200] Minor code refactoring --- .../ExplicitPropertyMetadataTypeAnalyzer.cs | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs index 450ffb83f..39d981f0b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs @@ -74,24 +74,8 @@ public override void Initialize(AnalysisContext context) return; } - // If the explicit type is not compatible (i.e. there's no implicit conversion and the type is not the underlying nullable type), emit an error. - // We also emit the same diagnostic if the explicit type is the nullable version of the property type, as that is not a supported scenario. - bool isPropertyTypeIncompatible = - typeSymbol.IsNullableValueTypeWithUnderlyingType(propertySymbol.Type) || - (!context.Compilation.HasImplicitConversion(propertySymbol.Type, typeSymbol)) && - !propertySymbol.Type.IsNullableValueTypeWithUnderlyingType(typeSymbol); - - // Special case: we want to also block incompatible assignments that would have an implicit conversion (eg. 'float' -> 'double') - if (propertySymbol.Type.IsValueType && - typeSymbol.IsValueType && - !propertySymbol.Type.IsNullableValueType() && - !typeSymbol.IsNullableValueType() && - !SymbolEqualityComparer.Default.Equals(propertySymbol.Type, typeSymbol)) - { - isPropertyTypeIncompatible = true; - } - - if (isPropertyTypeIncompatible) + // Emit a diagnostic if the explicit type is incompatible + if (!IsValidPropertyMetadataType(propertySymbol.Type, typeSymbol, context.Compilation)) { context.ReportDiagnostic(Diagnostic.Create( IncompatibleDependencyPropertyExplicitMetadataType, @@ -103,4 +87,39 @@ public override void Initialize(AnalysisContext context) }, SymbolKind.Property); }); } + + /// + /// Checks whether a given type is a valid property type for metadata, for a given dependency property. + /// + /// The property type on the property definition. + /// The type to use for the property declaration in metadata. + /// The instance for the current compilation. + /// Whether the target property type to use in metadata is valid for the property declaration. + internal static bool IsValidPropertyMetadataType(ITypeSymbol declaredPropertyTypeSymbol, ITypeSymbol explicitPropertyTypeSymbol, Compilation compilation) + { + // If the explicit type is the nullable version of the property type, that is not a supported scenario + if (explicitPropertyTypeSymbol.IsNullableValueTypeWithUnderlyingType(declaredPropertyTypeSymbol)) + { + return false; + } + + // Common check for whether the explicit type is not compatible (i.e. there's no implicit conversion and the type is not the underlying nullable type) + if (!compilation.HasImplicitConversion(declaredPropertyTypeSymbol, explicitPropertyTypeSymbol) && + !declaredPropertyTypeSymbol.IsNullableValueTypeWithUnderlyingType(explicitPropertyTypeSymbol)) + { + return false; + } + + // Special case: we want to also block incompatible assignments that would have an implicit conversion (eg. 'float' -> 'double') + if (declaredPropertyTypeSymbol.IsValueType && + explicitPropertyTypeSymbol.IsValueType && + !declaredPropertyTypeSymbol.IsNullableValueType() && + !explicitPropertyTypeSymbol.IsNullableValueType() && + !SymbolEqualityComparer.Default.Equals(declaredPropertyTypeSymbol, explicitPropertyTypeSymbol)) + { + return false; + } + + return true; + } } From 04a34651780b0a12d81a4d6b98d880c9918dcb10 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 31 Dec 2024 19:05:17 +0100 Subject: [PATCH 146/200] Support 'PropertyType' in code fixer, add tests --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 212 +++++++----- ...endencyPropertyOnManualPropertyAnalyzer.cs | 32 +- .../Test_Analyzers.cs | 35 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 317 +++++++++++++++++- 4 files changed, 501 insertions(+), 95 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 99c25db88..c9c045baa 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -46,12 +46,6 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) Diagnostic diagnostic = context.Diagnostics[0]; TextSpan diagnosticSpan = context.Span; - // We always expect the field location to be the first additional location, this must be present - if (diagnostic.AdditionalLocations is not [{ } fieldLocation, ..]) - { - return; - } - // This code fixer needs the semantic model, so check that first if (!context.Document.SupportsSemanticModel) { @@ -62,8 +56,12 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; - // Get any additional locations, if available - Location? defaultValueExpressionLocation = diagnostic.AdditionalLocations.ElementAtOrDefault(1); + // Get all additional locations we expect from the analyzer + GetAdditionalLocations( + diagnostic, + out Location fieldLocation, + out Location propertyTypeExpressionLocation, + out Location defaultValueExpressionLocation); SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); @@ -84,6 +82,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) root, propertyDeclaration, fieldDeclaration, + propertyTypeExpressionLocation, defaultValue, defaultValueTypeReferenceId, defaultValueExpressionLocation), @@ -128,6 +127,7 @@ private static bool TryGetGeneratedDependencyPropertyAttributeList( /// The original document being fixed. /// The instance for the current compilation. /// The original tree root belonging to the current document. + /// The location of the property type expression to use in metadata, if available. /// The expression for the default value of the property, if present /// The documentation comment reference id for type of the default value, if present. /// The location for the default value, if available. @@ -137,89 +137,114 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis SemanticModel semanticModel, SyntaxNode root, AttributeListSyntax generatedDependencyPropertyAttributeList, + Location propertyTypeExpressionLocation, string? defaultValueExpression, string? defaultValueTypeReferenceId, - Location? defaultValueExpressionLocation) + Location defaultValueExpressionLocation) { - // If we do have a default value expression, set it in the attribute. - // We extract the generated attribute so we can add the new argument. - // It's important to reuse it, as it has the "add usings" annotation. - if (defaultValueExpression is not null) + void HandlePropertyType(ref AttributeListSyntax generatedDependencyPropertyAttributeList) { - SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); - - // Special case if we have a location for the original expression, and we can resolve the node. - // In this case, we want to just carry that over with no changes (this is used for named constants). - // See notes below for how this method is constructing the new attribute argument to insert. - if (defaultValueExpressionLocation is not null && - root.FindNode(defaultValueExpressionLocation.SourceSpan) is ArgumentSyntax { Expression: { } defaultValueOriginalExpression }) + // If the property needs an explicit type in metadata, just carry it over from the original field declaration initializer + if (root.FindNode(propertyTypeExpressionLocation.SourceSpan) is ArgumentSyntax { Expression: { } propertyTypeOriginalExpression }) { - return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( generatedDependencyPropertyAttributeList, - [syntaxGenerator.AttributeArgument("DefaultValue", defaultValueOriginalExpression)]); + [syntaxGenerator.AttributeArgument("PropertyType", propertyTypeOriginalExpression)]); } + } - ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); - - // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'. - // We have two cases to handle, which require different logic to ensure the correct tree is always generated: - // - For nested enum types, we'll have a reference id. In this case, we manually insert annotations. - // - For normal enum member accesses, we resolve the type and then construct the tree from that expression. - if (defaultValueTypeReferenceId is not null) - { - // Here we're relying on the special 'SymbolId' annotation, which is used internally by Roslyn to track - // necessary imports for type expressions. We need to rely on this implementation detail here because - // there is no public API to correctly produce a tree for an enum member access on a nested type. - // This internal detail is one that many generators take a dependency on already, so it's safe-ish. - parsedExpression = parsedExpression.WithAdditionalAnnotations( - Simplifier.Annotation, - Simplifier.AddImportsAnnotation, - new SyntaxAnnotation("SymbolId", defaultValueTypeReferenceId)); - - // Create the attribute argument to insert - SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", parsedExpression); - - // Actually add the argument to the existing attribute syntax - return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); - } - else if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + void HandleDefaultValue(ref AttributeListSyntax generatedDependencyPropertyAttributeList) + { + // If we do have a default value expression, set it in the attribute. + // We extract the generated attribute so we can add the new argument. + // It's important to reuse it, as it has the "add usings" annotation. + if (defaultValueExpression is not null) { - string fullyQualifiedMetadataName = expressionSyntax.ToFullString(); + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); - // Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name) - if (fullyQualifiedMetadataName.StartsWith("global::")) + // Special case if we have a location for the original expression, and we can resolve the node. + // In this case, we want to just carry that over with no changes (this is used for named constants). + // See notes below for how this method is constructing the new attribute argument to insert. + if (root.FindNode(defaultValueExpressionLocation.SourceSpan) is ArgumentSyntax { Expression: { } defaultValueOriginalExpression }) { - fullyQualifiedMetadataName = fullyQualifiedMetadataName["global::".Length..]; + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", defaultValueOriginalExpression)]); + + return; } - // Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not - // a fully qualified type name. However, for virtually all cases for enum types, the two should match. - // That is, they will be the same if the type is not nested, and not generic, which is what we expect. - if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) is INamedTypeSymbol enumTypeSymbol) - { - // Create the identifier syntax for the enum type, with the right annotations - SyntaxNode enumTypeSyntax = syntaxGenerator.TypeExpression(enumTypeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + ExpressionSyntax parsedExpression = ParseExpression(defaultValueExpression); - // Create the member access expression for the target enum type - SyntaxNode enumMemberAccessExpressionSyntax = syntaxGenerator.MemberAccessExpression(enumTypeSyntax, memberName); + // Special case values which are simple enum member accesses, like 'global::Windows.UI.Xaml.Visibility.Collapsed'. + // We have two cases to handle, which require different logic to ensure the correct tree is always generated. + // For nested enum types, we'll have a reference id. In this case, we manually insert annotations. + if (defaultValueTypeReferenceId is not null) + { + // Here we're relying on the special 'SymbolId' annotation, which is used internally by Roslyn to track + // necessary imports for type expressions. We need to rely on this implementation detail here because + // there is no public API to correctly produce a tree for an enum member access on a nested type. + // This internal detail is one that many generators take a dependency on already, so it's safe-ish. + parsedExpression = parsedExpression.WithAdditionalAnnotations( + Simplifier.Annotation, + Simplifier.AddImportsAnnotation, + new SyntaxAnnotation("SymbolId", defaultValueTypeReferenceId)); + + // Create the attribute argument to insert + SyntaxNode attributeArgumentSyntax = syntaxGenerator.AttributeArgument("DefaultValue", parsedExpression); + + // Actually add the argument to the existing attribute syntax + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments(generatedDependencyPropertyAttributeList, [attributeArgumentSyntax]); + + return; + } - // Create the attribute argument, like in the previous case - return (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( - generatedDependencyPropertyAttributeList, - [syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax)]); + // For normal enum member accesses, we resolve the type and then construct the tree from that expression. + if (parsedExpression is MemberAccessExpressionSyntax { Expression: { } expressionSyntax, Name: IdentifierNameSyntax { Identifier.Text: { } memberName } }) + { + string fullyQualifiedMetadataName = expressionSyntax.ToFullString(); + + // Ensure we strip the global prefix, if present (it should always be present if we didn't have a metadata name) + if (fullyQualifiedMetadataName.StartsWith("global::")) + { + fullyQualifiedMetadataName = fullyQualifiedMetadataName["global::".Length..]; + } + + // Try to resolve the attribute type, if present. This API takes a fully qualified metadata name, not + // a fully qualified type name. However, for virtually all cases for enum types, the two should match. + // That is, they will be the same if the type is not nested, and not generic, which is what we expect. + if (semanticModel.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) is INamedTypeSymbol enumTypeSymbol) + { + // Create the identifier syntax for the enum type, with the right annotations + SyntaxNode enumTypeSyntax = syntaxGenerator.TypeExpression(enumTypeSymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + + // Create the member access expression for the target enum type + SyntaxNode enumMemberAccessExpressionSyntax = syntaxGenerator.MemberAccessExpression(enumTypeSyntax, memberName); + + // Create the attribute argument, like in the previous case + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", enumMemberAccessExpressionSyntax)]); + + return; + } } - } - // Otherwise, just add the new default value normally - return - AttributeList(SingletonSeparatedList( - generatedDependencyPropertyAttributeList.Attributes[0] - .AddArgumentListArguments( - AttributeArgument(ParseExpression(defaultValueExpression)) - .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); + // Otherwise, just add the new default value normally + generatedDependencyPropertyAttributeList = + AttributeList(SingletonSeparatedList( + generatedDependencyPropertyAttributeList.Attributes[0] + .AddArgumentListArguments( + AttributeArgument(ParseExpression(defaultValueExpression)) + .WithNameEquals(NameEquals(IdentifierName("DefaultValue")))))); + } } - // If we have no value expression, we can just reuse the attribute with no changes + HandlePropertyType(ref generatedDependencyPropertyAttributeList); + HandleDefaultValue(ref generatedDependencyPropertyAttributeList); + return generatedDependencyPropertyAttributeList; } @@ -231,6 +256,7 @@ private static AttributeListSyntax UpdateGeneratedDependencyPropertyAttributeLis /// The original tree root belonging to the current document. /// The for the property being updated. /// The for the declared property to remove. + /// The location of the property type expression to use in metadata, if available. /// The expression for the default value of the property, if present /// The documentation comment reference id for type of the default value, if present. /// The location for the default value, if available. @@ -241,9 +267,10 @@ private static async Task ConvertToPartialProperty( SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration, FieldDeclarationSyntax fieldDeclaration, + Location propertyTypeExpressionLocation, string? defaultValueExpression, string? defaultValueTypeReferenceId, - Location? defaultValueExpressionLocation) + Location defaultValueExpressionLocation) { await Task.CompletedTask; @@ -264,6 +291,7 @@ private static async Task ConvertToPartialProperty( fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, + propertyTypeExpressionLocation, defaultValueExpression, defaultValueTypeReferenceId, defaultValueExpressionLocation); @@ -284,6 +312,7 @@ private static async Task ConvertToPartialProperty( /// The for the declared property to remove. /// The with the attribute to add. /// The instance to use. + /// The location of the property type expression to use in metadata, if available. /// The expression for the default value of the property, if present /// The documentation comment reference id for type of the default value, if present. /// The location for the default value, if available. @@ -296,9 +325,10 @@ private static void ConvertToPartialProperty( FieldDeclarationSyntax fieldDeclaration, AttributeListSyntax generatedDependencyPropertyAttributeList, SyntaxEditor syntaxEditor, + Location propertyTypeExpressionLocation, string? defaultValueExpression, string? defaultValueTypeReferenceId, - Location? defaultValueExpressionLocation) + Location defaultValueExpressionLocation) { // Replace the property with the partial property using the attribute. Note that it's important to use the // lambda 'ReplaceNode' overload here, rather than creating a modifier property declaration syntax node and @@ -314,6 +344,7 @@ private static void ConvertToPartialProperty( semanticModel, root, generatedDependencyPropertyAttributeList, + propertyTypeExpressionLocation, defaultValueExpression, defaultValueTypeReferenceId, defaultValueExpressionLocation); @@ -467,6 +498,25 @@ private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollection + /// Gets the additional locations provided by the analyzer. + /// + /// The instance currently being processed. + /// The location of the field to remove. + /// The location of the property type expression to use in metadata. + /// The location for the default value. + private static void GetAdditionalLocations( + Diagnostic diagnostic, + out Location fieldLocation, + out Location propertyTypeExpressionLocation, + out Location defaultValueExpressionLocation) + { + // We always expect 3 additional locations, as per contract with the analyzer + fieldLocation = diagnostic.AdditionalLocations[0]; + propertyTypeExpressionLocation = diagnostic.AdditionalLocations[1]; + defaultValueExpressionLocation = diagnostic.AdditionalLocations[2]; + } + /// /// A custom with the logic from . /// @@ -508,9 +558,15 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider continue; } + // Get all additional locations we expect from the analyzer + GetAdditionalLocations( + diagnostic, + out Location fieldLocation, + out Location propertyTypeExpressionLocation, + out Location defaultValueExpressionLocation); + // Also check that we can find the target field to remove - if (diagnostic.AdditionalLocations is not [{ } fieldLocation, ..] || - root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) + if (root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) { continue; } @@ -519,9 +575,6 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; - // Get any additional locations, if available - Location? defaultValueExpressionLocation = diagnostic.AdditionalLocations.ElementAtOrDefault(1); - ConvertToPartialProperty( document, semanticModel, @@ -530,6 +583,7 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider fieldDeclaration, generatedDependencyPropertyAttributeList, syntaxEditor, + propertyTypeExpressionLocation, defaultValue, defaultValueTypeReferenceId, defaultValueExpressionLocation); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index b1ed4b073..46e5667dd 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -534,6 +534,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyName = propertyName; fieldFlags.PropertyType = propertyTypeSymbol; + fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); fieldFlags.FieldLocation = fieldDeclaration.GetLocation(); }, OperationKind.FieldInitializer); @@ -563,25 +564,44 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // We only support rewriting when the property name matches the field being initialized. // Note that the property name here is the literal being passed for the 'name' parameter. - if (fieldFlags.PropertyName != pair.Key.Name) + if (pair.Key.Name != fieldFlags.PropertyName) { continue; } // Make sure that the 'propertyType' value matches the actual type of the property. // We are intentionally not handling combinations of nullable value types here. - if (!SymbolEqualityComparer.Default.Equals(fieldFlags.PropertyType, pair.Key.Type)) + if (!SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType) && + (fieldFlags.PropertyType is null || !ExplicitPropertyMetadataTypeAnalyzer.IsValidPropertyMetadataType(pair.Key.Type, fieldFlags.PropertyType, context.Compilation))) { continue; } + // If the property type is an exact match for the property type in metadata, we can remove the location of that argument. + // This is because in that case there's no need to forward this type explicitly: the type will be the same by default. + if (SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType)) + { + fieldFlags.PropertyTypeExpressionLocation = null; + } + // Finally, check whether the field was valid (if so, we will have a valid location) if (fieldFlags.FieldLocation is Location fieldLocation) { + // Additional locations for the code fixer. The contract is shared between the analyzer and the code fixer: + // 1) The field location (this is always present) + // 2) The location of the 'typeof(...)' expression for the property type, if present + // 3) The location of the default value expression, if it should be directly carried over + Location[] additionalLocations = + [ + fieldLocation, + fieldFlags.PropertyTypeExpressionLocation ?? Location.None, + fieldFlags.DefaultValueExpressionLocation ?? Location.None + ]; + context.ReportDiagnostic(Diagnostic.Create( UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), - ((Location?[])[fieldLocation, fieldFlags.DefaultValueExpressionLocation]).OfType(), + additionalLocations, ImmutableDictionary.Create() .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId), @@ -604,6 +624,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla { fieldFlags.PropertyName = null; fieldFlags.PropertyType = null; + fieldFlags.PropertyTypeExpressionLocation = null; fieldFlags.DefaultValue = null; fieldFlags.DefaultValueTypeReferenceId = null; fieldFlags.DefaultValueExpressionLocation = null; @@ -676,6 +697,11 @@ private sealed class FieldFlags /// public ITypeSymbol? PropertyType; + /// + /// The location for the expression defining the property type. + /// + public Location? PropertyTypeExpressionLocation; + /// /// The default value to use (not present if it does not need to be set explicitly). /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 4eb2a7c2d..94775ee25 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1284,7 +1284,6 @@ public string? Name [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] [DataRow("\"Name\"", "typeof(int)", "typeof(MyControl)", "null")] [DataRow("\"Name\"", "typeof(MyControl)", "typeof(MyControl)", "null")] - [DataRow("\"Name\"", "typeof(object)", "typeof(MyControl)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] @@ -1627,6 +1626,40 @@ public class MyClass { } await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("string?", "object")] + [DataRow("MyControl", "DependencyObject")] + [DataRow("double?", "object")] + [DataRow("double?", "double")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitMetadataType_Warns(string declaredType, string propertyType) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{declaredType}} {|WCTDP0017:Name|} + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b948bdb3f..8ddd36d3a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -1307,7 +1307,6 @@ public class TestAttribute(int X, string Y) : Attribute; await test.RunAsync(); } - // Using 'object' for dependency properties is sometimes needed to work around an 'IReference' issue in some binding scenarios [TestMethod] public async Task MultipleProperties_WithInterspersedNonFixableProprty_HandlesAllPossibleProperties() { @@ -1336,7 +1335,7 @@ public partial class MyObject : DependencyObject /// public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( nameof(HorizontalOffset), - typeof(object), + typeof(float), typeof(MyObject), null); @@ -1372,7 +1371,7 @@ public partial class MyObject : DependencyObject /// public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( nameof(VerticalOffset), - typeof(object), + typeof(float), typeof(MyObject), null); @@ -1388,9 +1387,9 @@ public bool [|DisableAnimation|] /// /// Gets or sets the distance should be scrolled horizontally. /// - public double? HorizontalOffset + public double HorizontalOffset { - get => (double?)GetValue(HorizontalOffsetProperty); + get => (double)(float)GetValue(HorizontalOffsetProperty); set => SetValue(HorizontalOffsetProperty, value); } @@ -1424,9 +1423,9 @@ public ScrollViewer? [|TargetScrollViewer|] /// /// Gets or sets the distance should be scrolled vertically. /// - public double? VerticalOffset + public double VerticalOffset { - get => (double?)GetValue(VerticalOffsetProperty); + get => (double)(float)GetValue(VerticalOffsetProperty); set => SetValue(VerticalOffsetProperty, value); } } @@ -1449,7 +1448,7 @@ public partial class MyObject : DependencyObject /// public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( nameof(HorizontalOffset), - typeof(object), + typeof(float), typeof(MyObject), null); @@ -1458,7 +1457,7 @@ public partial class MyObject : DependencyObject /// public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( nameof(VerticalOffset), - typeof(object), + typeof(float), typeof(MyObject), null); @@ -1471,9 +1470,9 @@ public partial class MyObject : DependencyObject /// /// Gets or sets the distance should be scrolled horizontally. /// - public double? HorizontalOffset + public double HorizontalOffset { - get => (double?)GetValue(HorizontalOffsetProperty); + get => (double)(float)GetValue(HorizontalOffsetProperty); set => SetValue(HorizontalOffsetProperty, value); } @@ -1498,7 +1497,141 @@ public double? HorizontalOffset /// /// Gets or sets the distance should be scrolled vertically. /// - public double? VerticalOffset + public double VerticalOffset + { + get => (double)(float)GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + // Using 'object' for dependency properties is sometimes needed to work around an 'IReference' issue in some binding scenarios + [TestMethod] + public async Task MultipleProperties_WithWorkaroundPropertiesForReflectionBindings_HandlesAllPossibleProperties() + { + const string original = """ + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty DisableAnimationProperty = DependencyProperty.Register( + nameof(DisableAnimation), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register( + nameof(HorizontalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsHorizontalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsHorizontalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsVerticalOffsetRelativeProperty = DependencyProperty.Register( + nameof(IsVerticalOffsetRelative), + typeof(bool), + typeof(MyObject), + new PropertyMetadata(false)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetScrollViewerProperty = DependencyProperty.Register( + nameof(TargetScrollViewer), + typeof(ScrollViewer), + typeof(MyObject), + null); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( + nameof(VerticalOffset), + typeof(object), + typeof(MyObject), + null); + + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + public bool [|DisableAnimation|] + { + get => (bool)GetValue(DisableAnimationProperty); + set => SetValue(DisableAnimationProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + public double? [|HorizontalOffset|] + { + get => (double?)GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); + } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + public bool [|IsHorizontalOffsetRelative|] + { + get => (bool)GetValue(IsHorizontalOffsetRelativeProperty); + set => SetValue(IsHorizontalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + public bool [|IsVerticalOffsetRelative|] + { + get => (bool)GetValue(IsVerticalOffsetRelativeProperty); + set => SetValue(IsVerticalOffsetRelativeProperty, value); + } + + /// + /// Gets or sets the target . + /// + public ScrollViewer? [|TargetScrollViewer|] + { + get => (ScrollViewer?)GetValue(TargetScrollViewerProperty); + set => SetValue(TargetScrollViewerProperty, value); + } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + public double? [|VerticalOffset|] { get => (double?)GetValue(VerticalOffsetProperty); set => SetValue(VerticalOffsetProperty, value); @@ -1506,6 +1639,56 @@ public double? VerticalOffset } """; + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + /// + /// Gets or sets a value indicating whether the animation is disabled. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:DisableAnimation|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled horizontally. + /// + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial double? {|CS9248:HorizontalOffset|} { get; set; } + + /// + /// Gets or sets a value indicating whether the horizontal offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsHorizontalOffsetRelative|} { get; set; } + + /// + /// Gets or sets a value indicating whether the vertical offset is relative to the current offset. The default value is . + /// + [GeneratedDependencyProperty] + public partial bool {|CS9248:IsVerticalOffsetRelative|} { get; set; } + + /// + /// Gets or sets the target . + /// + [GeneratedDependencyProperty] + public partial ScrollViewer? {|CS9248:TargetScrollViewer|} { get; set; } + + /// + /// Gets or sets the distance should be scrolled vertically. + /// + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial double? {|CS9248:VerticalOffset|} { get; set; } + } + """; + CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, @@ -1682,4 +1865,114 @@ public partial class MyNestedObject : DependencyObject await test.RunAsync(); } + + [TestMethod] + [DataRow("string?", "object")] + [DataRow("MyObject", "DependencyObject")] + [DataRow("double?", "object")] + [DataRow("double?", "double")] + public async Task SimpleProperty_ExplicitMetadataType_IsHandledCorrectly(string declaredType, string propertyType) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: null); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string?", "object", "\"\"")] + [DataRow("double?", "object", "1.0")] + [DataRow("double?", "double", "1.0")] + public async Task SimpleProperty_ExplicitMetadataType_WithDefaultValue_IsHandledCorrectly( + string declaredType, + string propertyType, + string defaultValue) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValue}}, null)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}), DefaultValue = {{defaultValue}})] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From e8b0b1b27efe746c1e876e9debc5516d1cc36017 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 01:08:46 +0100 Subject: [PATCH 147/200] Fix a crash when getting additional locations --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index c9c045baa..c63dd843f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -52,17 +52,20 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return; } + // Get all additional locations we expect from the analyzer + if (!TryGetAdditionalLocations( + diagnostic, + out Location? fieldLocation, + out Location? propertyTypeExpressionLocation, + out Location? defaultValueExpressionLocation)) + { + return; + } + // Retrieve the properties passed by the analyzer string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName]; string? defaultValueTypeReferenceId = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValueTypeReferenceIdPropertyName]; - // Get all additional locations we expect from the analyzer - GetAdditionalLocations( - diagnostic, - out Location fieldLocation, - out Location propertyTypeExpressionLocation, - out Location defaultValueExpressionLocation); - SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); // Get the property declaration and the field declaration from the target diagnostic @@ -505,16 +508,29 @@ private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollectionThe location of the field to remove. /// The location of the property type expression to use in metadata. /// The location for the default value. - private static void GetAdditionalLocations( + /// Whether the additional locations were retrieved correctly. + private static bool TryGetAdditionalLocations( Diagnostic diagnostic, - out Location fieldLocation, - out Location propertyTypeExpressionLocation, - out Location defaultValueExpressionLocation) + [NotNullWhen(true)] out Location? fieldLocation, + [NotNullWhen(true)] out Location? propertyTypeExpressionLocation, + [NotNullWhen(true)] out Location? defaultValueExpressionLocation) { - // We always expect 3 additional locations, as per contract with the analyzer - fieldLocation = diagnostic.AdditionalLocations[0]; - propertyTypeExpressionLocation = diagnostic.AdditionalLocations[1]; - defaultValueExpressionLocation = diagnostic.AdditionalLocations[2]; + // We always expect 3 additional locations, as per contract with the analyzer. + // Do a sanity check just in case, as we've seen sporadic issues with this. + if (diagnostic.AdditionalLocations is not [{ } location1, { } location2, { } location3]) + { + fieldLocation = null; + propertyTypeExpressionLocation = null; + defaultValueExpressionLocation = null; + + return false; + } + + fieldLocation = location1; + propertyTypeExpressionLocation = location2; + defaultValueExpressionLocation = location3; + + return true; } /// @@ -559,11 +575,14 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider } // Get all additional locations we expect from the analyzer - GetAdditionalLocations( + if (!TryGetAdditionalLocations( diagnostic, - out Location fieldLocation, - out Location propertyTypeExpressionLocation, - out Location defaultValueExpressionLocation); + out Location? fieldLocation, + out Location? propertyTypeExpressionLocation, + out Location? defaultValueExpressionLocation)) + { + continue; + } // Also check that we can find the target field to remove if (root.FindNode(fieldLocation.SourceSpan) is not FieldDeclarationSyntax fieldDeclaration) From 2ae6c2a53676628a10ac40dd987c8f6aad02a0ab Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 12:21:51 +0100 Subject: [PATCH 148/200] Fix handling of additional locations for code fixer --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 67 +++++++++++++++++-- ...endencyPropertyOnManualPropertyAnalyzer.cs | 38 ++++++++++- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index c63dd843f..c8382510e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2,6 +2,7 @@ // 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.Composition; @@ -464,7 +465,7 @@ private static void RemoveLeftoverLeadingEndOfLines(IReadOnlyCollection public const string DefaultValueTypeReferenceIdPropertyName = "DefaultValueTypeReferenceId"; + /// + /// The property name for the serialized value. + /// + public const string AdditionalLocationKindPropertyName = "AdditionalLocationKind"; + /// public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; @@ -598,13 +604,21 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.DefaultValueExpressionLocation ?? Location.None ]; + // Track the available locations, so we can extract them back. We cannot rely on the length + // of the supplied array, because Roslyn will remove all 'None' locations from the array. + AdditionalLocationKind additionalLocationKind = + AdditionalLocationKind.FieldLocation | + (additionalLocations[1] != Location.None ? AdditionalLocationKind.PropertyTypeExpressionLocation : 0) | + (additionalLocations[2] != Location.None ? AdditionalLocationKind.DefaultValueExpressionLocation : 0); + context.ReportDiagnostic(Diagnostic.Create( UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), additionalLocations, ImmutableDictionary.Create() .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) - .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId), + .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId) + .Add(AdditionalLocationKindPropertyName, additionalLocationKind.ToString()), pair.Key)); } } @@ -723,3 +737,25 @@ private sealed class FieldFlags public Location? FieldLocation; } } + +/// +/// An enum indicating the type of additional locations that are produced by the analyzer. +/// +[Flags] +public enum AdditionalLocationKind +{ + /// + /// The location of the field to remove, always present. + /// + FieldLocation = 1 << 0, + + /// + /// The location of the property type expression, if present. + /// + PropertyTypeExpressionLocation = 1 << 1, + + /// + /// The location of the default value expression, if present. + /// + DefaultValueExpressionLocation = 1 << 2 +} From 6d8f6e29017707bd3d05bafac9b2a57a105c3ff6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 12:53:33 +0100 Subject: [PATCH 149/200] Remove workaround for unit test runner --- ...endencyPropertyOnManualPropertyCodeFixer.cs | 18 ------------------ ...pendencyPropertyOnManualPropertyAnalyzer.cs | 15 ++++++++------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index c8382510e..6a1e26d1c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -536,14 +536,6 @@ bool TryExtractAdditionalLocation(AdditionalLocationKind currentLocationKind, [N // Ensure the current kind is present and that we can extract an additional location if (!additionalLocationKind.HasFlag(currentLocationKind)) { - // Special case for the unit test runner. If we have 3 diagnostics (see details on the contract in the analyzer), - // it means we're running in the test runner, which does not remove 'None' diagnostics. In this case, we need to - // increment the current index, or otherwise we'll read incorrect locations after this one. - if (diagnostic.AdditionalLocations.Count == 3) - { - currentLocationIndex++; - } - fieldLocation = null; return false; @@ -552,16 +544,6 @@ bool TryExtractAdditionalLocation(AdditionalLocationKind currentLocationKind, [N // Parse the additional location fieldLocation = diagnostic.AdditionalLocations[currentLocationIndex++]; - // Special case: if the location is 'None', we should ignore it. This is because while the location is - // available when running tests, it will be removed when running in the IDE, because the serialization - // logic that Roslyn uses will filter out all 'None' locations. This step is needed to match that logic. - if (fieldLocation == Location.None) - { - fieldLocation = null; - - return false; - } - return true; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 94c33a94f..ff702630d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -597,24 +597,25 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // 1) The field location (this is always present) // 2) The location of the 'typeof(...)' expression for the property type, if present // 3) The location of the default value expression, if it should be directly carried over - Location[] additionalLocations = + Location?[] additionalLocations = [ fieldLocation, - fieldFlags.PropertyTypeExpressionLocation ?? Location.None, - fieldFlags.DefaultValueExpressionLocation ?? Location.None + fieldFlags.PropertyTypeExpressionLocation, + fieldFlags.DefaultValueExpressionLocation ]; // Track the available locations, so we can extract them back. We cannot rely on the length - // of the supplied array, because Roslyn will remove all 'None' locations from the array. + // of the supplied array, because Roslyn will remove all 'None' locations from the array. To + // match this behavior in tests too, we'll just filter out all 'null' elements below as well. AdditionalLocationKind additionalLocationKind = AdditionalLocationKind.FieldLocation | - (additionalLocations[1] != Location.None ? AdditionalLocationKind.PropertyTypeExpressionLocation : 0) | - (additionalLocations[2] != Location.None ? AdditionalLocationKind.DefaultValueExpressionLocation : 0); + (additionalLocations[1] is not null ? AdditionalLocationKind.PropertyTypeExpressionLocation : 0) | + (additionalLocations[2] is not null ? AdditionalLocationKind.DefaultValueExpressionLocation : 0); context.ReportDiagnostic(Diagnostic.Create( UseGeneratedDependencyPropertyForManualProperty, pair.Key.Locations.FirstOrDefault(), - additionalLocations, + additionalLocations.OfType(), ImmutableDictionary.Create() .Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()) .Add(DefaultValueTypeReferenceIdPropertyName, fieldFlags.DefaultValueTypeReferenceId) From 761f87c1435bf004182c88c2fc702e6efb00ec85 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 15:50:38 +0100 Subject: [PATCH 150/200] Add '[MaybeNull]' analyzer test --- .../Test_Analyzers.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 94775ee25..dbb64f667 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -741,6 +741,29 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithMaybeNull_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [MaybeNull] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Warns() { From c37fb2f984b94699eb3e58185c4db97410d61da7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 17:31:04 +0100 Subject: [PATCH 151/200] Tweak 'InvalidPropertyNullableAnnotationAnalyzer' --- ...dPropertyNonNullableDeclarationAnalyzer.cs | 71 ------- ...validPropertyNullableAnnotationAnalyzer.cs | 183 ++++++++++++++++++ .../Extensions/IMethodSymbolExtensions.cs | 50 +++++ .../Test_Analyzers.cs | 118 +++++++++-- 4 files changed, 340 insertions(+), 82 deletions(-) delete mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs deleted file mode 100644 index 4d729b2e0..000000000 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNonNullableDeclarationAnalyzer.cs +++ /dev/null @@ -1,71 +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.Immutable; -using CommunityToolkit.GeneratedDependencyProperty.Constants; -using CommunityToolkit.GeneratedDependencyProperty.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; - -namespace CommunityToolkit.GeneratedDependencyProperty; - -/// -/// A diagnostic analyzer that generates a warning when a property with [GeneratedDependencyProperty] would generate a nullability annotations violation. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class InvalidPropertyNonNullableDeclarationAnalyzer : DiagnosticAnalyzer -{ - /// - public override ImmutableArray SupportedDiagnostics { get; } = [NonNullablePropertyDeclarationIsNotEnforced]; - - /// - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(static context => - { - // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) - ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); - - // Attempt to also get the '[MaybeNull]' symbols (there might be multiples, due to polyfills) - ImmutableArray maybeNullAttributeSymbol = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); - - context.RegisterSymbolAction(context => - { - // Validate that we have a property that is of some type that can be explicitly nullable. - // We're intentionally ignoring 'Nullable' values here, since those are by defintiion nullable. - // Additionally, we only care about properties that are explicitly marked as not nullable. - // Lastly, we can skip required properties, since for those it's completely fine to be non-nullable. - if (context.Symbol is not IPropertySymbol { Type.IsValueType: false, NullableAnnotation: NullableAnnotation.NotAnnotated, IsRequired: false } propertySymbol) - { - return; - } - - // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do - if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) - { - return; - } - - // If the property has '[MaybeNull]', we never need to emit a diagnostic - if (propertySymbol.HasAttributeWithAnyType(maybeNullAttributeSymbol)) - { - return; - } - - // Emit a diagnostic if there is no default value, or if it's 'null' - if (!attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue) || defaultValue.IsNull) - { - context.ReportDiagnostic(Diagnostic.Create( - NonNullablePropertyDeclarationIsNotEnforced, - attributeData.GetLocation(), - propertySymbol)); - } - }, SymbolKind.Property); - }); - } -} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs new file mode 100644 index 000000000..903c4d602 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -0,0 +1,183 @@ +// 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 CommunityToolkit.GeneratedDependencyProperty.Constants; +using CommunityToolkit.GeneratedDependencyProperty.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.GeneratedDependencyProperty; + +/// +/// A diagnostic analyzer that generates a warning when a property with [GeneratedDependencyProperty] would generate a nullability annotations violation. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyNullableAnnotationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [NonNullablePropertyDeclarationIsNotEnforced]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode) + ImmutableArray generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute); + + // Attempt to also get the '[MaybeNull]', '[NotNull]', '[AllowNull]' and '[DisallowNull]' symbols (there might be multiples, due to polyfills) + ImmutableArray maybeNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute"); + ImmutableArray notNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.NotNullAttribute"); + ImmutableArray allowNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.AllowNullAttribute"); + ImmutableArray disallowNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.DisallowNullAttribute"); + + context.RegisterSymbolAction(context => + { + // Validate that we have a property that is of some type that could potentially become 'null' + if (context.Symbol is not IPropertySymbol { Type.IsValueType: false, NullableAnnotation: not NullableAnnotation.None } propertySymbol) + { + return; + } + + // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do + if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) + { + return; + } + + // Handle nullable and non-null properties differently + if (propertySymbol.NullableAnnotation is NullableAnnotation.Annotated) + { + // If we don't have '[NotNull]', we'll never need to emit a diagnostic. + // That is, the default nullable state will always be correct already. + if (!propertySymbol.HasAttributeWithAnyType(notNullAttributeSymbols)) + { + return; + } + + // If we have '[NotNull]', it means the property getter must always ensure that a non-null value is returned. + // This can be achieved in two different ways: + // 1) By implementing one of the 'On___Get' methods, and adding '[NotNull]' on the parameter. + // 2) By having '[DisallowNull]' on the property, and either marking the property as required, or providing a non-null default value. + if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) && + !(propertySymbol.HasAttributeWithAnyType(disallowNullAttributeSymbols) && + (propertySymbol.IsRequired || IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)))) + { + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + attributeData.GetLocation(), + propertySymbol)); + } + } + else + { + // If the property is not nullable and it has '[MaybeNull]', we never need to emit a diagnostic. + // That is, setting 'null' is valid, and the initial state doesn't matter, as the return is nullable. + if (propertySymbol.HasAttributeWithAnyType(maybeNullAttributeSymbols)) + { + return; + } + + // If setting 'null' values is allowed, then the initial state (and the default value) don't matter anymore. + // In order to be correct, we must have '[NotNull]' on any implemented getter or setter methods (same as above). + if (propertySymbol.HasAttributeWithAnyType(allowNullAttributeSymbols)) + { + if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) && + !IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) + { + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + attributeData.GetLocation(), + propertySymbol)); + } + } + else + { + // Otherwise, we need to check that either the property is required, or that the default value is not 'null'. + // This is because when the nullability of the setter is correct, then the default value takes precedence. + if (!propertySymbol.IsRequired && !IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)) + { + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + attributeData.GetLocation(), + propertySymbol)); + } + } + } + }, SymbolKind.Property); + }); + } + /// + /// Checks whether a given generated accessor method has [NotNull] on its parameter. + /// + /// The instance to inspect. + /// The syntax kind for the accessor method to look for. + /// The instances for [NotNull]. + /// Whether has a generated accessor method with its parameter marked with [NotNull]. + private static bool IsAccessorMethodMarkedAsNotNull(IPropertySymbol propertySymbol, SyntaxKind accessorKind, ImmutableArray notNullAttributeSymbols) + { + string suffix = accessorKind == SyntaxKind.GetAccessorDeclaration ? "Get" : "Set"; + + foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}{suffix}")) + { + // We really only expect to match our own generated methods, but do some basic filtering just in case + if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol, RefKind: RefKind.Ref } propertyValue] }) + { + continue; + } + + // Check if the parameter has '[NotNull]' on it + if (propertyValue.HasAttributeWithAnyType(notNullAttributeSymbols)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether a given property has a default value that is not . + /// + /// The instance to inspect. + /// The instance on . + /// The instances for [MaybeNull]. + /// The instances for [NotNull]. + /// + private static bool IsDefaultValueNotNull( + IPropertySymbol propertySymbol, + AttributeData attributeData, + ImmutableArray maybeNullAttributeSymbols, + ImmutableArray notNullAttributeSymbols) + { + // If we have a default value, check that it's not 'null' + if (attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue)) + { + return !defaultValue.IsNull; + } + + // If we have a callback, validate its return type + if (attributeData.TryGetNamedArgument("DefaultValueCallback", out TypedConstant defaultValueCallback)) + { + // Find the target method (same logic here as in the generator) + if (defaultValueCallback is { Type.SpecialType: SpecialType.System_String, Value: string { Length: > 0 } methodName } && + InvalidPropertyDefaultValueCallbackTypeAnalyzer.TryFindDefaultValueCallbackMethod(propertySymbol, methodName, out IMethodSymbol? methodSymbol) && + InvalidPropertyDefaultValueCallbackTypeAnalyzer.IsDefaultValueCallbackValid(propertySymbol, methodSymbol)) + { + // Verify that the return type can't possibly be 'null', including using attributes + return + (methodSymbol.ReturnNullableAnnotation is NullableAnnotation.NotAnnotated && !methodSymbol.HasReturnAttributeWithAnyType(maybeNullAttributeSymbols)) || + (methodSymbol.ReturnNullableAnnotation is NullableAnnotation.Annotated && methodSymbol.HasReturnAttributeWithAnyType(notNullAttributeSymbols)); + } + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs new file mode 100644 index 000000000..dbba4c2db --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IMethodSymbolExtensions.cs @@ -0,0 +1,50 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class IMethodSymbolExtensions +{ + /// + /// Checks whether or not a given symbol has a return attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// Whether or not has a return attribute with the specified type. + public static bool HasReturnAttributeWithAnyType(this IMethodSymbol symbol, ImmutableArray typeSymbols) + { + return TryGetReturnAttributeWithAnyType(symbol, typeSymbols, out _); + } + + /// + /// Tries to get a return attribute with any of the specified types. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The first return attribute of a type matching any type in , if found. + /// Whether or not has a return attribute with the specified type. + public static bool TryGetReturnAttributeWithAnyType(this IMethodSymbol symbol, ImmutableArray typeSymbols, [NotNullWhen(true)] out AttributeData? attributeData) + { + foreach (AttributeData attribute in symbol.GetReturnTypeAttributes()) + { + if (typeSymbols.Contains(attribute.AttributeClass!, SymbolEqualityComparer.Default)) + { + attributeData = attribute; + + return true; + } + } + + attributeData = null; + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index dbb64f667..43c8ac096 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -630,7 +630,7 @@ public class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -654,7 +654,7 @@ public partial class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -677,7 +677,7 @@ public partial class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -698,7 +698,7 @@ public partial class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -713,11 +713,11 @@ namespace MyApp; public partial class MyControl : Control { [GeneratedDependencyProperty] - public required partial string {|CS9248:Name|} { get; set; } + public partial string {|CS9248:Name|} { get; set; } } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -734,11 +734,59 @@ namespace MyApp; public partial class MyControl : Control { [GeneratedDependencyProperty(DefaultValue = "Bob")] - public required partial string {|CS9248:Name|} { get; set; } + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|CS9248:Name|} { get; set; } + + private static string GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_WithAttribute_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|CS9248:Name|} { get; set; } + + [return: NotNull] + private static string? GetDefaultName() => "Bob"; } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -761,7 +809,7 @@ public partial class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -782,7 +830,7 @@ public partial class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] @@ -803,7 +851,55 @@ public partial class MyControl : Control } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValueCallback_Warns() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0009:GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))|}] + public partial string {|CS9248:Name|} { get; set; } + + private static string? GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValueCallback_WithAttribute_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0009:GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))|}] + public partial string {|CS9248:Name|} { get; set; } + + [return: MaybeNull] + private static string GetDefaultName() => "Bob"; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } [TestMethod] From efde5ee195ad1dcf1af572d93765424b7ec1fd64 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 18:28:53 +0100 Subject: [PATCH 152/200] Add new nullable diagnostics to analyzer --- .../AnalyzerReleases.Shipped.md | 2 ++ ...validPropertyNullableAnnotationAnalyzer.cs | 18 ++++++++----- .../Diagnostics/DiagnosticDescriptors.cs | 26 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 4acea5e84..941bfc55e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -30,3 +30,5 @@ WCTDP0020 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera WCTDP0021 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0022 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | WCTDP0023 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | +WCTDP0024 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0025 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs index 903c4d602..6a26a97e5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -19,7 +19,12 @@ namespace CommunityToolkit.GeneratedDependencyProperty; public sealed class InvalidPropertyNullableAnnotationAnalyzer : DiagnosticAnalyzer { /// - public override ImmutableArray SupportedDiagnostics { get; } = [NonNullablePropertyDeclarationIsNotEnforced]; + public override ImmutableArray SupportedDiagnostics { get; } = + [ + NonNullablePropertyDeclarationIsNotEnforced, + NotNullResilientAccessorsForNullablePropertyDeclaration, + NotNullResilientAccessorsForNullablePropertyDeclaration + ]; /// public override void Initialize(AnalysisContext context) @@ -65,13 +70,14 @@ public override void Initialize(AnalysisContext context) // If we have '[NotNull]', it means the property getter must always ensure that a non-null value is returned. // This can be achieved in two different ways: // 1) By implementing one of the 'On___Get' methods, and adding '[NotNull]' on the parameter. - // 2) By having '[DisallowNull]' on the property, and either marking the property as required, or providing a non-null default value. + // 2) By having '[DisallowNull]' on the property or implementing one of the 'On___Set' methods with '[NotNull]' + // on the parameter, and either marking the property as required, or providing a non-null default value. if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) && - !(propertySymbol.HasAttributeWithAnyType(disallowNullAttributeSymbols) && - (propertySymbol.IsRequired || IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)))) + !((propertySymbol.HasAttributeWithAnyType(disallowNullAttributeSymbols) || IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) && + (propertySymbol.IsRequired || IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)))) { context.ReportDiagnostic(Diagnostic.Create( - NonNullablePropertyDeclarationIsNotEnforced, + NotNullResilientAccessorsForNullablePropertyDeclaration, attributeData.GetLocation(), propertySymbol)); } @@ -93,7 +99,7 @@ public override void Initialize(AnalysisContext context) !IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) { context.ReportDiagnostic(Diagnostic.Create( - NonNullablePropertyDeclarationIsNotEnforced, + NotNullResilientAccessorsForNullablePropertyDeclaration, attributeData.GetLocation(), propertySymbol)); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index b4bee3710..35e2f6fe3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -324,4 +324,30 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' must do so with a type that is compatible with the declared property type.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On___Get' or 'On___Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property). + /// + public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNotNullablePropertyDeclaration = new( + id: "WCTDP0024", + title: "Non-nullable dependency property using [AllowNull] incorrectly", + messageFormat: "The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On___Get' or 'On___Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Non-nullable properties annotated with [GeneratedDependencyProperty] using [AllowNull] should have at least one generated 'On___Get' or 'On___Set' method implemented as null-resilient (by adding [NotNull] on the 'propertyValue' parameter) to ensure assigning null values does not break the nullable annotations on the property.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null). + /// + public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNullablePropertyDeclaration = new( + id: "WCTDP0025", + title: "Nullable dependency property using [NotNull] incorrectly", + messageFormat: "The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null)", + category: typeof(DependencyPropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Nullable properties annotated with [GeneratedDependencyProperty] using [NotNull] should make their 'get' accessors null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or they must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either be marked as required properties, or ensure that the default value is not null.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } From 9aaaaea43ae3e69e38275b1cf2c9b55d5a103d92 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 18:52:06 +0100 Subject: [PATCH 153/200] Add nullability unit tests, minor bug fixes --- ...validPropertyNullableAnnotationAnalyzer.cs | 4 +- .../Test_Analyzers.cs | 444 +++++++++++++++++- 2 files changed, 433 insertions(+), 15 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs index 6a26a97e5..3432f0fa8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -22,7 +22,7 @@ public sealed class InvalidPropertyNullableAnnotationAnalyzer : DiagnosticAnalyz public override ImmutableArray SupportedDiagnostics { get; } = [ NonNullablePropertyDeclarationIsNotEnforced, - NotNullResilientAccessorsForNullablePropertyDeclaration, + NotNullResilientAccessorsForNotNullablePropertyDeclaration, NotNullResilientAccessorsForNullablePropertyDeclaration ]; @@ -99,7 +99,7 @@ public override void Initialize(AnalysisContext context) !IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) { context.ReportDiagnostic(Diagnostic.Create( - NotNullResilientAccessorsForNullablePropertyDeclaration, + NotNullResilientAccessorsForNotNullablePropertyDeclaration, attributeData.GetLocation(), propertySymbol)); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 43c8ac096..67a53de69 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -615,7 +615,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NoAttribute_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NoAttribute_DoesNotWarn() { const string source = """ using Windows.UI.Xaml.Controls; @@ -637,7 +637,7 @@ public class MyControl : Control [DataRow("int")] [DataRow("int?")] [DataRow("string?")] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NullableOrNotApplicableType_DoesNotWarn(string propertyType) + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableOrNotApplicableType_DoesNotWarn(string propertyType) { string source = $$""" using CommunityToolkit.WinUI; @@ -658,7 +658,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithMaybeNullAttribute_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithMaybeNullAttribute_DoesNotWarn() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -681,7 +681,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Required_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_Required_DoesNotWarn() { const string source = """ using CommunityToolkit.WinUI; @@ -702,7 +702,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_NullableDisabled_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_NullableDisabled_DoesNotWarn() { const string source = """ using CommunityToolkit.WinUI; @@ -721,7 +721,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValue_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNonNullDefaultValue_DoesNotWarn() { const string source = """ using CommunityToolkit.WinUI; @@ -742,7 +742,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_DoesNotWarn() { const string source = """ using CommunityToolkit.WinUI; @@ -765,7 +765,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_WithAttribute_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNonNullDefaultValueCallback_WithAttribute_DoesNotWarn() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -790,7 +790,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithMaybeNull_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithMaybeNull_DoesNotWarn() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -813,7 +813,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_Warns() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_Warns() { const string source = """ using CommunityToolkit.WinUI; @@ -834,7 +834,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValue_Warns() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNullDefaultValue_Warns() { const string source = """ using CommunityToolkit.WinUI; @@ -855,7 +855,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValueCallback_Warns() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNullDefaultValueCallback_Warns() { const string source = """ using CommunityToolkit.WinUI; @@ -878,7 +878,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNonNullableDeclarationAnalyzer_NotNullableType_WithNullDefaultValueCallback_WithAttribute_Warns() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_WithNullDefaultValueCallback_WithAttribute_Warns() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -902,6 +902,424 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_Object_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref object? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientGetter_Object_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}([NotNull] ref object? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientGetter_WithNoAttribute_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0024:GeneratedDependencyProperty|}] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}(ref string? propertyValue) + { + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0024:GeneratedDependencyProperty|}] + [AllowNull] + public partial string {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithoutNotNull_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameGet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithDisallowNull_Required_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + [DisallowNull] + public required partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientSetter_Required_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty] + [NotNull] + public required partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithDisallowNull_NonNullDefaultValue_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "")] + [NotNull] + [DisallowNull] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientSetter_NonNullDefaultValue_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [GeneratedDependencyProperty(DefaultValue = "")] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0025:GeneratedDependencyProperty|}] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_NoAttributeOnGetter_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0025:GeneratedDependencyProperty|}] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}(ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithDisallowNull_NotRequired_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0025:GeneratedDependencyProperty|}] + [NotNull] + [DisallowNull] + public partial string? {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_WithNullResilientSetter_NotRequired_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + [{|WCTDP0025:GeneratedDependencyProperty|}] + [NotNull] + public partial string? {|CS9248:Name|} { get; set; } + + partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) + { + propertyValue ??= "Bob"; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoAttribute_DoesNotWarn() { From 22363a9a9d25893936e55f1afc2e867fe931142e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 19:00:23 +0100 Subject: [PATCH 154/200] Refine logic to emit nullable diagnostics --- ...validPropertyNullableAnnotationAnalyzer.cs | 39 ++++++++++--------- .../Test_Analyzers.cs | 12 +++--- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs index 3432f0fa8..d1e2bd2e6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -91,30 +91,31 @@ public override void Initialize(AnalysisContext context) return; } + // If the getter is null-resilient, then we never need to emit a warning here + if (IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols)) + { + return; + } + // If setting 'null' values is allowed, then the initial state (and the default value) don't matter anymore. // In order to be correct, we must have '[NotNull]' on any implemented getter or setter methods (same as above). - if (propertySymbol.HasAttributeWithAnyType(allowNullAttributeSymbols)) + if (propertySymbol.HasAttributeWithAnyType(allowNullAttributeSymbols) && + !IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) { - if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) && - !IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols)) - { - context.ReportDiagnostic(Diagnostic.Create( - NotNullResilientAccessorsForNotNullablePropertyDeclaration, - attributeData.GetLocation(), - propertySymbol)); - } + context.ReportDiagnostic(Diagnostic.Create( + NotNullResilientAccessorsForNotNullablePropertyDeclaration, + attributeData.GetLocation(), + propertySymbol)); } - else + + // In either case, we need to check that either the property is required, or that the default value is not 'null'. + // This is because when the nullability of the setter is correct, then the default value takes precedence. + if (!propertySymbol.IsRequired && !IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)) { - // Otherwise, we need to check that either the property is required, or that the default value is not 'null'. - // This is because when the nullability of the setter is correct, then the default value takes precedence. - if (!propertySymbol.IsRequired && !IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols)) - { - context.ReportDiagnostic(Diagnostic.Create( - NonNullablePropertyDeclarationIsNotEnforced, - attributeData.GetLocation(), - propertySymbol)); - } + context.ReportDiagnostic(Diagnostic.Create( + NonNullablePropertyDeclarationIsNotEnforced, + attributeData.GetLocation(), + propertySymbol)); } } }, SymbolKind.Property); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 67a53de69..10d2de650 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -903,7 +903,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_Object_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_Object_Warns() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -916,7 +916,7 @@ namespace MyApp; public partial class MyControl : Control { - [GeneratedDependencyProperty] + [{|WCTDP0009:GeneratedDependencyProperty|}] [AllowNull] public partial string {|CS9248:Name|} { get; set; } @@ -959,7 +959,7 @@ public partial class MyControl : Control } [TestMethod] - public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_DoesNotWarn() + public async Task InvalidPropertyNullableAnnotationAnalyzer_NotNullableType_AllowNull_WithNullResilientSetter_Warns() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -972,7 +972,7 @@ namespace MyApp; public partial class MyControl : Control { - [GeneratedDependencyProperty] + [{|WCTDP0009:GeneratedDependencyProperty|}] [AllowNull] public partial string {|CS9248:Name|} { get; set; } @@ -1028,7 +1028,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0024:GeneratedDependencyProperty|}] + [{|WCTDP0009:{|WCTDP0024:GeneratedDependencyProperty|}|}] [AllowNull] public partial string {|CS9248:Name|} { get; set; } @@ -1055,7 +1055,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0024:GeneratedDependencyProperty|}] + [{|WCTDP0009:{|WCTDP0024:GeneratedDependencyProperty|}|}] [AllowNull] public partial string {|CS9248:Name|} { get; set; } } From f2b720664196383c5a8c719d3595b6b090791488 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 20:54:57 +0100 Subject: [PATCH 155/200] Handle generic containing types --- .../DependencyPropertyGenerator.Execute.cs | 19 +++++++++++++++---- .../DependencyPropertyGenerator.cs | 4 +++- .../Extensions/ITypeSymbolExtensions.cs | 7 +++++++ .../Extensions/WinRTExtensions.cs | 8 +++++++- .../Models/DependencyPropertyInfo.cs | 4 ++-- 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index ce49568d9..d35d8a9c4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -550,7 +550,7 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) => $"new global::{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}({defaultValue})", // Codegen for legacy UWP - { IsNet8OrGreater: false } => propertyInfo switch + { IsAdditionalTypesGenerationSupported: false } => propertyInfo switch { { DefaultValue: DependencyPropertyDefaultValue.Callback(string methodName), IsPropertyChangedCallbackImplemented: true, IsSharedPropertyChangedCallbackImplemented: false } => $""" @@ -586,7 +586,13 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) => $$""" new global::{{WellKnownTypeNames.PropertyMetadata(propertyInfo.UseWindowsUIXaml)}}( defaultValue: {{defaultValue}}, - propertyChangedCallback: static (d, e) => { (({{typeQualifiedName}})d).On{{propertyInfo.PropertyName}}PropertyChanged(e); (({{typeQualifiedName}})d).OnPropertyChanged(e); }) + propertyChangedCallback: static (d, e) => + { + {{typeQualifiedName}} __this = ({{typeQualifiedName}})d; + + __this.On{{propertyInfo.PropertyName}}PropertyChanged(e); + __this.OnPropertyChanged(e); + }) """, _ => throw new ArgumentException($"Invalid default value '{propertyInfo.DefaultValue}'."), }, @@ -902,8 +908,13 @@ static string GetExpressionWithTrailingSpace(Accessibility accessibility) /// Whether additional types are required. public static bool RequiresAdditionalTypes(EquatableArray propertyInfos) { - // If the target is not .NET 8, we never need additional types (as '[UnsafeAccessor]' is not available) - if (!propertyInfos[0].IsNet8OrGreater) + // Check whether generating additional types is supported. This is a performance optimization for some + // scenarios. We can only do this in some cases though. For instance, this is only supported on .NET 8 + // and above, as we need some additional types from the BCL (as '[UnsafeAccessor]'). Furthermore, if the + // containing type is generic, we cannot generate these additional types either, as it's not really viable + // to handle forwarding all type parameters to all generated accessors. In this case, we'll just use inline + // lambda expressions. This results in marginally worse codegen, but it's better than not supporting this. + if (!propertyInfos[0].IsAdditionalTypesGenerationSupported) { return false; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index a099ddfc4..0b5d6bb11 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -101,6 +101,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) bool isPropertyChangedCallbackImplemented = Execute.IsPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); bool isSharedPropertyChangedCallbackImplemented = Execute.IsSharedPropertyChangedCallbackImplemented(propertySymbol, useWindowsUIXaml); bool isNet8OrGreater = !context.SemanticModel.Compilation.IsWindowsRuntimeApplication(); + bool isContainedWithinGenericType = propertySymbol.ContainingType.IsGenericType; + bool isAdditionalTypesGenerationSupported = isNet8OrGreater && !isContainedWithinGenericType; token.ThrowIfCancellationRequested(); @@ -154,7 +156,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IsLocalCachingEnabled: isLocalCachingEnabled, IsPropertyChangedCallbackImplemented: isPropertyChangedCallbackImplemented, IsSharedPropertyChangedCallbackImplemented: isSharedPropertyChangedCallbackImplemented, - IsNet8OrGreater: isNet8OrGreater, + IsAdditionalTypesGenerationSupported: isAdditionalTypesGenerationSupported, UseWindowsUIXaml: useWindowsUIXaml, StaticFieldAttributes: staticFieldAttributes); }) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index c8d00e3dd..54c9197a8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -21,6 +21,13 @@ internal static class ITypeSymbolExtensions /// Whether the default value of is . public static bool IsDefaultValueNull(this ITypeSymbol symbol) { + // Special case unconstrained type parameters: their default value is not explicitly 'null' for all cases. + // If we do have a type parameter, check that it does have some reference type constraint on it. + if (symbol is ITypeParameterSymbol typeParameter) + { + return typeParameter.HasReferenceTypeConstraint; + } + return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs index aa8776155..56519947c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/WinRTExtensions.cs @@ -20,6 +20,12 @@ internal static class WinRTExtensions /// Whether is a well known WinRT projected value type.. public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, bool useWindowsUIXaml) { + // Early check: we don't care about type parameters, only about named types + if (symbol is not INamedTypeSymbol) + { + return false; + } + // This method only cares about non nullable value types if (symbol.IsDefaultValueNull()) { @@ -50,7 +56,7 @@ public static bool IsWellKnownWinRTProjectedValueType(this ITypeSymbol symbol, b } // Special case two more system types - if (symbol is INamedTypeSymbol { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) + if (symbol is { MetadataName: "TimeSpan" or "DateTimeOffset", ContainingNamespace.MetadataName: "System" }) { return true; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs index 6731d3c31..dea504c67 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyInfo.cs @@ -24,7 +24,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// Indicates whether local caching should be used for the property value. /// Indicates whether the WinRT-based property changed callback is implemented. /// Indicates whether the WinRT-based shared property changed callback is implemented. -/// Indicates whether the current target is .NET 8 or greater. +/// Indicates whether additional types can be generated. /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// The attributes to emit on the generated static field, if any. internal sealed record DependencyPropertyInfo( @@ -42,6 +42,6 @@ internal sealed record DependencyPropertyInfo( bool IsLocalCachingEnabled, bool IsPropertyChangedCallbackImplemented, bool IsSharedPropertyChangedCallbackImplemented, - bool IsNet8OrGreater, + bool IsAdditionalTypesGenerationSupported, bool UseWindowsUIXaml, EquatableArray StaticFieldAttributes); From a48bf9941db7d2894faea5a9b15f174839367a3a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 20:55:07 +0100 Subject: [PATCH 156/200] Add new generator unit tests --- .../Test_DependencyPropertyGenerator.cs | 1119 +++++++++++++++++ 1 file changed, 1119 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index af2fe64a5..8682ae456 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4530,4 +4530,1123 @@ public partial {{declaredType}} IsSelected CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching_WithCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: static (d, e) => ((MyObject)d).OnNumberPropertyChanged(e))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching_WithSharedCallback() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: static (d, e) => ((MyObject)d).OnPropertyChanged(e))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_String_WithNoCaching_WithBothCallbacks() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial string? Number { get; set; } + + partial void OnNumberPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + + partial void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata( + defaultValue: null, + propertyChangedCallback: static (d, e) => + { + MyObject __this = (MyObject)d; + + __this.OnNumberPropertyChanged(e); + __this.OnPropertyChanged(e); + })); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + string? __unboxedValue = (string?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref string? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref string? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(string? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T1_ReferenceType_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T1? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T1), + ownerType: typeof(MyObject), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T1? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T1? __unboxedValue = (T1?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T1? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T1? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T1? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T4_ValueType_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T4 Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T4), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(T4))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T4 Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T4 __unboxedValue = (T4)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T4 propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T4 propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T4 newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T4_ValueType_Nullable_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T4? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T4?), + ownerType: typeof(MyObject), + typeMetadata: null); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T4? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T4? __unboxedValue = (T4?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T4? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T4? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T4? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T2_Unconstrained_WithNoCaching() + { + const string source = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T2? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T2), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(T2))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T2? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T2? __unboxedValue = (T2?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T2? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T2? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T2? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public void SingleProperty_GenericType_T2_Unconstrained_WithInterface_WithNoCaching() + { + const string source = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T2 : IDisposable + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T2? Number { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty NumberProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "Number", + propertyType: typeof(T2), + ownerType: typeof(MyObject), + typeMetadata: new global::Windows.UI.Xaml.PropertyMetadata(default(T2))); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial T2? Number + { + get + { + object? __boxedValue = GetValue(NumberProperty); + + OnNumberGet(ref __boxedValue); + + T2? __unboxedValue = (T2?)__boxedValue; + + OnNumberGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnNumberSet(ref value); + + object? __boxedValue = value; + + OnNumberSet(ref __boxedValue); + + SetValue(NumberProperty, __boxedValue); + + OnNumberChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberGet(ref T2? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref object? propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberSet(ref T2? propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberChanged(T2? newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnNumberPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } } From 76d17fe93fa1a0c81a42dd05781b7763261407af Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 20:55:21 +0100 Subject: [PATCH 157/200] Update code fixer tests, add generic tests --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 255 +++++++++++++++++- 1 file changed, 253 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 8ddd36d3a..38899e96b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -835,13 +835,13 @@ public abstract partial class MyObject : DependencyObject /// /// Blah. /// - [GeneratedDependencyProperty] + [GeneratedDependencyProperty(DefaultValue = null)] public partial TValue? {|CS9248:Value|} { get; set; } /// /// Blah. /// - [GeneratedDependencyProperty] + [GeneratedDependencyProperty(DefaultValue = null)] public partial TElement? {|CS9248:TargetObject|} { get; set; } } """; @@ -1975,4 +1975,255 @@ public partial class MyObject : DependencyObject await test.RunAsync(); } + + [TestMethod] + public async Task MultipleProperties_WithinGenericType_HandlesAllPossibleProperties() + { + const string original = """ + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty P1Property = DependencyProperty.Register( + nameof(P1), + typeof(T1), + typeof(MyObject), + new PropertyMetadata(default(T1))); + + public static readonly DependencyProperty P2Property = DependencyProperty.Register( + nameof(P2), + typeof(T1), + typeof(MyObject), + null); + + public static readonly DependencyProperty P3Property = DependencyProperty.Register( + nameof(P3), + typeof(T2), + typeof(MyObject), + new PropertyMetadata(default(T2))); + + public static readonly DependencyProperty P4Property = DependencyProperty.Register( + nameof(P4), + typeof(T2), + typeof(MyObject), + null); + + public static readonly DependencyProperty P5Property = DependencyProperty.Register( + nameof(P5), + typeof(T3), + typeof(MyObject), + new PropertyMetadata(default(T3))); + + public static readonly DependencyProperty P6Property = DependencyProperty.Register( + nameof(P6), + typeof(T3), + typeof(MyObject), + null); + + public static readonly DependencyProperty P7Property = DependencyProperty.Register( + nameof(P7), + typeof(T4), + typeof(MyObject), + new PropertyMetadata(default(T4))); + + public static readonly DependencyProperty P8Property = DependencyProperty.Register( + nameof(P8), + typeof(T4), + typeof(MyObject), + null); + + public static readonly DependencyProperty P9Property = DependencyProperty.Register( + nameof(P9), + typeof(T4?), + typeof(MyObject), + new PropertyMetadata(default(T4?))); + + public static readonly DependencyProperty P10Property = DependencyProperty.Register( + nameof(P10), + typeof(T4?), + typeof(MyObject), + null); + + public static readonly DependencyProperty P11Property = DependencyProperty.Register( + nameof(P11), + typeof(T5), + typeof(MyObject), + new PropertyMetadata(default(T5))); + + public static readonly DependencyProperty P12Property = DependencyProperty.Register( + nameof(P12), + typeof(T5), + typeof(MyObject), + null); + + // Constrained to 'class', with default value matching 'null' (redundant) + public T1? [|P1|] + { + get => (T1?)GetValue(P1Property); + set => SetValue(P1Property, value); + } + + // Constrained to 'class', no default value + public T1? [|P2|] + { + get => (T1?)GetValue(P2Property); + set => SetValue(P2Property, value); + } + + // Unconstrained, with explicit default value + public T2? [|P3|] + { + get => (T2?)GetValue(P3Property); + set => SetValue(P3Property, value); + } + + // Unconstrained, with no metadata + public T2? [|P4|] + { + get => (T2?)GetValue(P4Property); + set => SetValue(P4Property, value); + } + + // Unconstrained, with explicit default value + public T3? [|P5|] + { + get => (T3?)GetValue(P5Property); + set => SetValue(P5Property, value); + } + + // Unconstrained, with no metadata + public T3? [|P6|] + { + get => (T3?)GetValue(P6Property); + set => SetValue(P6Property, value); + } + + // Constrained to value type, with explicit default value + public T4 [|P7|] + { + get => (T4)GetValue(P7Property); + set => SetValue(P7Property, value); + } + + // Constrained to value type, with no metadata + public T4 [|P8|] + { + get => (T4)GetValue(P8Property); + set => SetValue(P8Property, value); + } + + // Constrained to value type, nullable, with explicit default value + public T4? [|P9|] + { + get => (T4?)GetValue(P9Property); + set => SetValue(P9Property, value); + } + + // Constrained to value type, nullable, with no metadata + public T4? [|P10|] + { + get => (T4?)GetValue(P10Property); + set => SetValue(P10Property, value); + } + + // Constrained to just interface, with explicit default value + public T5? [|P11|] + { + get => (T5?)GetValue(P11Property); + set => SetValue(P11Property, value); + } + + // Constrained to just interface, with no metadata + public T5? [|P12|] + { + get => (T5?)GetValue(P12Property); + set => SetValue(P12Property, value); + } + } + """; + + const string @fixed = """ + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + // Constrained to 'class', with default value matching 'null' (redundant) + [GeneratedDependencyProperty] + public partial T1? {|CS9248:P1|} { get; set; } + + // Constrained to 'class', no default value + [GeneratedDependencyProperty] + public partial T1? {|CS9248:P2|} { get; set; } + + // Unconstrained, with explicit default value + [GeneratedDependencyProperty] + public partial T2? {|CS9248:P3|} { get; set; } + + // Unconstrained, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T2? {|CS9248:P4|} { get; set; } + + // Unconstrained, with explicit default value + [GeneratedDependencyProperty] + public partial T3? {|CS9248:P5|} { get; set; } + + // Unconstrained, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T3? {|CS9248:P6|} { get; set; } + + // Constrained to value type, with explicit default value + [GeneratedDependencyProperty] + public partial T4 {|CS9248:P7|} { get; set; } + + // Constrained to value type, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T4 {|CS9248:P8|} { get; set; } + + // Constrained to value type, nullable, with explicit default value + [GeneratedDependencyProperty] + public partial T4? {|CS9248:P9|} { get; set; } + + // Constrained to value type, nullable, with no metadata + [GeneratedDependencyProperty] + public partial T4? {|CS9248:P10|} { get; set; } + + // Constrained to just interface, with explicit default value + [GeneratedDependencyProperty] + public partial T5? {|CS9248:P11|} { get; set; } + + // Constrained to just interface, with no metadata + [GeneratedDependencyProperty(DefaultValue = null)] + public partial T5? {|CS9248:P12|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From 8782e148d0ade3c720313329ca7f6d6cd16a10b6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 21:51:18 +0100 Subject: [PATCH 158/200] Handle more explicit 'null'-s in code fixer, add tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 5 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 150 ++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index ff702630d..37572a65e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -460,9 +460,12 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla { // This is only allowed for reference or nullable types. This 'null' is redundant, but support it nonetheless. // It's not that uncommon for especially legacy codebases to have this kind of pattern in dependency properties. + // If the type is not actually nullable, make it explicit. This still allows rewriting the property to use the + // attribute, but it will cause the other analyzer to emit a diagnostic. This guarantees that even in this case, + // the original semantics are preserved (and developers can fix the code), rather than the fixer altering things. if (!propertyTypeSymbol.IsReferenceType && !isNullableValueType) { - return; + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; } } else if (TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 38899e96b..196991ead 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2226,4 +2226,154 @@ public partial class MyObject : DependencyObject await test.RunAsync(); } + + [TestMethod] + [DataRow("string?", "string", "")] + [DataRow("int", "int", "")] + [DataRow("int?", "int?", "")] + [DataRow("T1?", "T1", "")] + [DataRow("T2", "T2", "(DefaultValue = null)")] + [DataRow("T2?", "T2", "(DefaultValue = null)")] + [DataRow("T4", "T4", "(DefaultValue = null)")] + [DataRow("T4?", "T4?", "")] + [DataRow("T5", "T5", "(DefaultValue = null)")] + [DataRow("T5?", "T5", "(DefaultValue = null)")] + public async Task SimpleProperty_WithinGenericType_WithNullMetadata( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + nameof(Name), + typeof({{propertyType}}), + typeof(MyObject), + null); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } + + [TestMethod] + [DataRow("string?", "string", "")] + [DataRow("int", "int", "(DefaultValue = null)")] + [DataRow("int?", "int?", "")] + [DataRow("T1?", "T1", "")] + [DataRow("T2", "T2", "(DefaultValue = null)")] + [DataRow("T2?", "T2", "(DefaultValue = null)")] + [DataRow("T4", "T4", "(DefaultValue = null)")] + [DataRow("T4?", "T4?", "")] + [DataRow("T5", "T5", "(DefaultValue = null)")] + [DataRow("T5?", "T5", "(DefaultValue = null)")] + public async Task SimpleProperty_WithinGenericType_WithExplicitNullDefaultValue( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + nameof(Name), + typeof({{propertyType}}), + typeof(MyObject), + new PropertyMetadata(null)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From e3a8de1d062a05b977891121478f0d72e6817083 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jan 2025 22:16:38 +0100 Subject: [PATCH 159/200] Fix 'WCTDP0010' with type parameters, add tests --- ...InvalidPropertyDefaultValueTypeAnalyzer.cs | 7 +-- .../Diagnostics/DiagnosticDescriptors.cs | 4 +- .../Test_Analyzers.cs | 62 +++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs index 32967611e..664bd274a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -68,9 +68,6 @@ public override void Initialize(AnalysisContext context) return; } - bool isNullableValueType = propertySymbol.Type.IsNullableValueType(); - bool isNullableType = !propertySymbol.Type.IsValueType || isNullableValueType; - // If the value is 'null', handle all possible cases: // - Special placeholder for 'UnsetValue' // - Explicit 'null' value @@ -94,7 +91,7 @@ public override void Initialize(AnalysisContext context) } // Warn if the value is not nullable - if (!isNullableType) + if (!propertySymbol.Type.IsDefaultValueNull()) { context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDefaultValueNull, @@ -106,7 +103,7 @@ public override void Initialize(AnalysisContext context) else { // Get the target type with a special case for 'Nullable' - ITypeSymbol propertyTypeSymbol = isNullableValueType + ITypeSymbol propertyTypeSymbol = propertySymbol.Type.IsNullableValueType() ? ((INamedTypeSymbol)propertySymbol.Type).TypeArguments[0] : propertySymbol.Type; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 35e2f6fe3..50730c6f7 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -144,12 +144,12 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueNull = new( id: "WCTDP0010", title: "Invalid 'null' default value for [GeneratedDependencyProperty] use", - messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", category: typeof(DependencyPropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 10d2de650..e65ece8e3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1431,6 +1431,35 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("T1")] + [DataRow("T1?")] + [DataRow("T4?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ExplicitNull_DoesNotWarn(string propertyType) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty(DefaultValue = null)] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyDefaultValueTypeAnalyzer_NullValue_NonNullable_Warns() { @@ -1452,6 +1481,39 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("T2")] + [DataRow("T2?")] + [DataRow("T3")] + [DataRow("T3?")] + [DataRow("T4")] + [DataRow("T5")] + [DataRow("T5?")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ExplicitNull_Warns(string propertyType) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [{|WCTDP0010:GeneratedDependencyProperty(DefaultValue = null)|}] + public partial {{propertyType}} {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("string", "42")] [DataRow("string", "3.14")] From 0bf8edd8338a8cab7102b9f73eeccff5c6f79fdf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 00:12:45 +0100 Subject: [PATCH 160/200] Add more unit tests for generic types --- .../Test_Analyzers.cs | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index e65ece8e3..6027d3c1f 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1217,6 +1217,78 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NullableType_NotRequired_WithNotNull_NullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [NotNull] + public partial KeyFrameCollection? {|CS9248:KeyFrames|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}([NotNull] ref KeyFrameCollection? propertyValue) + { + propertyValue = new(); + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_NotRequired_WithAllowNull_NullResilientGetter_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty] + [AllowNull] + public partial KeyFrameCollection {|CS9248:KeyFrames|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}([NotNull] ref KeyFrameCollection? propertyValue) + { + propertyValue = new(); + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_Warns() { @@ -1320,6 +1392,103 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NullableType_NotRequired_WithNotNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [{|WCTDP0025:GeneratedDependencyProperty|}] + [NotNull] + public partial KeyFrameCollection? {|CS9248:KeyFrames|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) + { + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_NotRequired_WithAllowNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [{|WCTDP0009:{|WCTDP0024:GeneratedDependencyProperty|}|}] + [AllowNull] + public partial KeyFrameCollection {|CS9248:KeyFrames|} { get; set; } + + partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) + { + } + + partial void {|CS0759:OnKeyFramesPropertyChanged|}(DependencyPropertyChangedEventArgs e) + { + } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_rEQUIRED_WithAllowNull_Warns() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class Animation : DependencyObject + where TKeyFrame : unmanaged + { + [{|WCTDP0024:GeneratedDependencyProperty|}] + [AllowNull] + public required partial KeyFrameCollection {|CS9248:KeyFrames|} { get; set; } + } + + public sealed partial class KeyFrameCollection : DependencyObjectCollection + where TKeyFrame : unmanaged; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoAttribute_DoesNotWarn() { From 034d8a4cf9f610fcd3b4538ad8d7cb2b2da8bbcd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 00:33:07 +0100 Subject: [PATCH 161/200] Fix analyzers not running after generators --- .../Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs | 2 +- .../InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs | 8 ++++++-- .../Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs | 4 ++-- ...nvalidPropertyForwardedAttributeDeclarationAnalyzer.cs | 2 +- .../InvalidPropertyNullableAnnotationAnalyzer.cs | 4 ++-- .../PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs | 8 ++++++-- .../Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs | 6 +----- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs index 39d981f0b..e6c46b62d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs @@ -27,7 +27,7 @@ public sealed class ExplicitPropertyMetadataTypeAnalyzer : DiagnosticAnalyzer /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs index 945e7e97e..a7eeebcf4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -29,7 +29,7 @@ public sealed class InvalidPropertyDefaultValueCallbackTypeAnalyzer : Diagnostic /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => @@ -39,7 +39,11 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs index 664bd274a..ea3529d35 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -28,7 +28,7 @@ public sealed class InvalidPropertyDefaultValueTypeAnalyzer : DiagnosticAnalyzer /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => @@ -39,7 +39,7 @@ public override void Initialize(AnalysisContext context) context.RegisterOperationAction(context => { // We only care about attributes on properties - if (context.ContainingSymbol is not IPropertySymbol propertySymbol) + if (context.ContainingSymbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) { return; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs index 0824c140e..b0dd21068 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyForwardedAttributeDeclarationAnalyzer.cs @@ -31,7 +31,7 @@ public sealed class InvalidPropertyForwardedAttributeDeclarationAnalyzer : Diagn /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs index d1e2bd2e6..e8799507c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -29,7 +29,7 @@ public sealed class InvalidPropertyNullableAnnotationAnalyzer : DiagnosticAnalyz /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => @@ -46,7 +46,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { // Validate that we have a property that is of some type that could potentially become 'null' - if (context.Symbol is not IPropertySymbol { Type.IsValueType: false, NullableAnnotation: not NullableAnnotation.None } propertySymbol) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null, Type.IsValueType: false, NullableAnnotation: not NullableAnnotation.None } propertySymbol) { return; } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs index 6052a8beb..71634a8ca 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/PropertyDeclarationWithPropertyNameSuffixAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class PropertyDeclarationWithPropertyNameSuffixAnalyzer : Diagnost /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => @@ -33,7 +33,11 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } // We only want to lookup the attribute if the property name actually ends with the 'Property' suffix if (!propertySymbol.Name.EndsWith("Property")) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs index c05b15809..a9c022456 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -44,11 +44,7 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Ensure that we have some target property to analyze (also skip implementation parts) - if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) - { - return; - } + IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol; // If the property is not using '[GeneratedDependencyProperty]', there's nothing to do if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData)) From 3f743e86fc3732a5f803822839e26f763cfb315d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 00:40:07 +0100 Subject: [PATCH 162/200] Update tooling --- tooling | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling b/tooling index 3e178bc8f..725c1b575 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 3e178bc8f3d0eceb4ef9e955542ea5ce3892ca8a +Subproject commit 725c1b575a06ab5c68ea457cfaac15f7d2f638e7 From 3ba8643a67a7bb1e55db782efb79e9974aadf755 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 02:07:49 +0100 Subject: [PATCH 163/200] Fix category of all analyzer diagnostics --- .../AnalyzerReleases.Shipped.md | 50 ++++++++--------- .../Diagnostics/DiagnosticDescriptors.cs | 53 ++++++++++--------- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 941bfc55e..53ffc3d42 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -7,28 +7,28 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -WCTDP0001 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0002 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0003 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0004 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0005 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0006 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0007 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0008 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0009 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0010 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0011 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0012 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0013 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0014 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0015 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0016 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | -WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Info | -WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0020 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0021 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0022 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0023 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error | -WCTDP0024 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | -WCTDP0025 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning | +WCTDP0001 | DependencyPropertyGenerator | Error | +WCTDP0002 | DependencyPropertyGenerator | Error | +WCTDP0003 | DependencyPropertyGenerator | Error | +WCTDP0004 | DependencyPropertyGenerator | Error | +WCTDP0005 | DependencyPropertyGenerator | Error | +WCTDP0006 | DependencyPropertyGenerator | Error | +WCTDP0007 | DependencyPropertyGenerator | Error | +WCTDP0008 | DependencyPropertyGenerator | Error | +WCTDP0009 | DependencyPropertyGenerator | Warning | +WCTDP0010 | DependencyPropertyGenerator | Warning | +WCTDP0011 | DependencyPropertyGenerator | Warning | +WCTDP0012 | DependencyPropertyGenerator | Error | +WCTDP0013 | DependencyPropertyGenerator | Error | +WCTDP0014 | DependencyPropertyGenerator | Error | +WCTDP0015 | DependencyPropertyGenerator | Error | +WCTDP0016 | DependencyPropertyGenerator | Info | +WCTDP0017 | DependencyPropertyGenerator | Info | +WCTDP0018 | DependencyPropertyGenerator | Error | +WCTDP0019 | DependencyPropertyGenerator | Error | +WCTDP0020 | DependencyPropertyGenerator | Warning | +WCTDP0021 | DependencyPropertyGenerator | Warning | +WCTDP0022 | DependencyPropertyGenerator | Warning | +WCTDP0023 | DependencyPropertyGenerator | Error | +WCTDP0024 | DependencyPropertyGenerator | Warning | +WCTDP0025 | DependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 50730c6f7..b433021b1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -11,6 +11,11 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Diagnostics; /// internal static class DiagnosticDescriptors { + /// + /// The category to use for all diagnostics produced by analyzers in this project. + /// + private const string DiagnosticCategory = "DependencyPropertyGenerator"; + /// /// The diagnostic id for . /// @@ -33,7 +38,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0001", title: "Invalid property declaration for [GeneratedDependencyProperty]", messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must be instance (non static) partial properties, with a getter and a setter that is not init-only.", @@ -46,7 +51,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0002", title: "Using [GeneratedDependencyProperty] on an invalid partial property (not incomplete partial definition)", messageFormat: """The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "A property using [GeneratedDependencyProperty] is not a partial implementation part ([GeneratedDependencyProperty] must be used on partial property definitions with no implementation part).", @@ -59,7 +64,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0003", title: "Using [GeneratedDependencyProperty] on a property that returns byref", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must not return a ref value (only reference types and non byref-like types are supported).", @@ -72,7 +77,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0004", title: "Using [GeneratedDependencyProperty] on a property that returns byref-like", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must not return a byref-like value (only reference types and non byref-like types are supported).", @@ -85,7 +90,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0005", title: "Using [GeneratedDependencyProperty] on a property with invalid containing type", messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must be contained in a type that inherits from DependencyObject.", @@ -111,7 +116,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0007", title: "Using [GeneratedDependencyProperty] with 'IsLocalCachingEnabled' requires C# 'preview'", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and using the 'IsLocalCachingEnabled' option must be contained in a project using C# 'preview'. Make sure to add preview to your .csproj/.props file.", @@ -124,7 +129,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0008", title: "Conflicting property declaration for [GeneratedDependencyProperty]", messageFormat: "The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs')", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must not be declared in such a way that would cause generate members to cause conflicts. In particular, they cannot be named 'Property' and be of type either 'object' or 'DependencyPropertyChangedEventArgs'.", @@ -137,7 +142,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0009", title: "Non-nullable dependency property is not guaranteed to not be null", messageFormat: "The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Non-nullable properties annotated with [GeneratedDependencyProperty] should guarantee that their values will not be null upon exiting the constructor. This can be enforced by adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable.", @@ -150,7 +155,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0010", title: "Invalid 'null' default value for [GeneratedDependencyProperty] use", messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", @@ -163,7 +168,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0011", title: "Invalid default value type for [GeneratedDependencyProperty] use", messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", @@ -176,7 +181,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0012", title: "Using [GeneratedDependencyProperty] on a property that returns pointer type", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a pointer value ([GeneratedDependencyProperty] must be used on properties returning a non pointer value)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] must not return a pointer value (only reference types and non byref-like types are supported).", @@ -189,7 +194,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0013", title: "Using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback'", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] cannot use both 'DefaultValue' and 'DefaultValueCallback' at the same time.", @@ -202,7 +207,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0014", title: "Using [GeneratedDependencyProperty] with missing 'DefaultValueCallback' method", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of an accessible method in their same containing type.", @@ -215,7 +220,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0015", title: "Using [GeneratedDependencyProperty] with invalid 'DefaultValueCallback' method", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValueCallback' must use the name of a method with a valid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object').", @@ -228,7 +233,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0016", title: "Using [GeneratedDependencyProperty] on a property with the 'Property' suffix", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] should not have the 'Property' suffix in their name, as it is redundant (the generated dependency properties will always add the 'Property' suffix to the name of their associated properties).", @@ -241,7 +246,7 @@ internal static class DiagnosticDescriptors id: UseGeneratedDependencyPropertyForManualPropertyId, title: "Prefer using [GeneratedDependencyProperty] over manual properties", messageFormat: """The manual property '{0}' can be converted to a partial property using [GeneratedDependencyProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)""", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, description: "Manual properties should be converted to partial properties using [GeneratedDependencyProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).", @@ -254,7 +259,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0018", title: "Invalid dependency property targeted attribute type", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must correctly be resolved to valid types.", @@ -267,7 +272,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0019", title: "Invalid dependency property targeted attribute expression", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "All attributes targeting the generated dependency property for a property annotated with [GeneratedDependencyProperty] must have arguments using supported expressions.", @@ -280,7 +285,7 @@ internal static class DiagnosticDescriptors id: IncorrectDependencyPropertyFieldDeclarationId, title: "Incorrect dependency property field declaration", messageFormat: "The field '{0}' is a dependency property, but it is not declared correctly (all dependency property fields should be declared as 'public static readonly', and not be nullable)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "All dependency property fields should be declared as 'public static readonly', and not be nullable.", @@ -293,7 +298,7 @@ internal static class DiagnosticDescriptors id: DependencyPropertyFieldDeclarationId, title: "Dependency property declared as a property", messageFormat: "The property '{0}' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "All dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types.", @@ -306,7 +311,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0022", title: "Unnecessary dependency property explicit metadata type", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' should only do so when the explicit type would not match the declared property type.", @@ -319,7 +324,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0023", title: "Incompatible dependency property explicit metadata type", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, description: "Properties annotated with [GeneratedDependencyProperty] and setting 'PropertyType' must do so with a type that is compatible with the declared property type.", @@ -332,7 +337,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0024", title: "Non-nullable dependency property using [AllowNull] incorrectly", messageFormat: "The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On___Get' or 'On___Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Non-nullable properties annotated with [GeneratedDependencyProperty] using [AllowNull] should have at least one generated 'On___Get' or 'On___Set' method implemented as null-resilient (by adding [NotNull] on the 'propertyValue' parameter) to ensure assigning null values does not break the nullable annotations on the property.", @@ -345,7 +350,7 @@ internal static class DiagnosticDescriptors id: "WCTDP0025", title: "Nullable dependency property using [NotNull] incorrectly", messageFormat: "The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null)", - category: typeof(DependencyPropertyGenerator).FullName, + category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Nullable properties annotated with [GeneratedDependencyProperty] using [NotNull] should make their 'get' accessors null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or they must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either be marked as required properties, or ensure that the default value is not null.", From 717d602ff6de7ce1e1fa06b944bd0ea7f1318f42 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 02:26:16 +0100 Subject: [PATCH 164/200] Add 'GetNamedArgumentOrAttributeLocation' --- .../Extensions/AttributeDataExtensions.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs index ef945b78b..89d16f8e9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; @@ -28,6 +30,36 @@ internal static class AttributeDataExtensions return null; } + /// + /// Tries to get the location of a named argument in an input instance. + /// + /// The input instance to get the location for. + /// The name of the argument to look for. + /// The cancellation token for the operation. + /// The resulting location for , if a syntax reference is available. + public static Location? GetNamedArgumentOrAttributeLocation(this AttributeData attributeData, string name, CancellationToken token = default) + { + if (attributeData.ApplicationSyntaxReference is { } syntaxReference) + { + // If we can recover the syntax node, look for the target named argument + if (syntaxReference.GetSyntax(token) is AttributeSyntax { ArgumentList: { } argumentList }) + { + foreach (AttributeArgumentSyntax argument in argumentList.Arguments) + { + if (argument.NameEquals?.Name.Identifier.Text == name) + { + return argument.GetLocation(); + } + } + } + + // Otherwise, fallback to the location of the whole attribute + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + } + + return null; + } + /// /// Tries to get a constructor argument at a given index from the input instance. /// From b631a6c5e28c6274596522738869261eb4e8b91d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 02:27:32 +0100 Subject: [PATCH 165/200] Minor code refactoring to some extensions --- .../Extensions/AttributeDataExtensions.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs index 89d16f8e9..001ca6810 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -22,12 +22,12 @@ internal static class AttributeDataExtensions /// The resulting location for , if a syntax reference is available. public static Location? GetLocation(this AttributeData attributeData) { - if (attributeData.ApplicationSyntaxReference is { } syntaxReference) + if (attributeData.ApplicationSyntaxReference is not { } syntaxReference) { - return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + return null; } - return null; + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); } /// @@ -39,25 +39,25 @@ internal static class AttributeDataExtensions /// The resulting location for , if a syntax reference is available. public static Location? GetNamedArgumentOrAttributeLocation(this AttributeData attributeData, string name, CancellationToken token = default) { - if (attributeData.ApplicationSyntaxReference is { } syntaxReference) + if (attributeData.ApplicationSyntaxReference is not { } syntaxReference) { - // If we can recover the syntax node, look for the target named argument - if (syntaxReference.GetSyntax(token) is AttributeSyntax { ArgumentList: { } argumentList }) + return null; + } + + // If we can recover the syntax node, look for the target named argument + if (syntaxReference.GetSyntax(token) is AttributeSyntax { ArgumentList: { } argumentList }) + { + foreach (AttributeArgumentSyntax argument in argumentList.Arguments) { - foreach (AttributeArgumentSyntax argument in argumentList.Arguments) + if (argument.NameEquals?.Name.Identifier.Text == name) { - if (argument.NameEquals?.Name.Identifier.Text == name) - { - return argument.GetLocation(); - } + return argument.GetLocation(); } } - - // Otherwise, fallback to the location of the whole attribute - return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); } - return null; + // Otherwise, fallback to the location of the whole attribute + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); } /// From 5ad460b20f5d10e5b374f431b38a58ff1c4ec48a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 02:28:42 +0100 Subject: [PATCH 166/200] Improve locations of some diagnostics --- .../ExplicitPropertyMetadataTypeAnalyzer.cs | 4 +- ...ropertyDefaultValueCallbackTypeAnalyzer.cs | 4 +- ...InvalidPropertyDefaultValueTypeAnalyzer.cs | 4 +- ...validPropertyNullableAnnotationAnalyzer.cs | 7 +- .../Test_Analyzers.cs | 76 +++++++++---------- 5 files changed, 48 insertions(+), 47 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs index e6c46b62d..36a9637f9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/ExplicitPropertyMetadataTypeAnalyzer.cs @@ -67,7 +67,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( UnnecessaryDependencyPropertyExplicitMetadataType, - attributeData.GetLocation(), + attributeData.GetNamedArgumentOrAttributeLocation("PropertyType"), propertySymbol, propertySymbol.Type)); @@ -79,7 +79,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( IncompatibleDependencyPropertyExplicitMetadataType, - attributeData.GetLocation(), + attributeData.GetNamedArgumentOrAttributeLocation("PropertyType"), propertySymbol, typeSymbol, propertySymbol.Type)); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs index a7eeebcf4..9bca68162 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueCallbackTypeAnalyzer.cs @@ -77,7 +77,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound, - attributeData.GetLocation(), + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValueCallback"), propertySymbol, defaultValueCallback)); } @@ -86,7 +86,7 @@ public override void Initialize(AnalysisContext context) // Emit a diagnostic if the candidate method is not valid context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod, - attributeData.GetLocation(), + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValueCallback"), propertySymbol, defaultValueCallback)); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs index ea3529d35..74890c45e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -95,7 +95,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDefaultValueNull, - attributeData.GetLocation(), + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValue"), propertySymbol, propertySymbol.Type)); } @@ -112,7 +112,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( InvalidPropertyDefaultValueType, - attributeData.GetLocation(), + attributeData.GetNamedArgumentOrAttributeLocation("DefaultValue"), propertySymbol, propertySymbol.Type, defaultValue.Value, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs index e8799507c..e396f9dd1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; +using System.Linq; using CommunityToolkit.GeneratedDependencyProperty.Constants; using CommunityToolkit.GeneratedDependencyProperty.Extensions; using Microsoft.CodeAnalysis; @@ -78,7 +79,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( NotNullResilientAccessorsForNullablePropertyDeclaration, - attributeData.GetLocation(), + propertySymbol.Locations.FirstOrDefault(), propertySymbol)); } } @@ -104,7 +105,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( NotNullResilientAccessorsForNotNullablePropertyDeclaration, - attributeData.GetLocation(), + propertySymbol.Locations.FirstOrDefault(), propertySymbol)); } @@ -114,7 +115,7 @@ public override void Initialize(AnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( NonNullablePropertyDeclarationIsNotEnforced, - attributeData.GetLocation(), + propertySymbol.Locations.FirstOrDefault(), propertySymbol)); } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 6027d3c1f..b9ce1460b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -825,8 +825,8 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:GeneratedDependencyProperty|}] - public partial string {|CS9248:Name|} { get; set; } + [GeneratedDependencyProperty] + public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } } """; @@ -846,8 +846,8 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:GeneratedDependencyProperty(DefaultValue = null)|}] - public partial string {|CS9248:Name|} { get; set; } + [GeneratedDependencyProperty(DefaultValue = null)] + public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } } """; @@ -867,8 +867,8 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))|}] - public partial string {|CS9248:Name|} { get; set; } + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } private static string? GetDefaultName() => "Bob"; } @@ -891,8 +891,8 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))|}] - public partial string {|CS9248:Name|} { get; set; } + [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] + public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } [return: MaybeNull] private static string GetDefaultName() => "Bob"; @@ -916,9 +916,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [AllowNull] - public partial string {|CS9248:Name|} { get; set; } + public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}([NotNull] ref object? propertyValue) { @@ -972,9 +972,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [AllowNull] - public partial string {|CS9248:Name|} { get; set; } + public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) { @@ -1028,9 +1028,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:{|WCTDP0024:GeneratedDependencyProperty|}|}] + [GeneratedDependencyProperty] [AllowNull] - public partial string {|CS9248:Name|} { get; set; } + public partial string {|WCTDP0009:{|WCTDP0024:{|CS9248:Name|}|}|} { get; set; } partial void {|CS0759:OnNameGet|}(ref string? propertyValue) { @@ -1055,9 +1055,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0009:{|WCTDP0024:GeneratedDependencyProperty|}|}] + [GeneratedDependencyProperty] [AllowNull] - public partial string {|CS9248:Name|} { get; set; } + public partial string {|WCTDP0009:{|WCTDP0024:{|CS9248:Name|}|}|} { get; set; } } """; @@ -1303,9 +1303,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0025:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [NotNull] - public partial string? {|CS9248:Name|} { get; set; } + public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } } """; @@ -1326,9 +1326,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0025:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [NotNull] - public partial string? {|CS9248:Name|} { get; set; } + public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}(ref string? propertyValue) { @@ -1354,10 +1354,10 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0025:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [NotNull] [DisallowNull] - public partial string? {|CS9248:Name|} { get; set; } + public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } } """; @@ -1378,9 +1378,9 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0025:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [NotNull] - public partial string? {|CS9248:Name|} { get; set; } + public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) { @@ -1407,9 +1407,9 @@ namespace MyApp; public abstract partial class Animation : DependencyObject where TKeyFrame : unmanaged { - [{|WCTDP0025:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [NotNull] - public partial KeyFrameCollection? {|CS9248:KeyFrames|} { get; set; } + public partial KeyFrameCollection? {|WCTDP0025:{|CS9248:KeyFrames|}|} { get; set; } partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) { @@ -1442,9 +1442,9 @@ namespace MyApp; public abstract partial class Animation : DependencyObject where TKeyFrame : unmanaged { - [{|WCTDP0009:{|WCTDP0024:GeneratedDependencyProperty|}|}] + [GeneratedDependencyProperty] [AllowNull] - public partial KeyFrameCollection {|CS9248:KeyFrames|} { get; set; } + public partial KeyFrameCollection {|WCTDP0009:{|WCTDP0024:{|CS9248:KeyFrames|}|}|} { get; set; } partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) { @@ -1477,9 +1477,9 @@ namespace MyApp; public abstract partial class Animation : DependencyObject where TKeyFrame : unmanaged { - [{|WCTDP0024:GeneratedDependencyProperty|}] + [GeneratedDependencyProperty] [AllowNull] - public required partial KeyFrameCollection {|CS9248:KeyFrames|} { get; set; } + public required partial KeyFrameCollection {|WCTDP0024:{|CS9248:KeyFrames|}|} { get; set; } } public sealed partial class KeyFrameCollection : DependencyObjectCollection @@ -1642,7 +1642,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0010:GeneratedDependencyProperty(DefaultValue = null)|}] + [GeneratedDependencyProperty({|WCTDP0010:DefaultValue = null|})] public partial int {|CS9248:Name|} { get; set; } } """; @@ -1675,7 +1675,7 @@ public partial class MyObject : DependencyObject where T4 : unmanaged where T5 : IDisposable { - [{|WCTDP0010:GeneratedDependencyProperty(DefaultValue = null)|}] + [GeneratedDependencyProperty({|WCTDP0010:DefaultValue = null|})] public partial {{propertyType}} {|CS9248:Name|} { get; set; } } """; @@ -1700,7 +1700,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0011:GeneratedDependencyProperty(DefaultValue = {{defaultValueType}})|}] + [GeneratedDependencyProperty({|WCTDP0011:DefaultValue = {{defaultValueType}}|})] public partial {{propertyType}} {|CS9248:Name|} { get; set; } } """; @@ -1862,7 +1862,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0014:GeneratedDependencyProperty(DefaultValueCallback = "MissingMethod")|}] + [GeneratedDependencyProperty({|WCTDP0014:DefaultValueCallback = "MissingMethod"|})] public partial string? {|CS9248:Name|} { get; set; } } """; @@ -1883,7 +1883,7 @@ namespace MyApp; public partial class MyControl : Control, IGetDefaultValue { - [{|WCTDP0014:GeneratedDependencyProperty(DefaultValueCallback = "GetDefaultValue")|}] + [GeneratedDependencyProperty({|WCTDP0014:DefaultValueCallback = "GetDefaultValue"|})] public partial string? {|CS9248:Name|} { get; set; } static string? IGetDefaultValue.GetDefaultValue() => "Bob"; @@ -1915,7 +1915,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0015:GeneratedDependencyProperty(DefaultValueCallback = "GetDefaultName")|}] + [GeneratedDependencyProperty({|WCTDP0015:DefaultValueCallback = "GetDefaultName"|})] public partial string? {|CS9248:Name|} { get; set; } {{methodSignature}} => default!; @@ -2853,7 +2853,7 @@ public async Task ExplicitPropertyMetadataTypeAnalyzer_SameType_Warns(string typ public class MyObject : DependencyObject, IMyInterface { - [{|WCTDP0022:GeneratedDependencyProperty(PropertyType = typeof({{type}}))|}] + [GeneratedDependencyProperty({|WCTDP0022:PropertyType = typeof({{type}})|})] public {{type}} Name { get; set; } } @@ -2879,7 +2879,7 @@ public async Task ExplicitPropertyMetadataTypeAnalyzer_IncompatibleType_Warns(st public class MyObject : DependencyObject { - [{|WCTDP0023:GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))|}] + [GeneratedDependencyProperty({|WCTDP0023:PropertyType = typeof({{propertyType}})|})] public {{declaredType}} Name { get; set; } } From ae6c314925cab763b606e846deda03a639d9c8df Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 12:51:15 +0100 Subject: [PATCH 167/200] Handle covariant default value in code fixer --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 24 ++++++--- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 53 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 37572a65e..cdbb8656e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -519,12 +519,9 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - // Also make sure the type matches the property type (it's not technically guaranteed). - // If this succeeds, we can safely convert the property, the generated code will be fine. - if (!SymbolEqualityComparer.Default.Equals(defaultValueExpressionType, propertyTypeSymbol)) - { - return; - } + // Store the expression type for later, so we can validate it. We cannot validate it from here, as we + // only see the declared property type for metadata. This isn't guaranteed to match the property type. + fieldFlags.DefaultValueExpressionType = defaultValueExpressionType; } } } @@ -593,6 +590,15 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyTypeExpressionLocation = null; } + // Also make sure the default value type matches the property type (it's not technically guaranteed). + // If this succeeds, we can safely convert the property, the generated code will be fine. If this + // does not match, the generated code will be different, and we want to avoid changing semantics. + if (fieldFlags.DefaultValueExpressionType is not null && + !SymbolEqualityComparer.Default.Equals(fieldFlags.DefaultValueExpressionType, pair.Key.Type)) + { + continue; + } + // Finally, check whether the field was valid (if so, we will have a valid location) if (fieldFlags.FieldLocation is Location fieldLocation) { @@ -646,6 +652,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.DefaultValue = null; fieldFlags.DefaultValueTypeReferenceId = null; fieldFlags.DefaultValueExpressionLocation = null; + fieldFlags.DefaultValueExpressionType = null; fieldFlags.FieldLocation = null; fieldFlagsStack.Push(fieldFlags); @@ -735,6 +742,11 @@ private sealed class FieldFlags /// public Location? DefaultValueExpressionLocation; + /// + /// The type of the expression used for the default value, if validation is required. + /// + public ITypeSymbol? DefaultValueExpressionType; + /// /// The location of the target field being initialized. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 196991ead..e6c1b2f21 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2376,4 +2376,57 @@ public partial class MyObject : DependencyObject await test.RunAsync(); } + + // Hit this false negative in the Microsoft Store + [TestMethod] + public async Task SingleProperty_WithinGenericType_WithExplicitDefaultValue_TypeParameterToObject() + { + const string original = """ + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract class KeyFrame : DependencyObject + where TKeyFrame : unmanaged + { + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( + nameof(Value), + typeof(object), + typeof(KeyFrame), + new PropertyMetadata(default(TValue?))); + + public TValue? [|Value|] + { + get => (TValue?)GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public abstract partial class KeyFrame : DependencyObject + where TKeyFrame : unmanaged + { + [GeneratedDependencyProperty(PropertyType = typeof(object))] + public partial TValue? {|CS9248:Value|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From 28128de8575ad2235da809226f24fc322665d626 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 13:00:27 +0100 Subject: [PATCH 168/200] Add more unit tests for nullability --- .../Test_Analyzers.cs | 84 +++++++++- .../Test_DependencyPropertyGenerator.cs | 147 ++++++++++++++++++ 2 files changed, 230 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index b9ce1460b..d2e8c4434 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1289,6 +1289,60 @@ public sealed partial class KeyFrameCollection : DependencyOb await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NotNullableType_DoesNotWarn() + { + const string source = """ + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial T4 {|CS9248:Value|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("T1?")] + [DataRow("T2?")] + [DataRow("T3?")] + [DataRow("T4?")] + public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NullableType_DoesNotWarn(string declaredType) + { + string source = $$""" + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial {{declaredType}} {|CS9248:Value|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyNullableAnnotationAnalyzer_NullableType_Warns() { @@ -1463,7 +1517,7 @@ public sealed partial class KeyFrameCollection : DependencyOb } [TestMethod] - public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_rEQUIRED_WithAllowNull_Warns() + public async Task InvalidPropertyNullableAnnotationAnalyzer_InsideGeneric_NotNullableType_Required_WithAllowNull_Warns() { const string source = """ using System.Diagnostics.CodeAnalysis; @@ -1489,6 +1543,34 @@ public sealed partial class KeyFrameCollection : DependencyOb await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("T1")] + [DataRow("T2")] + [DataRow("T3")] + public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NotNullableType_Warns(string declaredType) + { + string source = $$""" + using System.Diagnostics.CodeAnalysis; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty] + public partial {{declaredType}} {|WCTDP0009:{|CS9248:Value|}|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyDefaultValueTypeAnalyzer_NoAttribute_DoesNotWarn() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 8682ae456..8f3b276ae 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4531,6 +4531,153 @@ public partial {{declaredType}} IsSelected CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyControl.g.cs", result), languageVersion: LanguageVersion.Preview); } + [TestMethod] + [DataRow("T1", "null", "T1", "object", "null")] + [DataRow("T1", "T1", "T1", "object", "null")] + [DataRow("T1", "object", "object", "object", "null")] + [DataRow("T1?", "null", "T1", "object?", "null")] + [DataRow("T1?", "T1", "T1", "object?", "null")] + [DataRow("T1?", "object", "object", "object?", "null")] + [DataRow("T2", "null", "T2", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2", "T2", "T2", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2?", "null", "T2", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2?", "T2", "T2", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T2?", "object", "object", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(default(T2))")] + [DataRow("T3", "null", "T3", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T3))")] + [DataRow("T3", "T3", "T3", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T3))")] + [DataRow("T3", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T3))")] + [DataRow("T4", "null", "T4", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T4))")] + [DataRow("T4", "T4", "T4", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T4))")] + [DataRow("T4", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(T4))")] + [DataRow("T4?", "null", "T4?", "object?", "null")] + [DataRow("T4?", "T4?", "T4?", "object?", "null")] + [DataRow("T4?", "object", "object", "object?", "null")] + public void SingleProperty_GenericType_WithCustomMetadataType_WithNoCaching( + string declaredType, + string propertyType, + string generatedPropertyType, + string boxedType, + string propertyMetadata) + { + string source = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyNamespace; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + { + [GeneratedDependencyProperty(PropertyType = typeof({{propertyType}}))] + public partial {{declaredType}} IsSelected { get; set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + + namespace MyNamespace + { + /// + partial class MyObject + { + /// + /// The backing instance for . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + public static readonly global::Windows.UI.Xaml.DependencyProperty IsSelectedProperty = global::Windows.UI.Xaml.DependencyProperty.Register( + name: "IsSelected", + propertyType: typeof({{generatedPropertyType}}), + ownerType: typeof(MyObject), + typeMetadata: {{propertyMetadata}}); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial {{declaredType}} IsSelected + { + get + { + object? __boxedValue = GetValue(IsSelectedProperty); + + OnIsSelectedGet(ref __boxedValue); + + {{declaredType}} __unboxedValue = ({{declaredType}})__boxedValue; + + OnIsSelectedGet(ref __unboxedValue); + + return __unboxedValue; + } + set + { + OnIsSelectedSet(ref value); + + object? __boxedValue = value; + + OnIsSelectedSet(ref __boxedValue); + + SetValue(IsSelectedProperty, __boxedValue); + + OnIsSelectedChanged(value); + } + } + + /// Executes the logic for when the accessor is invoked + /// The raw property value that has been retrieved from . + /// This method is invoked on the boxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The unboxed property value that has been retrieved from . + /// This method is invoked on the unboxed value retrieved via on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedGet(ref {{declaredType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The boxed property value that has been produced before assigning to . + /// This method is invoked on the boxed value that is about to be passed to on . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{boxedType}} propertyValue); + + /// Executes the logic for when the accessor is invoked + /// The property value that is being assigned to . + /// This method is invoked on the raw value being assigned to , before is used. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedSet(ref {{declaredType}} propertyValue); + + /// Executes the logic for when has just changed. + /// The new property value that has been set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedChanged({{declaredType}} newValue); + + /// Executes the logic for when has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnIsSelectedPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + + /// Executes the logic for when any dependency property has just changed. + /// Event data that is issued by any event that tracks changes to the effective value of this property. + /// This method is invoked by the infrastructure, after the value of any dependency property has just changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] + partial void OnPropertyChanged(global::Windows.UI.Xaml.DependencyPropertyChangedEventArgs e); + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyObject`4.g.cs", result), languageVersion: LanguageVersion.Preview); + } + [TestMethod] public void SingleProperty_GenericType_String_WithNoCaching() { From 45e7e5bb9eda6e360b25c52283f2eb81eea8675c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 13:39:19 +0100 Subject: [PATCH 169/200] Disable UWP build tools where unnecessary --- components/AppServices/src/CommunityToolkit.AppServices.csproj | 3 ++- .../Notifications/src/CommunityToolkit.Notifications.csproj | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 2c95352a3..b30639268 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -10,7 +10,8 @@ false false - + + false uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj index 6db92daf4..208bb3c6e 100644 --- a/components/Notifications/src/CommunityToolkit.Notifications.csproj +++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj @@ -17,7 +17,8 @@ false false - + + false uap10.0.17763;net8.0-windows10.0.17763.0;net9.0-windows10.0.17763.0; From 1cad5f39744a6512b71d3b245c5e7919d6ce0e95 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 17:35:32 +0100 Subject: [PATCH 170/200] Improve some diagnostic messages --- .../InvalidPropertyNullableAnnotationAnalyzer.cs | 6 ++++-- .../Diagnostics/DiagnosticDescriptors.cs | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs index e396f9dd1..c93b3988e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyNullableAnnotationAnalyzer.cs @@ -80,7 +80,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( NotNullResilientAccessorsForNullablePropertyDeclaration, propertySymbol.Locations.FirstOrDefault(), - propertySymbol)); + propertySymbol, + propertySymbol.Name)); } } else @@ -106,7 +107,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( NotNullResilientAccessorsForNotNullablePropertyDeclaration, propertySymbol.Locations.FirstOrDefault(), - propertySymbol)); + propertySymbol, + propertySymbol.Name)); } // In either case, we need to check that either the property is required, or that the default value is not 'null'. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index b433021b1..6d3711ded 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -331,28 +331,28 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On___Get' or 'On___Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property). + /// The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On{1}Get' or 'On{1}Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property). /// public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNotNullablePropertyDeclaration = new( id: "WCTDP0024", title: "Non-nullable dependency property using [AllowNull] incorrectly", - messageFormat: "The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On___Get' or 'On___Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property)", + messageFormat: "The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On{1}Get' or 'On{1}Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Non-nullable properties annotated with [GeneratedDependencyProperty] using [AllowNull] should have at least one generated 'On___Get' or 'On___Set' method implemented as null-resilient (by adding [NotNull] on the 'propertyValue' parameter) to ensure assigning null values does not break the nullable annotations on the property.", + description: "Non-nullable properties annotated with [GeneratedDependencyProperty] using [AllowNull] should have at least one generated getter or setter method (eg. 'OnNameGet', if the property is called 'Name') implemented as null-resilient (by adding [NotNull] on the 'propertyValue' parameter) to ensure assigning null values does not break the nullable annotations on the property.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null). + /// The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On{1}Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On{1}Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null). /// public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNullablePropertyDeclaration = new( id: "WCTDP0025", title: "Nullable dependency property using [NotNull] incorrectly", - messageFormat: "The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null)", + messageFormat: "The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On{1}Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On{1}Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Nullable properties annotated with [GeneratedDependencyProperty] using [NotNull] should make their 'get' accessors null-resilient, by implementing at least one generated 'On___Get' method with [NotNull] on the 'propertyValue' parameter, or they must either add [DisallowNull] or implement at least one generated 'On___Set' method with [NotNull], and also either be marked as required properties, or ensure that the default value is not null.", + description: "Nullable properties annotated with [GeneratedDependencyProperty] using [NotNull] should make their 'get' accessors null-resilient, by implementing at least one generated getter method (eg. 'OnNameGet', if the property is called 'Name') with [NotNull] on the 'propertyValue' parameter, or they must either add [DisallowNull] or implement at least one generated setter method (eg. 'OnNameSet', if the property is called 'Name') with [NotNull], and also either be marked as required properties, or ensure that the default value is not null.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } From 909923d920038191e7caee41a3eb0fabdef6c4ee Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 19:41:56 +0100 Subject: [PATCH 171/200] Add more analyzer unit tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 2 +- .../Test_Analyzers.cs | 121 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index cdbb8656e..ecc367f38 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -416,7 +416,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla { // Here we need to special case non nullable value types that are not well known WinRT projected types. // In this case, we cannot rely on XAML calling their default constructor. Rather, we need to preserve - // the explicit 'null' value that users had in their code. The analyzer will then warn on these cases + // the explicit 'null' value that users had in their code. The analyzer will then warn on these cases. if (!propertyTypeSymbol.IsDefaultValueNull() && !propertyTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)) { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index d2e8c4434..4b94a0d5d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2230,6 +2230,16 @@ public string? Name } [TestMethod] + [DataRow("global::System.Numerics.Matrix3x2?", "global::System.Numerics.Matrix3x2?", "default(global::System.Numerics.Matrix3x2)")] + [DataRow("global::System.Numerics.Matrix4x4?", "global::System.Numerics.Matrix4x4?", "default(global::System.Numerics.Matrix4x4)")] + [DataRow("global::System.Numerics.Plane?", "global::System.Numerics.Plane?", "default(global::System.Numerics.Plane)")] + [DataRow("global::System.Numerics.Quaternion?", "global::System.Numerics.Quaternion?", "default(global::System.Numerics.Quaternion)")] + [DataRow("global::System.Numerics.Vector2?", "global::System.Numerics.Vector2?", "default(global::System.Numerics.Vector2)")] + [DataRow("global::System.Numerics.Vector3?", "global::System.Numerics.Vector3?", "default(global::System.Numerics.Vector3)")] + [DataRow("global::System.Numerics.Vector4?", "global::System.Numerics.Vector4?", "default(global::System.Numerics.Vector4)")] + [DataRow("global::Windows.Foundation.Point?", "global::Windows.Foundation.Point?", "default(global::Windows.Foundation.Point)")] + [DataRow("global::Windows.Foundation.Rect?", "global::Windows.Foundation.Rect?", "default(global::Windows.Foundation.Rect)")] + [DataRow("global::Windows.Foundation.Size?", "global::Windows.Foundation.Size?", "default(global::Windows.Foundation.Size)")] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")] [DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn( @@ -2267,6 +2277,52 @@ public enum MyEnum { A, B, C } await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Using 'default(T)' is not a constant (therefore it's not allowed as attribute argument), even if constrained to an enum type. + // Some of these combinations (eg. 'object' property with 'T1?' backing type) are also just flat out invalid and would error out. + [TestMethod] + [DataRow("T1?", "T1?", "new PropertyMetadata(default(T1))")] + [DataRow("T1", "object", "new PropertyMetadata(default(T1))")] + [DataRow("T1?", "object", "new PropertyMetadata(default(T1))")] + [DataRow("object", "T1?", "new PropertyMetadata(default(T1))")] + [DataRow("T2?", "T2?", "new PropertyMetadata(default(T2))")] + [DataRow("T2", "object", "new PropertyMetadata(default(T2))")] + [DataRow("T2?", "object", "new PropertyMetadata(default(T2))")] + [DataRow("object", "T2?", "new PropertyMetadata(default(T2))")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_DoesNotWarn( + string dependencyPropertyType, + string propertyType, + string propertyMetadataExpression) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + where T1 : struct, Enum + where T2 : unmanaged, Enum + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithInvalidAttribute_DoesNotWarn() { @@ -2409,6 +2465,7 @@ public class TestAttribute : Attribute; [DataRow("int?", "int?", "null")] [DataRow("int?", "int?", "0")] [DataRow("int?", "int?", "42")] + [DataRow("int?", "int?", "default(int)")] [DataRow("int?", "int?", "default(int?)")] [DataRow("int?", "int?", "null")] [DataRow("global::System.Numerics.Matrix3x2", "global::System.Numerics.Matrix3x2", "default(global::System.Numerics.Matrix3x2)")] @@ -2424,6 +2481,14 @@ public class TestAttribute : Attribute; [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")] [DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility?)")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Collapsed")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility)")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "default(global::Windows.UI.Xaml.Visibility?)")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Visible")] + [DataRow("object", "global::Windows.UI.Xaml.Visibility?", "global::Windows.UI.Xaml.Visibility.Collapsed")] [DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")] [DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")] [DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")] @@ -2437,7 +2502,14 @@ public class TestAttribute : Attribute; [DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")] [DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")] [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.A")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.B")] + [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum)")] [DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] + [DataRow("object", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.A")] + [DataRow("object", "global::MyApp.MyEnum?", "global::MyApp.MyEnum.B")] + [DataRow("object", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum)")] + [DataRow("object", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")] [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")] [DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_Warns( @@ -2476,6 +2548,55 @@ public class MyClass { } await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Using the declared property type as first argument here for clarity when reading all combinations + [TestMethod] + [DataRow("T1?", "T1?", "new PropertyMetadata(default(T1?))")] + [DataRow("T1?", "object", "new PropertyMetadata(default(T1?))")] + [DataRow("T1?", "T1?", "new PropertyMetadata(null)")] + [DataRow("T1?", "object", "new PropertyMetadata(null)")] + [DataRow("T1?", "T1?", "null")] + [DataRow("T1?", "object", "null")] + [DataRow("T2?", "T2?", "new PropertyMetadata(default(T2?))")] + [DataRow("T2?", "object", "new PropertyMetadata(default(T2?))")] + [DataRow("T2?", "T2?", "new PropertyMetadata(null)")] + [DataRow("T2?", "object", "new PropertyMetadata(null)")] + [DataRow("T2?", "T2?", "null")] + [DataRow("T2?", "object", "null")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_Warns( + string propertyType, + string dependencyPropertyType, + string propertyMetadataExpression) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + where T1 : struct, Enum + where T2 : unmanaged, Enum + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + public {{propertyType}} {|WCTDP0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("string?", "object")] [DataRow("MyControl", "DependencyObject")] From e3e90d78aec0e9e496a0b436475040be160d4af4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 20:36:39 +0100 Subject: [PATCH 172/200] Fix some default enum scenarios, add tests --- .../Extensions/IOperationExtensions.cs | 17 ++++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 94 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs index fe9bc1a2e..1109c8d53 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/IOperationExtensions.cs @@ -36,6 +36,23 @@ public static bool IsConstantValueDefault(this IOperation operation) return true; } + // Special case enum types as well (enum types only support a subset of primitive types) + if (operation.Type is INamedTypeSymbol { TypeKind: TypeKind.Enum, EnumUnderlyingType: { } underlyingType }) + { + return (underlyingType.SpecialType, operation.ConstantValue.Value) switch + { + (SpecialType.System_Byte, default(byte)) or + (SpecialType.System_SByte, default(sbyte)) or + (SpecialType.System_Int16, default(short)) or + (SpecialType.System_UInt16, default(ushort)) or + (SpecialType.System_Int32, default(int)) or + (SpecialType.System_UInt32, default(uint)) or + (SpecialType.System_Int64, default(long)) or + (SpecialType.System_UInt64, default(ulong)) => true, + _ => false + }; + } + // Manually match for known primitive types (this should be kept in sync with 'IsWellKnownWinRTProjectedValueType') return (operation.Type.SpecialType, operation.ConstantValue.Value) switch { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index e6c1b2f21..8c408761b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2429,4 +2429,98 @@ public abstract partial class KeyFrame : DependencyObject await test.RunAsync(); } + + // Some mixed scenarios with enum types, nullable types, and different property metadata types. + // Not all of these are a 1:1 match with the code fixer (some aren't even fully valid) just yet. + [TestMethod] + [DataRow("int?", "int?", "new PropertyMetadata(default(int))", "(DefaultValue = 0)")] + //[DataRow("int?", "object", "new PropertyMetadata(default(int))", "(PropertyType = typeof(object), DefaultValue = 0)")] + [DataRow("Visibility", "Visibility", "null", "")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(null)", "(DefaultValue = null)")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(default(Visibility))", "")] + //[DataRow("Visibility", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] + //[DataRow("Visibility", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] + [DataRow("Visibility?", "Visibility?", "null", "")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(null)", "")] + //[DataRow("Visibility?", "Visibility?", "new PropertyMetadata(default(Visibility))", "(DefaultValue = Visibility.Visible)")] + //[DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Visible)", "(DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] + [DataRow("Visibility?", "object", "null", "(PropertyType = typeof(object))")] + [DataRow("Visibility?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] + //[DataRow("Visibility?", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] + //[DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Collapsed)", "(PropertyType = typeof(object), DefaultValue = Visibility.Collapsed)")] + [DataRow("MyEnum", "MyEnum", "null", "(DefaultValue = null)")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(null)", "(DefaultValue = null)")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "")] + //[DataRow("MyEnum", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] + //[DataRow("MyEnum", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("MyEnum", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object))")] + [DataRow("MyEnum?", "MyEnum?", "null", "")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(null)", "")] + //[DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(default(MyEnum))", "(DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.A)", "(DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] + [DataRow("MyEnum?", "object", "null", "(PropertyType = typeof(object))")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] + //[DataRow("MyEnum?", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] + //[DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.B)", "(PropertyType = typeof(object), DefaultValue = MyEnum.B)")] + public async Task SimpleProperty_MixedEnumAndNullable_WithPropertyType_HandlesAllScenariosCorrectly( + string declaredType, + string propertyType, + string propertyMetadataExpression, + string attributeArguments) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: {{propertyMetadataExpression}}); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A, B } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A, B } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed + }; + + await test.RunAsync(); + } } From e73eb24f8c131ae4fccb251445eb95c13d574267 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jan 2025 20:41:23 +0100 Subject: [PATCH 173/200] Fix an edge case with XAML enum types --- ...seGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs | 5 +++-- ...eGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index ecc367f38..502cf502b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -473,11 +473,12 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // We have found a valid constant. If it's an enum type, we have a couple special cases to handle. if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType) { - // As an optimization, we check whether the constant was the value + // As an optimization, we check whether the constant was the default value (not other enum member) // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the // case, the XAML infrastructure can default that values automatically, meaning we can skip the // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. - if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml))) + // We also need to exclude scenarios where the property is actually nullable, but we're assigning a value. + if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) && !isNullableValueType) { // Before actually enabling the optimization, validate that the default value is actually // the same as the default value of the enum (ie. the value of its first declared field). diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 8c408761b..150c6a827 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2443,8 +2443,8 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] [DataRow("Visibility?", "Visibility?", "null", "")] [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(null)", "")] - //[DataRow("Visibility?", "Visibility?", "new PropertyMetadata(default(Visibility))", "(DefaultValue = Visibility.Visible)")] - //[DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Visible)", "(DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(default(Visibility))", "(DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Visible)", "(DefaultValue = Visibility.Visible)")] [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] [DataRow("Visibility?", "object", "null", "(PropertyType = typeof(object))")] [DataRow("Visibility?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] @@ -2459,7 +2459,7 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("MyEnum", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object))")] [DataRow("MyEnum?", "MyEnum?", "null", "")] [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(null)", "")] - //[DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(default(MyEnum))", "(DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(default(MyEnum))", "(DefaultValue = MyEnum.A)")] [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.A)", "(DefaultValue = MyEnum.A)")] [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] [DataRow("MyEnum?", "object", "null", "(PropertyType = typeof(object))")] From ccd997cf06d7a02d77f86666a69aacb27db771e3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 02:05:16 +0100 Subject: [PATCH 174/200] Handle more codegen cases with explicit types --- .../DependencyPropertyGenerator.Execute.cs | 25 ++++++++++-- .../DependencyPropertyGenerator.cs | 6 ++- .../Models/DependencyPropertyDefaultValue.cs | 18 +++++++++ .../Test_DependencyPropertyGenerator.cs | 38 ++++++++++--------- 4 files changed, 64 insertions(+), 23 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs index d35d8a9c4..ec89759ea 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.Execute.cs @@ -243,13 +243,15 @@ public static bool TryGetAccessibilityModifiers( /// The input that triggered the annotation. /// The type name for the generated property (without nullability annotations). /// The type name for the generated property, including nullability annotations. - /// The type name for the metadata declaration of the property, if explicitly set. + /// The type name for the metadata declaration of the property, if explicitly set. + /// The type symbol for the metadata declaration of the property, if explicitly set. public static void GetPropertyTypes( IPropertySymbol propertySymbol, AttributeData attributeData, out string typeName, out string typeNameWithNullabilityAnnotations, - out string? metadataTypeName) + out string? metadataTypeName, + out ITypeSymbol? metadataTypeSymbol) { // These type names are always present and directly derived from the property type typeName = propertySymbol.Type.GetFullyQualifiedName(); @@ -264,6 +266,7 @@ public static void GetPropertyTypes( if (propertyType is { Kind: TypedConstantKind.Type, IsNull: false, Value: ITypeSymbol typeSymbol }) { metadataTypeName = typeSymbol.GetFullyQualifiedName(); + metadataTypeSymbol = typeSymbol; return; } @@ -271,6 +274,7 @@ public static void GetPropertyTypes( // By default, we'll just match the declared property type metadataTypeName = null; + metadataTypeSymbol = null; } /// @@ -278,6 +282,7 @@ public static void GetPropertyTypes( /// /// The input that triggered the annotation. /// The input instance. + /// The type symbol for the metadata declaration of the property, if explicitly set. /// The for the current compilation. /// Whether to use the UWP XAML or WinUI 3 XAML namespaces. /// The used to cancel the operation, if needed. @@ -285,8 +290,9 @@ public static void GetPropertyTypes( public static DependencyPropertyDefaultValue GetDefaultValue( AttributeData attributeData, IPropertySymbol propertySymbol, + ITypeSymbol? metadataTypeSymbol, SemanticModel semanticModel, - bool useWindowsUIXaml, + bool useWindowsUIXaml, CancellationToken token) { // First, check if we have a callback @@ -355,10 +361,21 @@ public static DependencyPropertyDefaultValue GetDefaultValue( // First we need to special case non nullable values, as for those we need 'default'. if (!propertySymbol.Type.IsDefaultValueNull()) { + // We need special logic to handle cases where the metadata type is different. For instance, + // the XAML initialization won't work if the metadata type on a property is just 'object'. + ITypeSymbol effectiveMetadataTypeSymbol = metadataTypeSymbol ?? propertySymbol.Type; + // For non nullable types, we return 'default(T)', unless we can optimize for projected types return new DependencyPropertyDefaultValue.Default( TypeName: propertySymbol.Type.GetFullyQualifiedName(), - IsProjectedType: propertySymbol.Type.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)); + IsProjectedType: effectiveMetadataTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)); + } + + // If the property type is nullable, but the metadata type is not, and it's a projected WinRT value + // type (meaning that XAML would initialize it to a value), we need to explicitly set it to 'null'. + if (metadataTypeSymbol?.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml) is true) + { + return DependencyPropertyDefaultValue.ExplicitNull.Instance; } // For all other ones, we can just use the 'null' placeholder again diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs index 0b5d6bb11..b2082b826 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/DependencyPropertyGenerator.cs @@ -94,7 +94,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.Attributes[0], out string typeName, out string typeNameWithNullabilityAnnotations, - out string? metadataTypeName); + out string? metadataTypeName, + out ITypeSymbol? metadataTypeSymbol); token.ThrowIfCancellationRequested(); @@ -115,8 +116,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) DependencyPropertyDefaultValue defaultValue = Execute.GetDefaultValue( context.Attributes[0], propertySymbol, + metadataTypeSymbol, context.SemanticModel, - useWindowsUIXaml, + useWindowsUIXaml, token); // The 'UnsetValue' can only be used when local caching is disabled diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs index 993c9d717..108541a2e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/DependencyPropertyDefaultValue.cs @@ -28,6 +28,24 @@ public override string ToString() } } + /// + /// A type representing an explicit value. + /// + /// This is used in some scenarios with mismatched metadata types. + public sealed record ExplicitNull : DependencyPropertyDefaultValue + { + /// + /// The shared instance (the type is stateless). + /// + public static ExplicitNull Instance { get; } = new(); + + /// + public override string ToString() + { + return "null"; + } + } + /// /// A type representing default value for a specific type. /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 8f3b276ae..6561a7826 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4400,21 +4400,25 @@ public partial bool IsSelected } [TestMethod] - [DataRow("bool", "null", "bool", "object")] - [DataRow("bool", "bool", "bool", "object")] - [DataRow("bool", "object", "object", "object")] - [DataRow("bool?", "null", "bool?", "object?")] - [DataRow("bool?", "bool?", "bool?", "object?")] - [DataRow("bool?", "object", "object", "object?")] - [DataRow("bool?", "bool", "bool", "object?")] - [DataRow("string?", "null", "string", "object?")] - [DataRow("string?", "string", "string", "object?")] - [DataRow("string?", "object", "object", "object?")] + [DataRow("bool", "bool", "null", "bool", "object", "null")] + [DataRow("bool", "bool", "bool", "bool", "object", "null")] + [DataRow("bool", "bool", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(bool))")] + [DataRow("bool?", "bool?", "null", "bool?", "object?", "null")] + [DataRow("bool?", "bool?", "bool?", "bool?", "object?", "null")] + [DataRow("bool?", "bool?", "object", "object", "object?", "null")] + [DataRow("bool?", "bool?", "bool", "bool", "object?", "new global::Windows.UI.Xaml.PropertyMetadata(null)")] + [DataRow("string?", "string?", "null", "string", "object?", "null")] + [DataRow("string?", "string?", "string", "string", "object?", "null")] + [DataRow("string?", "string?", "object", "object", "object?", "null")] + [DataRow("Visibility", "global::Windows.UI.Xaml.Visibility", "object", "object", "object", "new global::Windows.UI.Xaml.PropertyMetadata(default(global::Windows.UI.Xaml.Visibility))")] + [DataRow("Visibility?", "global::Windows.UI.Xaml.Visibility?", "object", "object", "object?", "null")] public void SingleProperty_WithCustomMetadataType_WithNoCaching( string declaredType, + string generatedDeclaredType, string propertyType, string generatedPropertyType, - string boxedType) + string boxedType, + string propertyMetadata) { string source = $$""" using CommunityToolkit.WinUI; @@ -4449,13 +4453,13 @@ partial class MyControl name: "IsSelected", propertyType: typeof({{generatedPropertyType}}), ownerType: typeof(MyControl), - typeMetadata: null); + typeMetadata: {{propertyMetadata}}); /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] [global::System.Diagnostics.DebuggerNonUserCode] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public partial {{declaredType}} IsSelected + public partial {{generatedDeclaredType}} IsSelected { get { @@ -4463,7 +4467,7 @@ public partial {{declaredType}} IsSelected OnIsSelectedGet(ref __boxedValue); - {{declaredType}} __unboxedValue = ({{declaredType}})__boxedValue; + {{generatedDeclaredType}} __unboxedValue = ({{generatedDeclaredType}})__boxedValue; OnIsSelectedGet(ref __unboxedValue); @@ -4493,7 +4497,7 @@ public partial {{declaredType}} IsSelected /// The unboxed property value that has been retrieved from . /// This method is invoked on the unboxed value retrieved via on . [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnIsSelectedGet(ref {{declaredType}} propertyValue); + partial void OnIsSelectedGet(ref {{generatedDeclaredType}} propertyValue); /// Executes the logic for when the accessor is invoked /// The boxed property value that has been produced before assigning to . @@ -4505,13 +4509,13 @@ public partial {{declaredType}} IsSelected /// The property value that is being assigned to . /// This method is invoked on the raw value being assigned to , before is used. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnIsSelectedSet(ref {{declaredType}} propertyValue); + partial void OnIsSelectedSet(ref {{generatedDeclaredType}} propertyValue); /// Executes the logic for when has just changed. /// The new property value that has been set. /// This method is invoked right after the value of is changed. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.WinUI.DependencyPropertyGenerator", )] - partial void OnIsSelectedChanged({{declaredType}} newValue); + partial void OnIsSelectedChanged({{generatedDeclaredType}} newValue); /// Executes the logic for when has just changed. /// Event data that is issued by any event that tracks changes to the effective value of this property. From a68ed9f63511887d21c47fc2cf09441641905f1d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 02:35:46 +0100 Subject: [PATCH 175/200] Fix more code fixer edge case scenarios --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 173 ++++++++++-------- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 31 +++- 2 files changed, 121 insertions(+), 83 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 502cf502b..fa4c203fa 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -449,82 +449,8 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - bool isNullableValueType = propertyTypeSymbol.IsNullableValueType(); - - // Check whether the value is a default constant value. If it is, then the property is valid (no explicit value). - // We need to special case nullable value types, as the default value for the underlying type is not the actual default. - if (!conversionOperation.Operand.IsConstantValueDefault() || isNullableValueType) - { - // The value is just 'null' with no type, special case this one and skip the other checks below - if (conversionOperation.Operand is { Type: null, ConstantValue: { HasValue: true, Value: null } }) - { - // This is only allowed for reference or nullable types. This 'null' is redundant, but support it nonetheless. - // It's not that uncommon for especially legacy codebases to have this kind of pattern in dependency properties. - // If the type is not actually nullable, make it explicit. This still allows rewriting the property to use the - // attribute, but it will cause the other analyzer to emit a diagnostic. This guarantees that even in this case, - // the original semantics are preserved (and developers can fix the code), rather than the fixer altering things. - if (!propertyTypeSymbol.IsReferenceType && !isNullableValueType) - { - fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; - } - } - else if (TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue)) - { - // We have found a valid constant. If it's an enum type, we have a couple special cases to handle. - if (conversionOperation.Operand.Type is { TypeKind: TypeKind.Enum } operandType) - { - // As an optimization, we check whether the constant was the default value (not other enum member) - // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the - // case, the XAML infrastructure can default that values automatically, meaning we can skip the - // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. - // We also need to exclude scenarios where the property is actually nullable, but we're assigning a value. - if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) && !isNullableValueType) - { - // Before actually enabling the optimization, validate that the default value is actually - // the same as the default value of the enum (ie. the value of its first declared field). - if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) && - conversionOperation.Operand.ConstantValue.Value == defaultValue) - { - fieldFlags.DefaultValue = null; - } - } - else if (operandType.ContainingType is not null) - { - // If the enum is nested, we need to also track the type symbol specifically, as the fully qualified - // expression we'd be using otherwise would not be the same as the metadata name, and resolving the - // enum type symbol from that in the code fixer would fail. This is an edge case, but it can happen. - fieldFlags.DefaultValueTypeReferenceId = DocumentationCommentId.CreateReferenceId(operandType); - } - } - - // Special case for named constants: in this case we want to carry the whole expression over, rather - // than serializing the constant value itself. This preserves the actual fields being referenced. - // We skip enum fields, as those are not named constants. Those will be handled by the logic above. - if (conversionOperation.Operand is IFieldReferenceOperation { Field: { IsConst: true, ContainingType.TypeKind: not TypeKind.Enum } }) - { - fieldFlags.DefaultValueExpressionLocation = conversionOperation.Operand.Syntax.GetLocation(); - } - } - else if (conversionOperation.Operand is IFieldReferenceOperation { Field: { ContainingType.SpecialType: SpecialType.System_String, Name: "Empty" } }) - { - // Special handling of the 'string.Empty' field. This is not a constant value, but we can still treat it as a constant, by just - // pretending this were the empty string literal instead. This way we can still support the property and convert to an attribute. - fieldFlags.DefaultValue = TypedConstantInfo.Primitive.String.Empty; - } - else - { - // If we don't have a constant, check if it's some constant value we can forward. In this case, we - // did not retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. - if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) - { - return; - } - - // Store the expression type for later, so we can validate it. We cannot validate it from here, as we - // only see the declared property type for metadata. This isn't guaranteed to match the property type. - fieldFlags.DefaultValueExpressionType = defaultValueExpressionType; - } - } + // Store the operation for later, as we need to wait to also get the metadata type to do the full validation + fieldFlags.DefaultValueOperation = conversionOperation.Operand; } // Find the parent field for the operation (we're guaranteed to only fine one) @@ -576,6 +502,95 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla continue; } + // Execute the deferred default value validation, if necessary + if (fieldFlags.DefaultValueOperation is not null) + { + bool isNullableValueType = pair.Key.Type.IsNullableValueType(); + + // Check whether the value is a default constant value. If it is, then the property is valid (no explicit value). + // We need to special case nullable value types, as the default value for the underlying type is not the actual default. + if (!fieldFlags.DefaultValueOperation.IsConstantValueDefault() || isNullableValueType) + { + // The value is just 'null' with no type, special case this one and skip the other checks below + if (fieldFlags.DefaultValueOperation is { Type: null, ConstantValue: { HasValue: true, Value: null } }) + { + // This is only allowed for reference or nullable types. This 'null' is redundant, but support it nonetheless. + // It's not that uncommon for especially legacy codebases to have this kind of pattern in dependency properties. + // If the type is not actually nullable, make it explicit. This still allows rewriting the property to use the + // attribute, but it will cause the other analyzer to emit a diagnostic. This guarantees that even in this case, + // the original semantics are preserved (and developers can fix the code), rather than the fixer altering things. + if (!pair.Key.Type.IsReferenceType && !isNullableValueType) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + else if (!SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType) && + !pair.Key.Type.IsNullableValueType() && + fieldFlags.PropertyType!.IsDefaultValueNull()) + { + // Special case: the property type is not nullable, but the property metadata type is explicitly declared as + // a nullable type, and the default value is set to 'null'. In this case, we need to preserve this value. + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + else if (TypedConstantInfo.TryCreate(fieldFlags.DefaultValueOperation, out fieldFlags.DefaultValue)) + { + // We have found a valid constant. If it's an enum type, we have a couple special cases to handle. + if (fieldFlags.DefaultValueOperation.Type is { TypeKind: TypeKind.Enum } operandType) + { + // As an optimization, we check whether the constant was the default value (not other enum member) + // of some projected built-in WinRT enum type (ie. not any user-defined enum type). If that is the + // case, the XAML infrastructure can default that values automatically, meaning we can skip the + // overhead of instantiating a 'PropertyMetadata' instance in code, and marshalling default value. + // We also need to exclude scenarios where the property is actually nullable, but we're assigning a value. + if (operandType.IsContainedInNamespace(WellKnownTypeNames.XamlNamespace(useWindowsUIXaml)) && !isNullableValueType) + { + // Before actually enabling the optimization, validate that the default value is actually + // the same as the default value of the enum (ie. the value of its first declared field). + if (operandType.TryGetDefaultValueForEnumType(out object? defaultValue) && + fieldFlags.DefaultValueOperation.ConstantValue.Value == defaultValue) + { + fieldFlags.DefaultValue = null; + } + } + else if (operandType.ContainingType is not null) + { + // If the enum is nested, we need to also track the type symbol specifically, as the fully qualified + // expression we'd be using otherwise would not be the same as the metadata name, and resolving the + // enum type symbol from that in the code fixer would fail. This is an edge case, but it can happen. + fieldFlags.DefaultValueTypeReferenceId = DocumentationCommentId.CreateReferenceId(operandType); + } + } + + // Special case for named constants: in this case we want to carry the whole expression over, rather + // than serializing the constant value itself. This preserves the actual fields being referenced. + // We skip enum fields, as those are not named constants. Those will be handled by the logic above. + if (fieldFlags.DefaultValueOperation is IFieldReferenceOperation { Field: { IsConst: true, ContainingType.TypeKind: not TypeKind.Enum } }) + { + fieldFlags.DefaultValueExpressionLocation = fieldFlags.DefaultValueOperation.Syntax.GetLocation(); + } + } + else if (fieldFlags.DefaultValueOperation is IFieldReferenceOperation { Field: { ContainingType.SpecialType: SpecialType.System_String, Name: "Empty" } }) + { + // Special handling of the 'string.Empty' field. This is not a constant value, but we can still treat it as a constant, by just + // pretending this were the empty string literal instead. This way we can still support the property and convert to an attribute. + fieldFlags.DefaultValue = TypedConstantInfo.Primitive.String.Empty; + } + else + { + // If we don't have a constant, check if it's some constant value we can forward. In this case, we + // did not retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. + if (fieldFlags.DefaultValueOperation is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) + { + continue; + } + + // Store the expression type for later, so we can validate it. We cannot validate it from here, as we + // only see the declared property type for metadata. This isn't guaranteed to match the property type. + fieldFlags.DefaultValueExpressionType = defaultValueExpressionType; + } + } + } + // Make sure that the 'propertyType' value matches the actual type of the property. // We are intentionally not handling combinations of nullable value types here. if (!SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType) && @@ -650,6 +665,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyName = null; fieldFlags.PropertyType = null; fieldFlags.PropertyTypeExpressionLocation = null; + fieldFlags.DefaultValueOperation = null; fieldFlags.DefaultValue = null; fieldFlags.DefaultValueTypeReferenceId = null; fieldFlags.DefaultValueExpressionLocation = null; @@ -728,6 +744,11 @@ private sealed class FieldFlags /// public Location? PropertyTypeExpressionLocation; + /// + /// The operation for the default value conversion, for deferred validation. + /// + public IOperation? DefaultValueOperation; + /// /// The default value to use (not present if it does not need to be set explicitly). /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 150c6a827..3eb889f97 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2433,13 +2433,26 @@ public abstract partial class KeyFrame : DependencyObject // Some mixed scenarios with enum types, nullable types, and different property metadata types. // Not all of these are a 1:1 match with the code fixer (some aren't even fully valid) just yet. [TestMethod] + [DataRow("int?", "int?", "null", "")] + [DataRow("int?", "int?", "new PropertyMetadata(null)", "")] + [DataRow("int?", "int?", "new PropertyMetadata(default(int?))", "")] + [DataRow("int?", "int?", "new PropertyMetadata(0)", "(DefaultValue = 0)")] [DataRow("int?", "int?", "new PropertyMetadata(default(int))", "(DefaultValue = 0)")] - //[DataRow("int?", "object", "new PropertyMetadata(default(int))", "(PropertyType = typeof(object), DefaultValue = 0)")] + [DataRow("int?", "object", "null", "(PropertyType = typeof(object))")] + [DataRow("int?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] + [DataRow("int?", "object", "new PropertyMetadata(default(int?))", "(PropertyType = typeof(object))")] + [DataRow("int?", "object", "new PropertyMetadata(0)", "(PropertyType = typeof(object), DefaultValue = 0)")] + [DataRow("int?", "object", "new PropertyMetadata(default(int))", "(PropertyType = typeof(object), DefaultValue = 0)")] [DataRow("Visibility", "Visibility", "null", "")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(null)", "(DefaultValue = null)")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(default(Visibility))", "")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Visible)", "")] + [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] //[DataRow("Visibility", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] - //[DataRow("Visibility", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("Visibility", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] + [DataRow("Visibility", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object))")] + [DataRow("Visibility", "object", "new PropertyMetadata(Visibility.Collapsed)", "(PropertyType = typeof(object), DefaultValue = Visibility.Collapsed)")] [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] [DataRow("Visibility?", "Visibility?", "null", "")] [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(null)", "")] @@ -2448,15 +2461,19 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("Visibility?", "Visibility?", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] [DataRow("Visibility?", "object", "null", "(PropertyType = typeof(object))")] [DataRow("Visibility?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] - //[DataRow("Visibility?", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] - //[DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] + [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Collapsed)", "(PropertyType = typeof(object), DefaultValue = Visibility.Collapsed)")] [DataRow("MyEnum", "MyEnum", "null", "(DefaultValue = null)")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(null)", "(DefaultValue = null)")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.A)", "")] + [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] //[DataRow("MyEnum", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] - //[DataRow("MyEnum", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("MyEnum", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] [DataRow("MyEnum", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object))")] + [DataRow("MyEnum", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object))")] + [DataRow("MyEnum", "object", "new PropertyMetadata(MyEnum.B)", "(PropertyType = typeof(object), DefaultValue = MyEnum.B)")] [DataRow("MyEnum?", "MyEnum?", "null", "")] [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(null)", "")] [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(default(MyEnum))", "(DefaultValue = MyEnum.A)")] @@ -2464,8 +2481,8 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("MyEnum?", "MyEnum?", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] [DataRow("MyEnum?", "object", "null", "(PropertyType = typeof(object))")] [DataRow("MyEnum?", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object))")] - //[DataRow("MyEnum?", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] - //[DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] + [DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object), DefaultValue = MyEnum.A)")] [DataRow("MyEnum?", "object", "new PropertyMetadata(MyEnum.B)", "(PropertyType = typeof(object), DefaultValue = MyEnum.B)")] public async Task SimpleProperty_MixedEnumAndNullable_WithPropertyType_HandlesAllScenariosCorrectly( string declaredType, From 23acb10fcbd0c663a4d87348caa942a749a251b0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 02:46:19 +0100 Subject: [PATCH 176/200] Fix more edge cases in code fixer --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 31 ++++++++++++++++--- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 4 +-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index fa4c203fa..00e1db009 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -502,8 +502,18 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla continue; } - // Execute the deferred default value validation, if necessary - if (fieldFlags.DefaultValueOperation is not null) + // Execute the deferred default value validation, if necessary. If we have an operation + // here, it means we had some constructed property metadata. Otherwise, it was 'null'. + if (fieldFlags.DefaultValueOperation is null) + { + // Special case: the whole property metadata is 'null', and the metadata type is nullable, but + // the property type isn't. In this case, we need to ensure the explicit 'null' is preserved. + if (fieldFlags.IsExplicitConversionFromNonNullableToNullableMetdataType(pair.Key.Type)) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } + } + else { bool isNullableValueType = pair.Key.Type.IsNullableValueType(); @@ -523,9 +533,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla { fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; } - else if (!SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType) && - !pair.Key.Type.IsNullableValueType() && - fieldFlags.PropertyType!.IsDefaultValueNull()) + else if (fieldFlags.IsExplicitConversionFromNonNullableToNullableMetdataType(pair.Key.Type)) { // Special case: the property type is not nullable, but the property metadata type is explicitly declared as // a nullable type, and the default value is set to 'null'. In this case, we need to preserve this value. @@ -773,6 +781,19 @@ private sealed class FieldFlags /// The location of the target field being initialized. /// public Location? FieldLocation; + + /// + /// Checks whether the field has an explicit conversion to a nullable metadata type (where the property isn't). + /// + /// The property type. + /// Whether the field has an explicit conversion to a nullable metadata type + public bool IsExplicitConversionFromNonNullableToNullableMetdataType(ITypeSymbol propertyType) + { + return + !SymbolEqualityComparer.Default.Equals(propertyType, PropertyType) && + !propertyType.IsDefaultValueNull() && + PropertyType!.IsDefaultValueNull(); + } } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 3eb889f97..29e18c0a5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2448,7 +2448,7 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("Visibility", "Visibility", "new PropertyMetadata(default(Visibility))", "")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Visible)", "")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] - //[DataRow("Visibility", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("Visibility", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] [DataRow("Visibility", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] [DataRow("Visibility", "object", "new PropertyMetadata(default(Visibility))", "(PropertyType = typeof(object))")] [DataRow("Visibility", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object))")] @@ -2469,7 +2469,7 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.A)", "")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] - //[DataRow("MyEnum", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] + [DataRow("MyEnum", "object", "null", "(PropertyType = typeof(object), DefaultValue = null)")] [DataRow("MyEnum", "object", "new PropertyMetadata(null)", "(PropertyType = typeof(object), DefaultValue = null)")] [DataRow("MyEnum", "object", "new PropertyMetadata(default(MyEnum))", "(PropertyType = typeof(object))")] [DataRow("MyEnum", "object", "new PropertyMetadata(MyEnum.A)", "(PropertyType = typeof(object))")] From 88662ef91c46033854f4711653bf634c99b0f3e9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 14:26:16 +0100 Subject: [PATCH 177/200] Add supplementary warnings on property fields --- .../AnalyzerReleases.Shipped.md | 5 + ...endencyPropertyOnManualPropertyAnalyzer.cs | 142 +++++++++++++++--- .../Diagnostics/DiagnosticDescriptors.cs | 65 ++++++++ 3 files changed, 188 insertions(+), 24 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 53ffc3d42..533b79927 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -32,3 +32,8 @@ WCTDP0022 | DependencyPropertyGenerator | Warning | WCTDP0023 | DependencyPropertyGenerator | Error | WCTDP0024 | DependencyPropertyGenerator | Warning | WCTDP0025 | DependencyPropertyGenerator | Warning | +WCTDP0026 | DependencyPropertyGenerator | Warning | +WCTDP0027 | DependencyPropertyGenerator | Warning | +WCTDP0028 | DependencyPropertyGenerator | Warning | +WCTDP0029 | DependencyPropertyGenerator | Warning | +WCTDP0030 | DependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 00e1db009..0fa9267b0 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -69,7 +69,15 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia public const string AdditionalLocationKindPropertyName = "AdditionalLocationKind"; /// - public override ImmutableArray SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty]; + public override ImmutableArray SupportedDiagnostics { get; } = + [ + UseGeneratedDependencyPropertyForManualProperty, + NoPropertySuffixOnDependencyPropertyField, + InvalidPropertyNameOnDependencyPropertyField, + MismatchedPropertyNameOnDependencyPropertyField, + InvalidOwningTypeOnDependencyPropertyField, + InvalidPropertyTypeOnDependencyPropertyField + ]; /// public override void Initialize(AnalysisContext context) @@ -363,7 +371,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // Same as above, but targeting field initializers (we can't just inspect field symbols) context.RegisterOperationAction(context => { - // Only look for field symbols, which we should always get here, and an invocation in the initializer block (the 'DependencyProperty.Register' call) + // Only look for field symbols, which we should always get here, and an invocation in the initializer block (the 'DependencyProperty.Register' call). + // As far as the additional diagnostics are concerned (that is, the ones for failure cases), we don't need this to be 100% accurate. For instance, we + // don't care about initializers assigning to multiple fields, as practically speaking nobody will ever do that. Similarly, we also don't worry about + // spotting fields without an initializers, as nobody would realistically be declaring dependency property fields without an initializer. if (context.Operation is not IFieldInitializerOperation { InitializedFields: [{ } fieldSymbol], Value: IInvocationOperation invocationOperation }) { return; @@ -375,42 +386,78 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } + fieldFlags.FieldSymbol = fieldSymbol; + // Validate that we are calling 'DependencyProperty.Register' if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, dependencyPropertyRegisterSymbol)) { return; } + // Additional diagnostic #1: the dependency property field name should have the "Property" suffix + if (!fieldSymbol.Name.EndsWith("Property")) + { + context.ReportDiagnostic(Diagnostic.Create( + NoPropertySuffixOnDependencyPropertyField, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol)); + + fieldFlags.HasAnyDiagnostics = true; + } + // Next, make sure we have the arguments we expect if (invocationOperation.Arguments is not [{ } nameArgument, { } propertyTypeArgument, { } ownerTypeArgument, { } propertyMetadataArgument]) { return; } + fieldFlags.PropertyNameExpressionLocation = nameArgument.Syntax.GetLocation(); + fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); + // We cannot validate the property name from here yet, but let's check it's a constant, and save it for later - if (nameArgument.Value.ConstantValue is not { HasValue: true, Value: string propertyName }) + if (nameArgument.Value.ConstantValue is { HasValue: true, Value: string propertyName }) { - return; - } + fieldFlags.PropertyName = propertyName; - // Extract the property type, we can validate it later - if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) - { - return; + // Additional diagnostic #2: the property name should be the same as the field name, without the "Property" suffix (as per convention) + if (fieldSymbol.Name.EndsWith("Property") && propertyName != fieldSymbol.Name[..^"Property".Length]) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyNameOnDependencyPropertyField, + nameArgument.Syntax.GetLocation(), + fieldSymbol, + propertyName)); + + fieldFlags.HasAnyDiagnostics = true; + } } - // Extract the owning type, we can validate it right now - if (ownerTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } owningTypeSymbol }) + // Extract the owning type, we can validate it right now. Move this before checking the property type, even though this + // comes after it, so that we can still produce this diagnostic correctly if the property type is missing or invalid. + if (ownerTypeArgument.Value is ITypeOfOperation { TypeOperand: { } owningTypeSymbol }) { - return; + // Additional diagnostic #3: the owning type always has to be exactly the same as the containing type + if (!SymbolEqualityComparer.Default.Equals(owningTypeSymbol, typeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidOwningTypeOnDependencyPropertyField, + ownerTypeArgument.Syntax.GetLocation(), + fieldSymbol, + owningTypeSymbol, + typeSymbol)); + + fieldFlags.HasAnyDiagnostics = true; + } } - // The owning type always has to be exactly the same as the containing type - if (!SymbolEqualityComparer.Default.Equals(owningTypeSymbol, typeSymbol)) + // Extract the property type, we can validate it later + if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) { return; } + fieldFlags.PropertyType = propertyTypeSymbol; + // First, check if the metadata is 'null' (simplest case) if (propertyMetadataArgument.Value.ConstantValue is { HasValue: true, Value: null }) { @@ -465,9 +512,6 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - fieldFlags.PropertyName = propertyName; - fieldFlags.PropertyType = propertyTypeSymbol; - fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); fieldFlags.FieldLocation = fieldDeclaration.GetLocation(); }, OperationKind.FieldInitializer); @@ -495,11 +539,22 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla continue; } - // We only support rewriting when the property name matches the field being initialized. - // Note that the property name here is the literal being passed for the 'name' parameter. - if (pair.Key.Name != fieldFlags.PropertyName) + // Additional diagnostic #4: we only support rewriting when the property name matches the field being + // initialized. Note that the property name here is the literal being passed for the 'name' parameter. + // These two names should always match, and if they don't that's most definitely a bug in the code. + if (fieldFlags.FieldSymbol is not null && pair.Key.Name != fieldFlags.PropertyName) { - continue; + // Ensure we do have a property name before emitting a diagnostic (if it's 'null', that's a user error) + if (fieldFlags.PropertyName is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + MismatchedPropertyNameOnDependencyPropertyField, + fieldFlags.PropertyNameExpressionLocation!, + fieldFlags.PropertyName, + pair.Key.Name)); + } + + fieldFlags.HasAnyDiagnostics = true; } // Execute the deferred default value validation, if necessary. If we have an operation @@ -599,12 +654,25 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } } - // Make sure that the 'propertyType' value matches the actual type of the property. - // We are intentionally not handling combinations of nullable value types here. + // Additional diagnostic #5: make sure that the 'propertyType' value matches the actual type + // of the property. We are intentionally not handling combinations of nullable value types here. + // The logic in 'IsValidPropertyMetadataType' will already cover all supported combinations. if (!SymbolEqualityComparer.Default.Equals(pair.Key.Type, fieldFlags.PropertyType) && (fieldFlags.PropertyType is null || !ExplicitPropertyMetadataTypeAnalyzer.IsValidPropertyMetadataType(pair.Key.Type, fieldFlags.PropertyType, context.Compilation))) { - continue; + // Let's be extra cautious, in case the field initializer is entirely missing, we don't want to produce a garbage diagnostic + if (fieldFlags.FieldSymbol is not null && fieldFlags.PropertyType is not null) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyTypeOnDependencyPropertyField, + fieldFlags.PropertyTypeExpressionLocation ?? fieldFlags.FieldSymbol.Locations.FirstOrDefault(), + fieldFlags.FieldSymbol, + fieldFlags.PropertyType, + pair.Key.Type, + pair.Key)); + } + + fieldFlags.HasAnyDiagnostics = true; } // If the property type is an exact match for the property type in metadata, we can remove the location of that argument. @@ -623,6 +691,14 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla continue; } + // If any diagnostics were produced, we consider the property as invalid. We use this sytem to be + // able to still execute the rest of the logic, so we can emit all applicable diagnostics at the + // same time. Otherwise we'd only ever produce one at a time, which makes for a frustrating experience. + if (fieldFlags.HasAnyDiagnostics) + { + continue; + } + // Finally, check whether the field was valid (if so, we will have a valid location) if (fieldFlags.FieldLocation is Location fieldLocation) { @@ -670,7 +746,9 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // Same for the field flags foreach (FieldFlags fieldFlags in fieldMap.Values) { + fieldFlags.FieldSymbol = null; fieldFlags.PropertyName = null; + fieldFlags.PropertyNameExpressionLocation = null; fieldFlags.PropertyType = null; fieldFlags.PropertyTypeExpressionLocation = null; fieldFlags.DefaultValueOperation = null; @@ -679,6 +757,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.DefaultValueExpressionLocation = null; fieldFlags.DefaultValueExpressionType = null; fieldFlags.FieldLocation = null; + fieldFlags.HasAnyDiagnostics = false; fieldFlagsStack.Push(fieldFlags); } @@ -737,11 +816,21 @@ private sealed class PropertyFlags /// private sealed class FieldFlags { + /// + /// The symbol for the field being initialized. + /// + public IFieldSymbol? FieldSymbol; + /// /// The name of the property. /// public string? PropertyName; + /// + /// The location for the expression defining the property name. + /// + public Location? PropertyNameExpressionLocation; + /// /// The type of the property (as in, of values that can be assigned to it). /// @@ -782,6 +871,11 @@ private sealed class FieldFlags /// public Location? FieldLocation; + /// + /// Indicates whether any diagnostics were produced for the field. + /// + public bool HasAnyDiagnostics; + /// /// Checks whether the field has an explicit conversion to a nullable metadata type (where the property isn't). /// diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 6d3711ded..2921d5fd2 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -355,4 +355,69 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Nullable properties annotated with [GeneratedDependencyProperty] using [NotNull] should make their 'get' accessors null-resilient, by implementing at least one generated getter method (eg. 'OnNameGet', if the property is called 'Name') with [NotNull] on the 'propertyValue' parameter, or they must either add [DisallowNull] or implement at least one generated setter method (eg. 'OnNameSet', if the property is called 'Name') with [NotNull], and also either be marked as required properties, or ensure that the default value is not null.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its name is not following the recommended naming convention (all dependency property fields should use the 'Property' suffix in their names). + /// + public static readonly DiagnosticDescriptor NoPropertySuffixOnDependencyPropertyField = new( + id: "WCTDP0026", + title: "No 'Property' suffix on dependency property field name", + messageFormat: "The field '{0}' is registering a dependency property, but its name is not following the recommended naming convention (all dependency property fields should use the 'Property' suffix in their names)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should be named following the recommended naming convention (they should use the 'Property' suffix in their names).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata is not following the recommended naming convention (all property names should match the name of their declared fields, minus the 'Property' suffix). + /// + public static readonly DiagnosticDescriptor InvalidPropertyNameOnDependencyPropertyField = new( + id: "WCTDP0027", + title: "Invalid property name in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata is not following the recommended naming convention (all property names should match the name of their declared fields, minus the 'Property' suffix)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare property names in metadata following the recommended naming convention (they should match the name of the declared fields, minus the 'Property' suffix).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata does not match the property name '{2}' wrapping the dependency property (all property names should match the name of the wrapping properties of each dependency property field). + /// + public static readonly DiagnosticDescriptor MismatchedPropertyNameOnDependencyPropertyField = new( + id: "WCTDP0028", + title: "Mismatched property name in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata does not match the property name '{2}' wrapping the dependency property (all property names should match the name of the wrapping properties of each dependency property field)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare property names in metadata following the recommended naming convention (they should exactly match the name of the wrapping properties).", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its owning type '{1}' declared in metadata does not match the containing type '{2}' for the field (the owning type of a dependency property should always match the containing type of the field declaration). + /// + public static readonly DiagnosticDescriptor InvalidOwningTypeOnDependencyPropertyField = new( + id: "WCTDP0029", + title: "Invalid owning type in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its owning type '{1}' declared in metadata does not match the containing type '{2}' for the field (the owning type of a dependency property should always match the containing type of the field declaration)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare owning types in metadata matching their containing types.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its property type '{1}' does not match the type '{2}' of the wrapping property '{3}', and there is no valid type conversion between the two (all property types should either match the type of the wrapping properties for each dependency property field, or be of a valid assignable type from the type of each wrapping property). + /// + public static readonly DiagnosticDescriptor InvalidPropertyTypeOnDependencyPropertyField = new( + id: "WCTDP0030", + title: "Invalid property type in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its property type '{1}' does not match the type '{2}' of the wrapping property '{3}', and there is no valid type conversion between the two (all property types should either match the type of the wrapping properties for each dependency property field, or be of a valid assignable type from the type of each wrapping property)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields should declare property types in metadata matching the type of their wrapping properties, or with a valid type conversion between the two.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } From 189bd516b61209260ceaf5410745bd85ce5e5978 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 14:26:25 +0100 Subject: [PATCH 178/200] Update analyzer tests, add more tests --- .../Test_Analyzers.cs | 187 +++++++++++++++++- 1 file changed, 178 insertions(+), 9 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 4b94a0d5d..d3cf50e6a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2112,7 +2112,7 @@ namespace MyApp; public partial class MyControl : Control { public static readonly DependencyProperty OtherNameProperty = DependencyProperty.Register( - name: "Name", + {|WCTDP0027:name: "Name"|}, propertyType: typeof(string), ownerType: typeof(MyControl), typeMetadata: null); @@ -2128,17 +2128,113 @@ public string? Name await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_WCTDP0026_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty {|WCTDP0026:NameField|} = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyControl), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameField); + set => SetValue(NameField, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("null", "typeof(string)", "typeof(MyControl)", "null")] - [DataRow("\"NameProperty\"", "typeof(string)", "typeof(MyControl)", "null")] - [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_NoAdditionalDiagnostic_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: {{name}}, + propertyType: {{propertyType}}, + ownerType: {{ownerType}}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] [DataRow("\"Name\"", "typeof(int)", "typeof(MyControl)", "null")] [DataRow("\"Name\"", "typeof(MyControl)", "typeof(MyControl)", "null")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: {{name}}, + {|WCTDP0030:propertyType: {{propertyType}}|}, + ownerType: {{ownerType}}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] - [DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_DoesNotWarn( + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0029_DoesNotWarn( string name, string propertyType, string ownerType, @@ -2157,6 +2253,42 @@ public partial class MyControl : Control public static readonly DependencyProperty NameProperty = DependencyProperty.Register( name: {{name}}, propertyType: {{propertyType}}, + {|WCTDP0029:ownerType: {{ownerType}}|}, + typeMetadata: {{typeMetadata}}); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("\"NameProperty\"", "typeof(string)", "typeof(MyControl)", "null")] + [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0027_WCTDP0028_DoesNotWarn( + string name, + string propertyType, + string ownerType, + string typeMetadata) + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + {|WCTDP0027:{|WCTDP0028:name: {{name}}|}|}, + propertyType: {{propertyType}}, ownerType: {{ownerType}}, typeMetadata: {{typeMetadata}}); @@ -2281,12 +2413,8 @@ public enum MyEnum { A, B, C } // Some of these combinations (eg. 'object' property with 'T1?' backing type) are also just flat out invalid and would error out. [TestMethod] [DataRow("T1?", "T1?", "new PropertyMetadata(default(T1))")] - [DataRow("T1", "object", "new PropertyMetadata(default(T1))")] - [DataRow("T1?", "object", "new PropertyMetadata(default(T1))")] [DataRow("object", "T1?", "new PropertyMetadata(default(T1))")] [DataRow("T2?", "T2?", "new PropertyMetadata(default(T2))")] - [DataRow("T2", "object", "new PropertyMetadata(default(T2))")] - [DataRow("T2?", "object", "new PropertyMetadata(default(T2))")] [DataRow("object", "T2?", "new PropertyMetadata(default(T2))")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_DoesNotWarn( string dependencyPropertyType, @@ -2323,6 +2451,47 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Same as above, but this one also produces some WCTDP0030 warnings, so we need to split these cases out + [TestMethod] + [DataRow("T1", "object", "new PropertyMetadata(default(T1))")] + [DataRow("T1?", "object", "new PropertyMetadata(default(T1))")] + [DataRow("T2", "object", "new PropertyMetadata(default(T2))")] + [DataRow("T2?", "object", "new PropertyMetadata(default(T2))")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_ConstrainedGeneric_WithMismatchedType_DoesNotWarn( + string dependencyPropertyType, + string propertyType, + string propertyMetadataExpression) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyControl : Control + where T1 : struct, Enum + where T2 : unmanaged, Enum + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + {|WCTDP0030:propertyType: typeof({{dependencyPropertyType}})|}, + ownerType: typeof(MyControl), + typeMetadata: {{propertyMetadataExpression}}); + + public {{propertyType}} Name + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithInvalidAttribute_DoesNotWarn() { From 2f412ccca6a2f8b69bf721ee41c875aae8c269b7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 14:29:46 +0100 Subject: [PATCH 179/200] Update code fixer unit tests --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 88 +++++++++++++------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 29e18c0a5..cd5149913 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; using CSharpCodeFixTest = CommunityToolkit.GeneratedDependencyProperty.Tests.Helpers.CSharpCodeFixTest< CommunityToolkit.GeneratedDependencyProperty.UseGeneratedDependencyPropertyOnManualPropertyAnalyzer, @@ -104,7 +105,8 @@ public class MyClass { } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -164,7 +166,8 @@ public enum MyEnum { A, B, C } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -248,7 +251,8 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -315,7 +319,8 @@ public enum MyEnum { A } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -367,7 +372,8 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -425,7 +431,8 @@ public enum MyEnum { A, B } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -485,7 +492,8 @@ public enum MyEnum { A, B } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -538,7 +546,8 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -628,7 +637,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -699,7 +709,8 @@ public class TestAttribute(int X, string Y) : Attribute; CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -766,7 +777,8 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -849,7 +861,8 @@ public abstract partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -922,7 +935,8 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -998,7 +1012,8 @@ public string? Name1 CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1081,7 +1096,8 @@ public string? Name1 CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1210,7 +1226,8 @@ public partial class MyControl : Control CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1301,7 +1318,8 @@ public class TestAttribute(int X, string Y) : Attribute; CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1508,7 +1526,8 @@ public double VerticalOffset CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1692,7 +1711,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1748,7 +1768,8 @@ public partial class MyNestedObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1804,7 +1825,8 @@ public partial class MyNestedObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1860,7 +1882,8 @@ public partial class MyNestedObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1914,7 +1937,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -1970,7 +1994,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -2221,7 +2246,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -2296,7 +2322,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -2371,7 +2398,8 @@ public partial class MyObject : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -2424,7 +2452,8 @@ public abstract partial class KeyFrame : DependencyObject CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); @@ -2535,7 +2564,8 @@ public enum MyEnum { A, B } CSharpCodeFixTest test = new(LanguageVersion.Preview) { TestCode = original, - FixedCode = @fixed + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor }; await test.RunAsync(); From 544c0e5bc6e2ccc511dee6afd9927e57f72e9322 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 16:27:53 +0100 Subject: [PATCH 180/200] Enable diagnostics on orphaned properties, add tests --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 49 ++++++++--- .../Test_Analyzers.cs | 86 +++++++++++++++++++ 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 0fa9267b0..8370a5cff 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -380,18 +380,19 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - // Check that the field is one of the ones we expect to encounter - if (!fieldMap.TryGetValue(fieldSymbol, out FieldFlags? fieldFlags)) + // Validate that we are calling 'DependencyProperty.Register' + if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, dependencyPropertyRegisterSymbol)) { return; } - fieldFlags.FieldSymbol = fieldSymbol; - - // Validate that we are calling 'DependencyProperty.Register' - if (!SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod, dependencyPropertyRegisterSymbol)) + // Check that the field is one of the ones we expect to encounter + if (fieldMap.TryGetValue(fieldSymbol, out FieldFlags? fieldFlags)) { - return; + // Start to set some field flags, if we could retrieve an instance. This comment applies to all equivalent + // statements below. We want to still emit any applicable diagnostics even in case the field is orphaned. + // To do so, we do that validation even if we couldn't retrieve the flags, and simply skip any assignments. + fieldFlags.FieldSymbol = fieldSymbol; } // Additional diagnostic #1: the dependency property field name should have the "Property" suffix @@ -402,7 +403,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldSymbol.Locations.FirstOrDefault(), fieldSymbol)); - fieldFlags.HasAnyDiagnostics = true; + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } } // Next, make sure we have the arguments we expect @@ -411,13 +415,19 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - fieldFlags.PropertyNameExpressionLocation = nameArgument.Syntax.GetLocation(); - fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); + if (fieldFlags is not null) + { + fieldFlags.PropertyNameExpressionLocation = nameArgument.Syntax.GetLocation(); + fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); + } // We cannot validate the property name from here yet, but let's check it's a constant, and save it for later if (nameArgument.Value.ConstantValue is { HasValue: true, Value: string propertyName }) { - fieldFlags.PropertyName = propertyName; + if (fieldFlags is not null) + { + fieldFlags.PropertyName = propertyName; + } // Additional diagnostic #2: the property name should be the same as the field name, without the "Property" suffix (as per convention) if (fieldSymbol.Name.EndsWith("Property") && propertyName != fieldSymbol.Name[..^"Property".Length]) @@ -428,7 +438,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldSymbol, propertyName)); - fieldFlags.HasAnyDiagnostics = true; + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } } } @@ -446,10 +459,20 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla owningTypeSymbol, typeSymbol)); - fieldFlags.HasAnyDiagnostics = true; + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } } } + // At this point we've exhausted all possible diagnostics that are also valid on orphaned fields. + // We can now validate that we did manage to retrieve the field flags, and stop if we didn't. + if (fieldFlags is null) + { + return; + } + // Extract the property type, we can validate it later if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index d3cf50e6a..0c10ea5ac 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2800,6 +2800,92 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_NoPropertySuffixOnDependencyPropertyField_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty {|WCTDP0026:NameField|} = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_InvalidPropertyNameOnDependencyPropertyField_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + {|WCTDP0027:name: "Text"|}, + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_InvalidOwningTypeOnDependencyPropertyField_Warns() + { + const string source = """ + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + {|WCTDP0029:ownerType: typeof(MyOtherObject)|}, + typeMetadata: null); + } + + public class MyOtherObject : DependencyObject; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() { From ddce264b5ebf6a58881daaa28b244b7ea8751ece Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 18:09:45 +0100 Subject: [PATCH 181/200] Emit 'WCTDP0030' even on misnamed fields --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 14 +++---- .../Test_Analyzers.cs | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 8370a5cff..d5bfa92f5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -269,13 +269,6 @@ void HandleGetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - // The field must follow the expected naming pattern. We can check this just here in the getter. - // If this is valid, the whole property will be skipped anyway, so no need to do it twice. - if (fieldSymbol.Name != $"{propertySymbol.Name}Property") - { - return; - } - // We can in the meantime at least verify that we do have the field in the set if (!fieldMap.ContainsKey(fieldSymbol)) { @@ -714,6 +707,13 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla continue; } + // The field must follow the expected naming pattern. This check could be in the getter, but we're delaying + // it to here so that we can still emit all other diagnostics even if this check would otherwise fail. + if (getValueFieldSymbol.Name != $"{pair.Key.Name}Property") + { + continue; + } + // If any diagnostics were produced, we consider the property as invalid. We use this sytem to be // able to still execute the rest of the logic, so we can emit all applicable diagnostics at the // same time. Otherwise we'd only ever produce one at a time, which makes for a frustrating experience. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 0c10ea5ac..77e446cc5 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2230,6 +2230,47 @@ public string? Name await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Regression test for a case found in https://github.com/jenius-apps/ambie + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithInvalidPropertyName_DoesNotWarn() + { + string source = $$""" + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace AmbientSounds.Controls; + + public sealed partial class PlayerControl : UserControl + { + public static readonly DependencyProperty PlayVisibleProperty = DependencyProperty.Register( + {|WCTDP0027:nameof(PlayButtonVisible)|}, + {|WCTDP0030:typeof(bool)|}, + typeof(PlayerControl), + new PropertyMetadata(Visibility.Visible)); + + public static readonly DependencyProperty VolumeVisibleProperty = DependencyProperty.Register( + nameof(VolumeVisible), + {|WCTDP0030:typeof(bool)|}, + typeof(PlayerControl), + new PropertyMetadata(Visibility.Visible)); + + public Visibility PlayButtonVisible + { + get => (Visibility)GetValue(PlayVisibleProperty); + set => SetValue(PlayVisibleProperty, value); + } + + public Visibility VolumeVisible + { + get => (Visibility)GetValue(VolumeVisibleProperty); + set => SetValue(VolumeVisibleProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] From 2ca8aa86fb08e6a035ed33d145f26a049d3f38c6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 21:22:57 +0100 Subject: [PATCH 182/200] Add new diagnostics for default values --- .../AnalyzerReleases.Shipped.md | 2 + ...endencyPropertyOnManualPropertyAnalyzer.cs | 83 +++++++++-- .../Diagnostics/DiagnosticDescriptors.cs | 26 ++++ .../Test_Analyzers.cs | 94 +++++++++++- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 135 +++++++++++++++++- 5 files changed, 320 insertions(+), 20 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 533b79927..45a33a153 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -37,3 +37,5 @@ WCTDP0027 | DependencyPropertyGenerator | Warning | WCTDP0028 | DependencyPropertyGenerator | Warning | WCTDP0029 | DependencyPropertyGenerator | Warning | WCTDP0030 | DependencyPropertyGenerator | Warning | +WCTDP0031 | DependencyPropertyGenerator | Warning | +WCTDP0032 | DependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index d5bfa92f5..4aba76898 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -76,7 +76,9 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia InvalidPropertyNameOnDependencyPropertyField, MismatchedPropertyNameOnDependencyPropertyField, InvalidOwningTypeOnDependencyPropertyField, - InvalidPropertyTypeOnDependencyPropertyField + InvalidPropertyTypeOnDependencyPropertyField, + InvalidDefaultValueNullOnDependencyPropertyField, + InvalidDefaultValueTypeOnDependencyPropertyField ]; /// @@ -459,20 +461,16 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } } - // At this point we've exhausted all possible diagnostics that are also valid on orphaned fields. - // We can now validate that we did manage to retrieve the field flags, and stop if we didn't. - if (fieldFlags is null) - { - return; - } - // Extract the property type, we can validate it later if (propertyTypeArgument.Value is not ITypeOfOperation { TypeOperand: { } propertyTypeSymbol }) { return; } - fieldFlags.PropertyType = propertyTypeSymbol; + if (fieldFlags is not null) + { + fieldFlags.PropertyType = propertyTypeSymbol; + } // First, check if the metadata is 'null' (simplest case) if (propertyMetadataArgument.Value.ConstantValue is { HasValue: true, Value: null }) @@ -483,7 +481,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla if (!propertyTypeSymbol.IsDefaultValueNull() && !propertyTypeSymbol.IsWellKnownWinRTProjectedValueType(useWindowsUIXaml)) { - fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + if (fieldFlags is not null) + { + fieldFlags.DefaultValue = TypedConstantInfo.Null.Instance; + } } } else @@ -506,14 +507,72 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } + // Emit diagnostics for invalid default values (equivalent logic to 'InvalidPropertyDefaultValueTypeAnalyzer'). + // Note: we don't flag the property if we emit diagnostics here, as we can still apply the code fixer either way. + // If we do that, then the other analyzer will simply start producing an error on the attribute, rather than here. + if (defaultValueArgument.Value is { ConstantValue: { HasValue: true, Value: null } }) + { + // Default value diagnostic #1: the value is 'null' but the property type is not nullable + if (!propertyTypeSymbol.IsDefaultValueNull()) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDefaultValueNullOnDependencyPropertyField, + defaultValueArgument.Syntax.GetLocation(), + fieldSymbol, + propertyTypeSymbol)); + } + } + else + { + bool isDependencyPropertyUnsetValue = + defaultValueArgument.Value is IPropertyReferenceOperation { Property: { IsStatic: true, Name: "UnsetValue" } unsetValuePropertySymbol } && + SymbolEqualityComparer.Default.Equals(unsetValuePropertySymbol.ContainingType, dependencyPropertySymbol); + + // We never emit diagnostics when assigning 'UnsetValue': this value is special, so let's just ignore it + if (!isDependencyPropertyUnsetValue) + { + // Get the type of the conversion operation (same as the one below, but we get it here too just to opportunistically emit a diagnostic) + if (defaultValueArgument.Value is IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object, Operand.Type: { } operandTypeSymbol }) + { + // Get the target type with a special case for 'Nullable' (same as in the other analyzer) + ITypeSymbol unwrappedPropertyTypeSymbol = propertyTypeSymbol.IsNullableValueType() + ? ((INamedTypeSymbol)propertyTypeSymbol).TypeArguments[0] + : propertyTypeSymbol; + + // Warn if the type of the default value is not compatible + if (!SymbolEqualityComparer.Default.Equals(unwrappedPropertyTypeSymbol, operandTypeSymbol) && + !SymbolEqualityComparer.Default.Equals(propertyTypeSymbol, operandTypeSymbol) && + context.Compilation.ClassifyConversion(operandTypeSymbol, propertyTypeSymbol) is not ({ IsBoxing: true } or { IsReference: true })) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDefaultValueTypeOnDependencyPropertyField, + defaultValueArgument.Syntax.GetLocation(), + fieldSymbol, + operandTypeSymbol, + propertyTypeSymbol)); + } + } + } + } + // The argument should be a conversion operation (boxing) if (defaultValueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) { return; } - // Store the operation for later, as we need to wait to also get the metadata type to do the full validation - fieldFlags.DefaultValueOperation = conversionOperation.Operand; + if (fieldFlags is not null) + { + // Store the operation for later, as we need to wait to also get the metadata type to do the full validation + fieldFlags.DefaultValueOperation = conversionOperation.Operand; + } + } + + // At this point we've exhausted all possible diagnostics that are also valid on orphaned fields. + // We can now validate that we did manage to retrieve the field flags, and stop if we didn't. + if (fieldFlags is null) + { + return; } // Find the parent field for the operation (we're guaranteed to only fine one) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 2921d5fd2..d6acb208a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -420,4 +420,30 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "All dependency property fields should declare property types in metadata matching the type of their wrapping properties, or with a valid type conversion between the two.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). + /// + public static readonly DiagnosticDescriptor InvalidDefaultValueNullOnDependencyPropertyField = new( + id: "WCTDP0031", + title: "Invalid 'null' default value in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); + + /// + /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// + public static readonly DiagnosticDescriptor InvalidDefaultValueTypeOnDependencyPropertyField = new( + id: "WCTDP0032", + title: "Invalid default value type in dependency property field metadata", + messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + category: DiagnosticCategory, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 77e446cc5..044e8a984 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2246,13 +2246,13 @@ public sealed partial class PlayerControl : UserControl {|WCTDP0027:nameof(PlayButtonVisible)|}, {|WCTDP0030:typeof(bool)|}, typeof(PlayerControl), - new PropertyMetadata(Visibility.Visible)); + new PropertyMetadata({|WCTDP0032:Visibility.Visible|})); public static readonly DependencyProperty VolumeVisibleProperty = DependencyProperty.Register( nameof(VolumeVisible), {|WCTDP0030:typeof(bool)|}, typeof(PlayerControl), - new PropertyMetadata(Visibility.Visible)); + new PropertyMetadata({|WCTDP0032:Visibility.Visible|})); public Visibility PlayButtonVisible { @@ -2927,6 +2927,96 @@ public class MyOtherObject : DependencyObject; await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("string", "null")] + [DataRow("string", "\"Bob\"")] + [DataRow("object", "null")] + [DataRow("object", "\"Bob\"")] + [DataRow("object", "42")] + [DataRow("int?", "null")] + [DataRow("Visibility?", "null")] + [DataRow("string", "DependencyProperty.UnsetValue")] + [DataRow("object", "DependencyProperty.UnsetValue")] + [DataRow("int", "DependencyProperty.UnsetValue")] + [DataRow("int?", "DependencyProperty.UnsetValue")] + [DataRow("Visibility", "DependencyProperty.UnsetValue")] + [DataRow("Visibility?", "DependencyProperty.UnsetValue")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_WithExplicitDefaultValue_DoesNotWarn( + string propertyType, + string defaultValueExpression) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValueExpression}})); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int")] + [DataRow("global::System.TimeSpan")] + [DataRow("global::Windows.Foundation.Rect")] + [DataRow("Visibility")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_NullDefaultValue_Warns(string propertyType) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDP0031:null|})); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("int", "3.0")] + [DataRow("int", "3.0F")] + [DataRow("int", "3L")] + [DataRow("int", "\"Bob\"")] + [DataRow("int", "Visibility.Visible")] + [DataRow("int", "default(Visibility)")] + [DataRow("bool", "Visibility.Visible")] + [DataRow("string", "42")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_OrphanedPropertyField_InvalidDefaultValue_Warns(string propertyType, string defaultValue) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDP0032:{{defaultValue}}|})); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index cd5149913..de269c21b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2331,16 +2331,81 @@ public partial class MyObject : DependencyObject [TestMethod] [DataRow("string?", "string", "")] - [DataRow("int", "int", "(DefaultValue = null)")] [DataRow("int?", "int?", "")] [DataRow("T1?", "T1", "")] + [DataRow("T4?", "T4?", "")] + public async Task SimpleProperty_WithinGenericType_WithExplicitNullDefaultValue( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + nameof(Name), + typeof({{propertyType}}), + typeof(MyObject), + new PropertyMetadata(null)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + string @fixed = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + where T1 : class + where T3 : T2, new() + where T4 : unmanaged + where T5 : IDisposable + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } + + [TestMethod] [DataRow("T2", "T2", "(DefaultValue = null)")] [DataRow("T2?", "T2", "(DefaultValue = null)")] [DataRow("T4", "T4", "(DefaultValue = null)")] - [DataRow("T4?", "T4?", "")] [DataRow("T5", "T5", "(DefaultValue = null)")] [DataRow("T5?", "T5", "(DefaultValue = null)")] - public async Task SimpleProperty_WithinGenericType_WithExplicitNullDefaultValue( + public async Task SimpleProperty_WithinGenericType_WithExplicitNullDefaultValue_FixesButAlsoWarnsInOriginalCode( string declaredType, string propertyType, string attributeArguments) @@ -2364,7 +2429,7 @@ public partial class MyObject : DependencyObject nameof(Name), typeof({{propertyType}}), typeof(MyObject), - new PropertyMetadata(null)); + new PropertyMetadata({|WCTDP0031:null|})); public {{declaredType}} [|Name|] { @@ -2473,7 +2538,6 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("int?", "object", "new PropertyMetadata(0)", "(PropertyType = typeof(object), DefaultValue = 0)")] [DataRow("int?", "object", "new PropertyMetadata(default(int))", "(PropertyType = typeof(object), DefaultValue = 0)")] [DataRow("Visibility", "Visibility", "null", "")] - [DataRow("Visibility", "Visibility", "new PropertyMetadata(null)", "(DefaultValue = null)")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(default(Visibility))", "")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Visible)", "")] [DataRow("Visibility", "Visibility", "new PropertyMetadata(Visibility.Collapsed)", "(DefaultValue = Visibility.Collapsed)")] @@ -2494,7 +2558,6 @@ public abstract partial class KeyFrame : DependencyObject [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Visible)", "(PropertyType = typeof(object), DefaultValue = Visibility.Visible)")] [DataRow("Visibility?", "object", "new PropertyMetadata(Visibility.Collapsed)", "(PropertyType = typeof(object), DefaultValue = Visibility.Collapsed)")] [DataRow("MyEnum", "MyEnum", "null", "(DefaultValue = null)")] - [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(null)", "(DefaultValue = null)")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(default(MyEnum))", "")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.A)", "")] [DataRow("MyEnum", "MyEnum", "new PropertyMetadata(MyEnum.B)", "(DefaultValue = MyEnum.B)")] @@ -2570,4 +2633,64 @@ public enum MyEnum { A, B } await test.RunAsync(); } + + [TestMethod] + [DataRow("Visibility", "Visibility", "(DefaultValue = null)")] + [DataRow("MyEnum", "MyEnum", "(DefaultValue = null)")] + public async Task SimpleProperty_MixedEnumAndNullable_WithPropertyType_WithInvalidDefaultValueNull_HandlesAllScenariosCorrectly( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDP0031:null|})); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A, B } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A, B } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } } From d5189198c5891cada64c85df9551dc12dae09e0c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 22:16:44 +0100 Subject: [PATCH 183/200] Improve message formats for some diagnostics --- ...InvalidPropertyDefaultValueTypeAnalyzer.cs | 6 +++-- ...endencyPropertyOnManualPropertyAnalyzer.cs | 11 +++++++-- .../Diagnostics/DiagnosticDescriptors.cs | 24 +++++++++---------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs index 74890c45e..26b82dbc8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyDefaultValueTypeAnalyzer.cs @@ -97,7 +97,8 @@ public override void Initialize(AnalysisContext context) InvalidPropertyDefaultValueNull, attributeData.GetNamedArgumentOrAttributeLocation("DefaultValue"), propertySymbol, - propertySymbol.Type)); + propertySymbol.Type, + propertySymbol.Name)); } } else @@ -116,7 +117,8 @@ public override void Initialize(AnalysisContext context) propertySymbol, propertySymbol.Type, defaultValue.Value, - defaultValue.Type)); + defaultValue.Type, + propertySymbol.Name)); } } }, OperationKind.Attribute); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 4aba76898..0b373dcfa 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -416,6 +416,9 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); } + // Best effort name to use to interpolate any diagnostics below to emit if the default value is not valid + string propertyNameForMessageFormat = "___"; + // We cannot validate the property name from here yet, but let's check it's a constant, and save it for later if (nameArgument.Value.ConstantValue is { HasValue: true, Value: string propertyName }) { @@ -424,6 +427,8 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyName = propertyName; } + propertyNameForMessageFormat = propertyName; + // Additional diagnostic #2: the property name should be the same as the field name, without the "Property" suffix (as per convention) if (fieldSymbol.Name.EndsWith("Property") && propertyName != fieldSymbol.Name[..^"Property".Length]) { @@ -519,7 +524,8 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla InvalidDefaultValueNullOnDependencyPropertyField, defaultValueArgument.Syntax.GetLocation(), fieldSymbol, - propertyTypeSymbol)); + propertyTypeSymbol, + propertyNameForMessageFormat)); } } else @@ -549,7 +555,8 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla defaultValueArgument.Syntax.GetLocation(), fieldSymbol, operandTypeSymbol, - propertyTypeSymbol)); + propertyTypeSymbol, + propertyNameForMessageFormat)); } } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index d6acb208a..c19bfb234 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -149,29 +149,29 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueNull = new( id: "WCTDP0010", title: "Invalid 'null' default value for [GeneratedDependencyProperty] use", - messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'On{4}Get(ref object)' partial method to handle the type mismatch). /// public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueType = new( id: "WCTDP0011", title: "Invalid default value type for [GeneratedDependencyProperty] use", - messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'On{4}Get(ref object)' partial method to handle the type mismatch)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + description: "Properties annotated with [GeneratedDependencyProperty] and setting 'DefaultValue' should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// @@ -422,28 +422,28 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). + /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidDefaultValueNullOnDependencyPropertyField = new( id: "WCTDP0031", title: "Invalid 'null' default value in dependency property field metadata", - messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", + messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch). + /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'On{3}Get(ref object)' partial method to handle the type mismatch). /// public static readonly DiagnosticDescriptor InvalidDefaultValueTypeOnDependencyPropertyField = new( id: "WCTDP0032", title: "Invalid default value type in dependency property field metadata", - messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'Get(ref object)' partial method to handle the type mismatch)", + messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'On{3}Get(ref object)' partial method to handle the type mismatch)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the 'Get(ref object)' method should be implemented to handle the type mismatch.", + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } From ff99c7755023ac7545b343c38c03c96d0bc014f0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 4 Jan 2025 22:37:01 +0100 Subject: [PATCH 184/200] Handle 'UnsetValue' in analyzer and code fixer --- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 10 +++ ...endencyPropertyOnManualPropertyAnalyzer.cs | 71 +++++++++++-------- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 62 ++++++++++++++++ 3 files changed, 115 insertions(+), 28 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 6a1e26d1c..6b222901d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.CodeFixers/UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -168,6 +168,16 @@ void HandleDefaultValue(ref AttributeListSyntax generatedDependencyPropertyAttri { SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + // Special case for 'UnsetValue', we need to convert to the 'GeneratedDependencyProperty' type + if (defaultValueExpression == $"\"{UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.UnsetValueSpecialIdentifier}\"") + { + generatedDependencyPropertyAttributeList = (AttributeListSyntax)syntaxGenerator.AddAttributeArguments( + generatedDependencyPropertyAttributeList, + [syntaxGenerator.AttributeArgument("DefaultValue", ParseExpression("GeneratedDependencyProperty.UnsetValue"))]); + + return; + } + // Special case if we have a location for the original expression, and we can resolve the node. // In this case, we want to just carry that over with no changes (this is used for named constants). // See notes below for how this method is constructing the new attribute argument to insert. diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 0b373dcfa..edd39b04c 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -68,6 +68,11 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia /// public const string AdditionalLocationKindPropertyName = "AdditionalLocationKind"; + /// + /// The special identifier to represent the unset value. This allows marshalling the value as a , for simplicity. + /// + public const string UnsetValueSpecialIdentifier = "E5FDFA8B-7E6B-4217-8844-52E5B30084B1"; + /// public override ImmutableArray SupportedDiagnostics { get; } = [ @@ -512,6 +517,8 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } + bool isDependencyPropertyUnsetValue = false; + // Emit diagnostics for invalid default values (equivalent logic to 'InvalidPropertyDefaultValueTypeAnalyzer'). // Note: we don't flag the property if we emit diagnostics here, as we can still apply the code fixer either way. // If we do that, then the other analyzer will simply start producing an error on the attribute, rather than here. @@ -530,48 +537,56 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla } else { - bool isDependencyPropertyUnsetValue = + isDependencyPropertyUnsetValue = defaultValueArgument.Value is IPropertyReferenceOperation { Property: { IsStatic: true, Name: "UnsetValue" } unsetValuePropertySymbol } && SymbolEqualityComparer.Default.Equals(unsetValuePropertySymbol.ContainingType, dependencyPropertySymbol); // We never emit diagnostics when assigning 'UnsetValue': this value is special, so let's just ignore it - if (!isDependencyPropertyUnsetValue) + // for the purposes of diagnostics. However, we do need to track the special identifier for the code fixer. + if (isDependencyPropertyUnsetValue) { - // Get the type of the conversion operation (same as the one below, but we get it here too just to opportunistically emit a diagnostic) - if (defaultValueArgument.Value is IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object, Operand.Type: { } operandTypeSymbol }) + if (fieldFlags is not null) { - // Get the target type with a special case for 'Nullable' (same as in the other analyzer) - ITypeSymbol unwrappedPropertyTypeSymbol = propertyTypeSymbol.IsNullableValueType() - ? ((INamedTypeSymbol)propertyTypeSymbol).TypeArguments[0] - : propertyTypeSymbol; - - // Warn if the type of the default value is not compatible - if (!SymbolEqualityComparer.Default.Equals(unwrappedPropertyTypeSymbol, operandTypeSymbol) && - !SymbolEqualityComparer.Default.Equals(propertyTypeSymbol, operandTypeSymbol) && - context.Compilation.ClassifyConversion(operandTypeSymbol, propertyTypeSymbol) is not ({ IsBoxing: true } or { IsReference: true })) - { - context.ReportDiagnostic(Diagnostic.Create( - InvalidDefaultValueTypeOnDependencyPropertyField, - defaultValueArgument.Syntax.GetLocation(), - fieldSymbol, - operandTypeSymbol, - propertyTypeSymbol, - propertyNameForMessageFormat)); - } + fieldFlags.DefaultValue = new TypedConstantInfo.Primitive.String(UnsetValueSpecialIdentifier); + } + } + else if (defaultValueArgument.Value is IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object, Operand.Type: { } operandTypeSymbol }) + { + // Get the type of the conversion operation (same as the one below, but we get it here too just to opportunistically emit a diagnostic). + // Additionally, we need to get the target type with a special case for 'Nullable' (same as in the other analyzer). + ITypeSymbol unwrappedPropertyTypeSymbol = propertyTypeSymbol.IsNullableValueType() + ? ((INamedTypeSymbol)propertyTypeSymbol).TypeArguments[0] + : propertyTypeSymbol; + + // Warn if the type of the default value is not compatible + if (!SymbolEqualityComparer.Default.Equals(unwrappedPropertyTypeSymbol, operandTypeSymbol) && + !SymbolEqualityComparer.Default.Equals(propertyTypeSymbol, operandTypeSymbol) && + context.Compilation.ClassifyConversion(operandTypeSymbol, propertyTypeSymbol) is not ({ IsBoxing: true } or { IsReference: true })) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidDefaultValueTypeOnDependencyPropertyField, + defaultValueArgument.Syntax.GetLocation(), + fieldSymbol, + operandTypeSymbol, + propertyTypeSymbol, + propertyNameForMessageFormat)); } } } // The argument should be a conversion operation (boxing) - if (defaultValueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) + if (defaultValueArgument.Value is IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation) { - return; + if (fieldFlags is not null) + { + // Store the operation for later, as we need to wait to also get the metadata type to do the full validation + fieldFlags.DefaultValueOperation = conversionOperation.Operand; + } } - - if (fieldFlags is not null) + else if (!isDependencyPropertyUnsetValue) { - // Store the operation for later, as we need to wait to also get the metadata type to do the full validation - fieldFlags.DefaultValueOperation = conversionOperation.Operand; + // The only other supported case is for 'UnsetValue', so stop here if that's not it + return; } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index de269c21b..b7c3be135 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2693,4 +2693,66 @@ public enum MyEnum { A, B } await test.RunAsync(); } + + [TestMethod] + [DataRow("int", "int", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("int?", "int?", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("int?", "object", "(PropertyType = typeof(object), DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("string", "string", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("string", "object", "(PropertyType = typeof(object), DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("Visibility", "Visibility", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("Visibility?", "Visibility?", "(DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + [DataRow("Visibility?", "object", "(PropertyType = typeof(object), DefaultValue = GeneratedDependencyProperty.UnsetValue)")] + public async Task SimpleProperty_WithDefaultValue_UnsetValue( + string declaredType, + string propertyType, + string attributeArguments) + { + string original = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{propertyType}}), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata(DependencyProperty.UnsetValue)); + + public {{declaredType}} [|Name|] + { + get => ({{declaredType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public enum MyEnum { A, B } + """; + + string @fixed = $$""" + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty{{attributeArguments}}] + public partial {{declaredType}} {|CS9248:Name|} { get; set; } + } + + public enum MyEnum { A, B } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } } From 901c1067ba5d6295e142a7c1067755be2135c0c1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 5 Jan 2025 01:25:15 +0100 Subject: [PATCH 185/200] Fix format message for 'WCTDP0028' diagnostic --- .../UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index edd39b04c..1678545c3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -647,6 +647,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla context.ReportDiagnostic(Diagnostic.Create( MismatchedPropertyNameOnDependencyPropertyField, fieldFlags.PropertyNameExpressionLocation!, + fieldFlags.FieldSymbol, fieldFlags.PropertyName, pair.Key.Name)); } From 515a42c9410177276bc8f53595e9c318f753d623 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 5 Jan 2025 01:46:41 +0100 Subject: [PATCH 186/200] Add more unit tests for 'WCTDP0030' diagnostic --- .../Test_Analyzers.cs | 78 +++++++++++++++---- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 044e8a984..d7512aa1e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2195,33 +2195,37 @@ public string? Name } [TestMethod] - [DataRow("\"Name\"", "typeof(int)", "typeof(MyControl)", "null")] - [DataRow("\"Name\"", "typeof(MyControl)", "typeof(MyControl)", "null")] + [DataRow("int", "float")] + [DataRow("float", "double")] + [DataRow("int", "object")] + [DataRow("int?", "object")] + [DataRow("string", "object")] + [DataRow("MyControl", "IDisposable")] + [DataRow("MyControl", "Control")] + [DataRow("MyControl", "DependencyObject")] + [DataRow("Control", "DependencyObject")] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_DoesNotWarn( - string name, - string propertyType, - string ownerType, - string typeMetadata) + string dependencyPropertyType, + string propertyType) { string source = $$""" + using System; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; - - #nullable enable namespace MyApp; public partial class MyControl : Control { public static readonly DependencyProperty NameProperty = DependencyProperty.Register( - name: {{name}}, - {|WCTDP0030:propertyType: {{propertyType}}|}, - ownerType: {{ownerType}}, - typeMetadata: {{typeMetadata}}); + name: "Name", + {|WCTDP0030:propertyType: typeof({{dependencyPropertyType}})|}, + ownerType: typeof(MyControl), + typeMetadata: null); - public string? Name + public {{propertyType}} Name { - get => (string?)GetValue(NameProperty); + get => ({{propertyType}})GetValue(NameProperty); set => SetValue(NameProperty, value); } } @@ -3017,6 +3021,52 @@ public class MyObject : DependencyObject await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // This is an explicit test to ensure upcasts are correctly detected and allowed + [TestMethod] + [DataRow("IDisposable", "MyClass")] + [DataRow("object", "string")] + [DataRow("object", "int")] + [DataRow("object", "int?")] + [DataRow("Control", "MyControl")] + [DataRow("DependencyObject", "MyControl")] + [DataRow("DependencyObject", "Control")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithUpcast_Warns( + string dependencyPropertyType, + string propertyType) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof({{dependencyPropertyType}}), + ownerType: typeof(MyControl), + typeMetadata: null); + + public {{propertyType}} {|WCTDP0017:Name|} + { + get => ({{propertyType}})GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + + public class MyClass : IDisposable + { + public void Dispose() + { + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() { From d6743ba2dee62ec42b702f968cf0fee8aa58e507 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 5 Jan 2025 02:05:27 +0100 Subject: [PATCH 187/200] Emit diagnostics even if default value is not constant --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 20 +++---- .../Test_Analyzers.cs | 57 +++++++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 1678545c3..a66f3e7d9 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -736,19 +736,19 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // pretending this were the empty string literal instead. This way we can still support the property and convert to an attribute. fieldFlags.DefaultValue = TypedConstantInfo.Primitive.String.Empty; } - else + else if (fieldFlags.DefaultValueOperation is IDefaultValueOperation { Type: { } defaultValueExpressionType }) { - // If we don't have a constant, check if it's some constant value we can forward. In this case, we - // did not retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression. - if (fieldFlags.DefaultValueOperation is not IDefaultValueOperation { Type: { } defaultValueExpressionType }) - { - continue; - } - - // Store the expression type for later, so we can validate it. We cannot validate it from here, as we - // only see the declared property type for metadata. This isn't guaranteed to match the property type. + // We don't have a constant. As a last resort, check if this is explicitly a 'default(T)' expression. + // If so, store the expression type for later, so we can validate it. We cannot validate it here, as + // we still want to execute the rest of the checks below to potentially emit more diagnostics first. fieldFlags.DefaultValueExpressionType = defaultValueExpressionType; } + else + { + // At this point we know the property cannot possibly be converted, so mark it as invalid. We do this + // rather than returning immediately, to still allow more diagnostics to be produced in the steps below. + fieldFlags.HasAnyDiagnostics = true; + } } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index d7512aa1e..afc365c6e 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2234,6 +2234,63 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Regression test for a case found in the Microsoft Store + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithObjectInitialization_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty MarginProperty = DependencyProperty.Register( + nameof(Margin), + {|WCTDP0030:typeof(double)|}, + typeof(MyControl), + new PropertyMetadata({|WCTDP0032:new Thickness(0)|})); + + private Thickness Margin + { + get => (Thickness)GetValue(MarginProperty); + set => SetValue(MarginProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithExplicitDefaultValueNull_DoesNotWarn() + { + const string source = """ + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + namespace MyApp; + + public partial class MyControl : Control + { + public static readonly DependencyProperty MarginProperty = DependencyProperty.Register( + nameof(Margin), + {|WCTDP0030:typeof(double)|}, + typeof(MyControl), + new PropertyMetadata({|WCTDP0031:null|})); + + private Thickness Margin + { + get => (Thickness)GetValue(MarginProperty); + set => SetValue(MarginProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + // Regression test for a case found in https://github.com/jenius-apps/ambie [TestMethod] public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithInvalidPropertyName_DoesNotWarn() From c3ec9cf25ed101e582d8803e7c4bb4b3812e53b0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 5 Jan 2025 15:51:56 +0100 Subject: [PATCH 188/200] Handle name colon in forwarded attribute arguments --- .../Models/AttributeInfo.cs | 31 +++++++++++++++---- .../Test_DependencyPropertyGenerator.cs | 9 ++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs index 8ac269e7c..346403db6 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Models/AttributeInfo.cs @@ -23,7 +23,7 @@ namespace CommunityToolkit.GeneratedDependencyProperty.Models; /// The values for all named arguments for the attribute. internal sealed record AttributeInfo( string TypeName, - EquatableArray ConstructorArgumentInfo, + EquatableArray<(string? Name, TypedConstantInfo Value)> ConstructorArgumentInfo, EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) { /// @@ -44,7 +44,7 @@ public static bool TryCreate( { string typeName = typeSymbol.GetFullyQualifiedName(); - using ImmutableArrayBuilder constructorArguments = new(); + using ImmutableArrayBuilder<(string?, TypedConstantInfo)> constructorArguments = new(); using ImmutableArrayBuilder<(string, TypedConstantInfo)> namedArguments = new(); foreach (AttributeArgumentSyntax argument in arguments) @@ -65,13 +65,18 @@ public static bool TryCreate( // Try to get the identifier name if the current expression is a named argument expression. If it // isn't, then the expression is a normal attribute constructor argument, so no extra work is needed. - if (argument.NameEquals is { Name.Identifier.ValueText: string argumentName }) + if (argument.NameEquals is { Name.Identifier.ValueText: string nameEqualsName }) { - namedArguments.Add((argumentName, argumentInfo)); + namedArguments.Add((nameEqualsName, argumentInfo)); + } + else if (argument.NameColon is { Name.Identifier.ValueText: string nameColonName }) + { + // This special case also handles named constructor parameters (i.e. '[Test(value: 42)]', not '[Test(Value = 42)]') + constructorArguments.Add((nameColonName, argumentInfo)); } else { - constructorArguments.Add(argumentInfo); + constructorArguments.Add((null, argumentInfo)); } } @@ -86,10 +91,24 @@ public static bool TryCreate( /// public override string ToString() { + // Helper to format constructor parameters + static AttributeArgumentSyntax CreateConstructorArgument(string? name, TypedConstantInfo value) + { + AttributeArgumentSyntax argument = AttributeArgument(ParseExpression(value.ToString())); + + // The name color expression is not guaranteed to be present (in fact, it's more common for it to be missing) + if (name is not null) + { + argument = argument.WithNameColon(NameColon(IdentifierName(name))); + } + + return argument; + } + // Gather the constructor arguments IEnumerable arguments = ConstructorArgumentInfo - .Select(static arg => AttributeArgument(ParseExpression(arg.ToString()))); + .Select(static arg => CreateConstructorArgument(arg.Name, arg.Value)); // Gather the named arguments IEnumerable namedArguments = diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs index 6561a7826..c1be55365 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_DependencyPropertyGenerator.cs @@ -4149,10 +4149,15 @@ public partial {{propertyType}} Name [TestMethod] [DataRow("A", "global::MyNamespace.AAttribute()")] [DataRow("B(42, 10)", "global::MyNamespace.BAttribute(42, 10)")] + [DataRow("B(X: 42, Y: 10)", "global::MyNamespace.BAttribute(X: 42, Y: 10)")] + [DataRow("B(Y: 42, X: 10)", "global::MyNamespace.BAttribute(Y: 42, X: 10)")] + [DataRow("B(42, Y: 10)", "global::MyNamespace.BAttribute(42, Y: 10)")] [DataRow("""C(10, X = "Test", Y = 42)""", """global::MyNamespace.CAttribute(10, X = "Test", Y = 42)""")] + [DataRow("""C(Z: 10, X = "Test", Y = 42)""", """global::MyNamespace.CAttribute(Z: 10, X = "Test", Y = 42)""")] [DataRow("D(Foo.B, typeof(string), new[] { 1, 2, 3 })", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] [DataRow("D(Foo.B, typeof(string), new int[] { 1, 2, 3 })", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] [DataRow("D(Foo.B, typeof(string), [1, 2, 3])", "global::MyNamespace.DAttribute(global::MyNamespace.Foo.B, typeof(string), new int[] { 1, 2, 3 })")] + [DataRow("""E(42, Y: 10, Z: "Bob", W = 100)""", """global::MyNamespace.EAttribute(42, Y: 10, Z: "Bob", W = 100)""")] public void SingleProperty_String_WithNoCaching_WithForwardedAttribute( string attributeDefinition, string attributeForwarding) @@ -4179,6 +4184,10 @@ public class CAttribute(int Z) : Attribute public int Y { get; set; } } public class DAttribute(Foo X, Type Y, int[] Z) : Attribute; + public class EAttribute(int X, int Y, string Z) : Attribute + { + public int W { get; set; } + } public enum Foo { A, B } """; From 762665e3339c8d77c3b805da1e517d2a0e660159 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 5 Jan 2025 18:14:52 +0100 Subject: [PATCH 189/200] Tweak diagnostic error codes --- .../AnalyzerReleases.Shipped.md | 64 +++---- .../Diagnostics/DiagnosticDescriptors.cs | 64 +++---- .../Test_Analyzers.cs | 168 +++++++++--------- .../Test_UseFieldDeclarationCodeFixer.cs | 2 +- ...ndencyPropertyOnManualPropertyCodeFixer.cs | 4 +- 5 files changed, 151 insertions(+), 151 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md index 45a33a153..e8f0ff5f4 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md @@ -7,35 +7,35 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -WCTDP0001 | DependencyPropertyGenerator | Error | -WCTDP0002 | DependencyPropertyGenerator | Error | -WCTDP0003 | DependencyPropertyGenerator | Error | -WCTDP0004 | DependencyPropertyGenerator | Error | -WCTDP0005 | DependencyPropertyGenerator | Error | -WCTDP0006 | DependencyPropertyGenerator | Error | -WCTDP0007 | DependencyPropertyGenerator | Error | -WCTDP0008 | DependencyPropertyGenerator | Error | -WCTDP0009 | DependencyPropertyGenerator | Warning | -WCTDP0010 | DependencyPropertyGenerator | Warning | -WCTDP0011 | DependencyPropertyGenerator | Warning | -WCTDP0012 | DependencyPropertyGenerator | Error | -WCTDP0013 | DependencyPropertyGenerator | Error | -WCTDP0014 | DependencyPropertyGenerator | Error | -WCTDP0015 | DependencyPropertyGenerator | Error | -WCTDP0016 | DependencyPropertyGenerator | Info | -WCTDP0017 | DependencyPropertyGenerator | Info | -WCTDP0018 | DependencyPropertyGenerator | Error | -WCTDP0019 | DependencyPropertyGenerator | Error | -WCTDP0020 | DependencyPropertyGenerator | Warning | -WCTDP0021 | DependencyPropertyGenerator | Warning | -WCTDP0022 | DependencyPropertyGenerator | Warning | -WCTDP0023 | DependencyPropertyGenerator | Error | -WCTDP0024 | DependencyPropertyGenerator | Warning | -WCTDP0025 | DependencyPropertyGenerator | Warning | -WCTDP0026 | DependencyPropertyGenerator | Warning | -WCTDP0027 | DependencyPropertyGenerator | Warning | -WCTDP0028 | DependencyPropertyGenerator | Warning | -WCTDP0029 | DependencyPropertyGenerator | Warning | -WCTDP0030 | DependencyPropertyGenerator | Warning | -WCTDP0031 | DependencyPropertyGenerator | Warning | -WCTDP0032 | DependencyPropertyGenerator | Warning | +WCTDPG0001 | DependencyPropertyGenerator | Error | +WCTDPG0002 | DependencyPropertyGenerator | Error | +WCTDPG0003 | DependencyPropertyGenerator | Error | +WCTDPG0004 | DependencyPropertyGenerator | Error | +WCTDPG0005 | DependencyPropertyGenerator | Error | +WCTDPG0006 | DependencyPropertyGenerator | Error | +WCTDPG0007 | DependencyPropertyGenerator | Error | +WCTDPG0008 | DependencyPropertyGenerator | Error | +WCTDPG0009 | DependencyPropertyGenerator | Warning | +WCTDPG0010 | DependencyPropertyGenerator | Warning | +WCTDPG0011 | DependencyPropertyGenerator | Warning | +WCTDPG0012 | DependencyPropertyGenerator | Error | +WCTDPG0013 | DependencyPropertyGenerator | Error | +WCTDPG0014 | DependencyPropertyGenerator | Error | +WCTDPG0015 | DependencyPropertyGenerator | Error | +WCTDPG0016 | DependencyPropertyGenerator | Info | +WCTDPG0017 | DependencyPropertyGenerator | Info | +WCTDPG0018 | DependencyPropertyGenerator | Error | +WCTDPG0019 | DependencyPropertyGenerator | Error | +WCTDPG0020 | DependencyPropertyGenerator | Warning | +WCTDPG0021 | DependencyPropertyGenerator | Warning | +WCTDPG0022 | DependencyPropertyGenerator | Warning | +WCTDPG0023 | DependencyPropertyGenerator | Error | +WCTDPG0024 | DependencyPropertyGenerator | Warning | +WCTDPG0025 | DependencyPropertyGenerator | Warning | +WCTDPG0026 | DependencyPropertyGenerator | Warning | +WCTDPG0027 | DependencyPropertyGenerator | Warning | +WCTDPG0028 | DependencyPropertyGenerator | Warning | +WCTDPG0029 | DependencyPropertyGenerator | Warning | +WCTDPG0030 | DependencyPropertyGenerator | Warning | +WCTDPG0031 | DependencyPropertyGenerator | Warning | +WCTDPG0032 | DependencyPropertyGenerator | Warning | diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index c19bfb234..e35bd53b8 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -19,23 +19,23 @@ internal static class DiagnosticDescriptors /// /// The diagnostic id for . /// - public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDP0017"; + public const string UseGeneratedDependencyPropertyForManualPropertyId = "WCTDPG0017"; /// /// The diagnostic id for . /// - public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDP0020"; + public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDPG0020"; /// /// The diagnostic id for . /// - public const string DependencyPropertyFieldDeclarationId = "WCTDP0021"; + public const string DependencyPropertyFieldDeclarationId = "WCTDPG0021"; /// /// "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclaration = new( - id: "WCTDP0001", + id: "WCTDPG0001", title: "Invalid property declaration for [GeneratedDependencyProperty]", messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its declaration is not valid (it must be an instance (non static) partial property, with a getter and a setter that is not init-only)", category: DiagnosticCategory, @@ -48,7 +48,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationIsNotIncompletePartialDefinition = new( - id: "WCTDP0002", + id: "WCTDPG0002", title: "Using [GeneratedDependencyProperty] on an invalid partial property (not incomplete partial definition)", messageFormat: """The property '{0}' is not an incomplete partial definition ([ObservableProperty] must be used on a partial property definition with no implementation part)""", category: DiagnosticCategory, @@ -61,7 +61,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsByRef = new( - id: "WCTDP0003", + id: "WCTDPG0003", title: "Using [GeneratedDependencyProperty] on a property that returns byref", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a ref value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", category: DiagnosticCategory, @@ -74,7 +74,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsRefLikeType = new( - id: "WCTDP0004", + id: "WCTDPG0004", title: "Using [GeneratedDependencyProperty] on a property that returns byref-like", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a byref-like value ([GeneratedDependencyProperty] must be used on properties returning a non byref-like type by value)""", category: DiagnosticCategory, @@ -87,7 +87,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationContainingTypeIsNotDependencyObject = new( - id: "WCTDP0005", + id: "WCTDPG0005", title: "Using [GeneratedDependencyProperty] on a property with invalid containing type", messageFormat: "The property '{0}' cannot be used to generate a dependency property, as its containing type doesn't inherit from DependencyObject", category: DiagnosticCategory, @@ -100,7 +100,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 13 or greater (add 13.0 to your .csproj/.props file). /// public static readonly DiagnosticDescriptor PropertyDeclarationRequiresCSharp13 = new( - id: "WCTDP0006", + id: "WCTDPG0006", title: "Using [GeneratedDependencyProperty] requires C# 13", messageFormat: "The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 13 or greater (add 13.0 to your .csproj/.props file)", category: typeof(UnsupportedCSharpLanguageVersionAnalyzer).FullName, @@ -113,7 +113,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file). /// public static readonly DiagnosticDescriptor LocalCachingRequiresCSharpPreview = new( - id: "WCTDP0007", + id: "WCTDPG0007", title: "Using [GeneratedDependencyProperty] with 'IsLocalCachingEnabled' requires C# 'preview'", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as the project is not using C# 'preview', which is required when using the 'IsLocalCachingEnabled' option (add preview to your .csproj/.props file)""", category: DiagnosticCategory, @@ -126,7 +126,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs'). /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationWouldCauseConflicts = new( - id: "WCTDP0008", + id: "WCTDPG0008", title: "Conflicting property declaration for [GeneratedDependencyProperty]", messageFormat: "The property '{0}' cannot be used to generate an dependency property, as its name or type would cause conflicts with other generated members ([GeneratedDependencyProperty] must not be used on properties named 'Property' of type either 'object' or 'DependencyPropertyChangedEventArgs')", category: DiagnosticCategory, @@ -139,7 +139,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable). /// public static readonly DiagnosticDescriptor NonNullablePropertyDeclarationIsNotEnforced = new( - id: "WCTDP0009", + id: "WCTDPG0009", title: "Non-nullable dependency property is not guaranteed to not be null", messageFormat: "The property '{0}' is not annotated as nullable, but it might contain a null value upon exiting the constructor (consider adding the 'required' modifier, setting a non-null default value if possible, or declaring the property as nullable)", category: DiagnosticCategory, @@ -152,7 +152,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueNull = new( - id: "WCTDP0010", + id: "WCTDPG0010", title: "Invalid 'null' default value for [GeneratedDependencyProperty] use", messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to 'null', which is not compatible (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", category: DiagnosticCategory, @@ -165,7 +165,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'On{4}Get(ref object)' partial method to handle the type mismatch). /// public static readonly DiagnosticDescriptor InvalidPropertyDefaultValueType = new( - id: "WCTDP0011", + id: "WCTDPG0011", title: "Invalid default value type for [GeneratedDependencyProperty] use", messageFormat: "The property '{0}' is declared with type '{1}', but 'DefaultValue' is set to value '{2}' (type '{3}'), which is not compatible (consider fixing the default value, or implementing the 'On{4}Get(ref object)' partial method to handle the type mismatch)", category: DiagnosticCategory, @@ -178,7 +178,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationReturnsPointerType = new( - id: "WCTDP0012", + id: "WCTDPG0012", title: "Using [GeneratedDependencyProperty] on a property that returns pointer type", messageFormat: """The property '{0}' cannot be used to generate a dependency property, as it returns a pointer value ([GeneratedDependencyProperty] must be used on properties returning a non pointer value)""", category: DiagnosticCategory, @@ -191,7 +191,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackMixed = new( - id: "WCTDP0013", + id: "WCTDPG0013", title: "Using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback'", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with both 'DefaultValue' and 'DefaultValueCallback' and being set, which is not supported (only one of these properties can be set at a time)""", category: DiagnosticCategory, @@ -204,7 +204,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackNoMethodFound = new( - id: "WCTDP0014", + id: "WCTDPG0014", title: "Using [GeneratedDependencyProperty] with missing 'DefaultValueCallback' method", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but no accessible method with that name was found (make sure the target method is in the same containing type)""", category: DiagnosticCategory, @@ -217,7 +217,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')". /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationDefaultValueCallbackInvalidMethod = new( - id: "WCTDP0015", + id: "WCTDPG0015", title: "Using [GeneratedDependencyProperty] with invalid 'DefaultValueCallback' method", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] with 'DefaultValueCallback' set to '{1}', but the method has an invalid signature (it must be a static method with no parameters, returning a value compatible with the property type: either the same type, or 'object')""", category: DiagnosticCategory, @@ -230,7 +230,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)". /// public static readonly DiagnosticDescriptor PropertyDeclarationWithPropertySuffix = new( - id: "WCTDP0016", + id: "WCTDPG0016", title: "Using [GeneratedDependencyProperty] on a property with the 'Property' suffix", messageFormat: """The property '{0}' is using [GeneratedDependencyProperty] and has a name ending with the 'Property' suffix, which is redundant (the generated dependency property will always add the 'Property' suffix to the name of its associated property)""", category: DiagnosticCategory, @@ -256,7 +256,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)". /// public static readonly DiagnosticDescriptor InvalidDependencyPropertyTargetedAttributeType = new( - id: "WCTDP0018", + id: "WCTDPG0018", title: "Invalid dependency property targeted attribute type", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' which was not recognized as a valid type (are you missing a using directive?)", category: DiagnosticCategory, @@ -269,7 +269,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)". /// public static readonly DiagnosticDescriptor InvalidDependencyPropertyTargetedAttributeTypeArgumentExpression = new( - id: "WCTDP0019", + id: "WCTDPG0019", title: "Invalid dependency property targeted attribute expression", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is using attribute '{1}' with an invalid expression (are you passing any incorrect parameters to the attribute constructor?)", category: DiagnosticCategory, @@ -308,7 +308,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)". /// public static readonly DiagnosticDescriptor UnnecessaryDependencyPropertyExplicitMetadataType = new( - id: "WCTDP0022", + id: "WCTDPG0022", title: "Unnecessary dependency property explicit metadata type", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is unnecessary (the type is the same as the declared property type)", category: DiagnosticCategory, @@ -321,7 +321,7 @@ internal static class DiagnosticDescriptors /// "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)". /// public static readonly DiagnosticDescriptor IncompatibleDependencyPropertyExplicitMetadataType = new( - id: "WCTDP0023", + id: "WCTDPG0023", title: "Incompatible dependency property explicit metadata type", messageFormat: "The property '{0}' annotated with [GeneratedDependencyProperty] is specifying '{1}' as its property type in metadata, which is not compatible with its declared type '{2}' (the 'PropertyType' option should be used with a compatible type)", category: DiagnosticCategory, @@ -334,7 +334,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On{1}Get' or 'On{1}Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property). /// public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNotNullablePropertyDeclaration = new( - id: "WCTDP0024", + id: "WCTDPG0024", title: "Non-nullable dependency property using [AllowNull] incorrectly", messageFormat: "The property '{0}' is not annotated as nullable and is using [AllowNull], but neither of its accessors are null-resilient (at least one generated 'On{1}Get' or 'On{1}Set' method must be implemented with [NotNull] on the 'propertyValue' parameter, to ensure assigning null values does not break the nullable annotations on the property)", category: DiagnosticCategory, @@ -347,7 +347,7 @@ internal static class DiagnosticDescriptors /// The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On{1}Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On{1}Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null). /// public static readonly DiagnosticDescriptor NotNullResilientAccessorsForNullablePropertyDeclaration = new( - id: "WCTDP0025", + id: "WCTDPG0025", title: "Nullable dependency property using [NotNull] incorrectly", messageFormat: "The property '{0}' is annotated as nullable and is using [NotNull], but it's not guaranteeing that returned values will not be null (it must either make its 'get' accessor null-resilient, by implementing at least one generated 'On{1}Get' method with [NotNull] on the 'propertyValue' parameter, or it must either add [DisallowNull] or implement at least one generated 'On{1}Set' method with [NotNull], and also either mark the property as required, or ensure that its default value is not null)", category: DiagnosticCategory, @@ -360,7 +360,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but its name is not following the recommended naming convention (all dependency property fields should use the 'Property' suffix in their names). /// public static readonly DiagnosticDescriptor NoPropertySuffixOnDependencyPropertyField = new( - id: "WCTDP0026", + id: "WCTDPG0026", title: "No 'Property' suffix on dependency property field name", messageFormat: "The field '{0}' is registering a dependency property, but its name is not following the recommended naming convention (all dependency property fields should use the 'Property' suffix in their names)", category: DiagnosticCategory, @@ -373,7 +373,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata is not following the recommended naming convention (all property names should match the name of their declared fields, minus the 'Property' suffix). /// public static readonly DiagnosticDescriptor InvalidPropertyNameOnDependencyPropertyField = new( - id: "WCTDP0027", + id: "WCTDPG0027", title: "Invalid property name in dependency property field metadata", messageFormat: "The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata is not following the recommended naming convention (all property names should match the name of their declared fields, minus the 'Property' suffix)", category: DiagnosticCategory, @@ -386,7 +386,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata does not match the property name '{2}' wrapping the dependency property (all property names should match the name of the wrapping properties of each dependency property field). /// public static readonly DiagnosticDescriptor MismatchedPropertyNameOnDependencyPropertyField = new( - id: "WCTDP0028", + id: "WCTDPG0028", title: "Mismatched property name in dependency property field metadata", messageFormat: "The field '{0}' is registering a dependency property, but the property name '{1}' declared in metadata does not match the property name '{2}' wrapping the dependency property (all property names should match the name of the wrapping properties of each dependency property field)", category: DiagnosticCategory, @@ -399,7 +399,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but its owning type '{1}' declared in metadata does not match the containing type '{2}' for the field (the owning type of a dependency property should always match the containing type of the field declaration). /// public static readonly DiagnosticDescriptor InvalidOwningTypeOnDependencyPropertyField = new( - id: "WCTDP0029", + id: "WCTDPG0029", title: "Invalid owning type in dependency property field metadata", messageFormat: "The field '{0}' is registering a dependency property, but its owning type '{1}' declared in metadata does not match the containing type '{2}' for the field (the owning type of a dependency property should always match the containing type of the field declaration)", category: DiagnosticCategory, @@ -412,7 +412,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but its property type '{1}' does not match the type '{2}' of the wrapping property '{3}', and there is no valid type conversion between the two (all property types should either match the type of the wrapping properties for each dependency property field, or be of a valid assignable type from the type of each wrapping property). /// public static readonly DiagnosticDescriptor InvalidPropertyTypeOnDependencyPropertyField = new( - id: "WCTDP0030", + id: "WCTDPG0030", title: "Invalid property type in dependency property field metadata", messageFormat: "The field '{0}' is registering a dependency property, but its property type '{1}' does not match the type '{2}' of the wrapping property '{3}', and there is no valid type conversion between the two (all property types should either match the type of the wrapping properties for each dependency property field, or be of a valid assignable type from the type of each wrapping property)", category: DiagnosticCategory, @@ -425,7 +425,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidDefaultValueNullOnDependencyPropertyField = new( - id: "WCTDP0031", + id: "WCTDPG0031", title: "Invalid 'null' default value in dependency property field metadata", messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", category: DiagnosticCategory, @@ -438,7 +438,7 @@ internal static class DiagnosticDescriptors /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'On{3}Get(ref object)' partial method to handle the type mismatch). /// public static readonly DiagnosticDescriptor InvalidDefaultValueTypeOnDependencyPropertyField = new( - id: "WCTDP0032", + id: "WCTDPG0032", title: "Invalid default value type in dependency property field metadata", messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'On{3}Get(ref object)' partial method to handle the type mismatch)", category: DiagnosticCategory, diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index afc365c6e..5edf3dd4d 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -60,7 +60,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0001:GeneratedDependencyProperty|}] + [{|WCTDPG0001:GeneratedDependencyProperty|}] public string? Name { get; set; } } """; @@ -79,7 +79,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0001:GeneratedDependencyProperty|}] + [{|WCTDPG0001:GeneratedDependencyProperty|}] public partial string? {|CS9248:Name|} { get; } } """; @@ -98,7 +98,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0001:GeneratedDependencyProperty|}] + [{|WCTDPG0001:GeneratedDependencyProperty|}] public partial string? {|CS9248:Name|} { set; } } """; @@ -117,7 +117,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0001:GeneratedDependencyProperty|}] + [{|WCTDPG0001:GeneratedDependencyProperty|}] public partial string? {|CS9248:Name|} { get; init; } } """; @@ -136,7 +136,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0001:GeneratedDependencyProperty|}] + [{|WCTDPG0001:GeneratedDependencyProperty|}] public static partial string? {|CS9248:Name|} { get; set; } } """; @@ -242,7 +242,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0002:GeneratedDependencyProperty|}] + [{|WCTDPG0002:GeneratedDependencyProperty|}] public partial string Name { get; set; } [GeneratedCode("Some.Other.Generator", "1.0.0")] @@ -268,7 +268,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0002:GeneratedDependencyProperty|}] + [{|WCTDPG0002:GeneratedDependencyProperty|}] public partial string Name { get; set; } public partial string Name @@ -293,7 +293,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0003:GeneratedDependencyProperty|}] + [{|WCTDPG0003:GeneratedDependencyProperty|}] public partial ref int {|CS9248:Name|} { get; {|CS8147:set|}; } } """; @@ -312,7 +312,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0003:GeneratedDependencyProperty|}] + [{|WCTDPG0003:GeneratedDependencyProperty|}] public partial ref readonly int {|CS9248:Name|} { get; {|CS8147:set|}; } } """; @@ -332,7 +332,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0004:GeneratedDependencyProperty|}] + [{|WCTDPG0004:GeneratedDependencyProperty|}] public partial Span {|CS9248:Name|} { get; set; } } """; @@ -351,7 +351,7 @@ namespace MyApp; public unsafe partial class MyControl : Control { - [{|WCTDP0012:GeneratedDependencyProperty|}] + [{|WCTDPG0012:GeneratedDependencyProperty|}] public partial int* {|CS9248:Name|} { get; set; } } """; @@ -370,7 +370,7 @@ namespace MyApp; public unsafe partial class MyControl : Control { - [{|WCTDP0012:GeneratedDependencyProperty|}] + [{|WCTDPG0012:GeneratedDependencyProperty|}] public partial delegate* unmanaged[Stdcall] {|CS9248:Name|} { get; set; } } """; @@ -444,7 +444,7 @@ namespace MyApp; public partial class MyControl { - [{|WCTDP0005:GeneratedDependencyProperty|}] + [{|WCTDPG0005:GeneratedDependencyProperty|}] public partial string? {|CS9248:Name|} { get; set; } } """; @@ -504,7 +504,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0006:GeneratedDependencyProperty|}] + [{|WCTDPG0006:GeneratedDependencyProperty|}] public string? Name { get; set; } } """; @@ -525,7 +525,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0007:GeneratedDependencyProperty(IsLocalCacheEnabled = true)|}] + [{|WCTDPG0007:GeneratedDependencyProperty(IsLocalCacheEnabled = true)|}] public string? Name { get; set; } } """; @@ -606,7 +606,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0008:GeneratedDependencyProperty|}] + [{|WCTDPG0008:GeneratedDependencyProperty|}] public partial {{propertyType}} {|CS9248:Property|} { get; set; } } """; @@ -826,7 +826,7 @@ namespace MyApp; public partial class MyControl : Control { [GeneratedDependencyProperty] - public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } } """; @@ -847,7 +847,7 @@ namespace MyApp; public partial class MyControl : Control { [GeneratedDependencyProperty(DefaultValue = null)] - public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } } """; @@ -868,7 +868,7 @@ namespace MyApp; public partial class MyControl : Control { [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] - public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } private static string? GetDefaultName() => "Bob"; } @@ -892,7 +892,7 @@ namespace MyApp; public partial class MyControl : Control { [GeneratedDependencyProperty(DefaultValueCallback = nameof(GetDefaultName))] - public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } [return: MaybeNull] private static string GetDefaultName() => "Bob"; @@ -918,7 +918,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [AllowNull] - public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}([NotNull] ref object? propertyValue) { @@ -974,7 +974,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [AllowNull] - public partial string {|WCTDP0009:{|CS9248:Name|}|} { get; set; } + public partial string {|WCTDPG0009:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) { @@ -1030,7 +1030,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [AllowNull] - public partial string {|WCTDP0009:{|WCTDP0024:{|CS9248:Name|}|}|} { get; set; } + public partial string {|WCTDPG0009:{|WCTDPG0024:{|CS9248:Name|}|}|} { get; set; } partial void {|CS0759:OnNameGet|}(ref string? propertyValue) { @@ -1057,7 +1057,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [AllowNull] - public partial string {|WCTDP0009:{|WCTDP0024:{|CS9248:Name|}|}|} { get; set; } + public partial string {|WCTDPG0009:{|WCTDPG0024:{|CS9248:Name|}|}|} { get; set; } } """; @@ -1359,7 +1359,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [NotNull] - public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } } """; @@ -1382,7 +1382,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [NotNull] - public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}(ref string? propertyValue) { @@ -1411,7 +1411,7 @@ public partial class MyControl : Control [GeneratedDependencyProperty] [NotNull] [DisallowNull] - public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } } """; @@ -1434,7 +1434,7 @@ public partial class MyControl : Control { [GeneratedDependencyProperty] [NotNull] - public partial string? {|WCTDP0025:{|CS9248:Name|}|} { get; set; } + public partial string? {|WCTDPG0025:{|CS9248:Name|}|} { get; set; } partial void {|CS0759:OnNameSet|}([NotNull] ref string? propertyValue) { @@ -1463,7 +1463,7 @@ public abstract partial class Animation : DependencyObject { [GeneratedDependencyProperty] [NotNull] - public partial KeyFrameCollection? {|WCTDP0025:{|CS9248:KeyFrames|}|} { get; set; } + public partial KeyFrameCollection? {|WCTDPG0025:{|CS9248:KeyFrames|}|} { get; set; } partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) { @@ -1498,7 +1498,7 @@ public abstract partial class Animation : DependencyObject { [GeneratedDependencyProperty] [AllowNull] - public partial KeyFrameCollection {|WCTDP0009:{|WCTDP0024:{|CS9248:KeyFrames|}|}|} { get; set; } + public partial KeyFrameCollection {|WCTDPG0009:{|WCTDPG0024:{|CS9248:KeyFrames|}|}|} { get; set; } partial void {|CS0759:OnKeyFramesGet|}(ref KeyFrameCollection? propertyValue) { @@ -1533,7 +1533,7 @@ public abstract partial class Animation : DependencyObject { [GeneratedDependencyProperty] [AllowNull] - public required partial KeyFrameCollection {|WCTDP0024:{|CS9248:KeyFrames|}|} { get; set; } + public required partial KeyFrameCollection {|WCTDPG0024:{|CS9248:KeyFrames|}|} { get; set; } } public sealed partial class KeyFrameCollection : DependencyObjectCollection @@ -1564,7 +1564,7 @@ public partial class MyObject : DependencyObject where T4 : unmanaged { [GeneratedDependencyProperty] - public partial {{declaredType}} {|WCTDP0009:{|CS9248:Value|}|} { get; set; } + public partial {{declaredType}} {|WCTDPG0009:{|CS9248:Value|}|} { get; set; } } """; @@ -1724,7 +1724,7 @@ namespace MyApp; public partial class MyControl : Control { - [GeneratedDependencyProperty({|WCTDP0010:DefaultValue = null|})] + [GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})] public partial int {|CS9248:Name|} { get; set; } } """; @@ -1757,7 +1757,7 @@ public partial class MyObject : DependencyObject where T4 : unmanaged where T5 : IDisposable { - [GeneratedDependencyProperty({|WCTDP0010:DefaultValue = null|})] + [GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})] public partial {{propertyType}} {|CS9248:Name|} { get; set; } } """; @@ -1782,7 +1782,7 @@ namespace MyApp; public partial class MyControl : Control { - [GeneratedDependencyProperty({|WCTDP0011:DefaultValue = {{defaultValueType}}|})] + [GeneratedDependencyProperty({|WCTDPG0011:DefaultValue = {{defaultValueType}}|})] public partial {{propertyType}} {|CS9248:Name|} { get; set; } } """; @@ -1921,7 +1921,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0013:GeneratedDependencyProperty(DefaultValue = "Bob", DefaultValueCallback = nameof(GetDefaultName))|}] + [{|WCTDPG0013:GeneratedDependencyProperty(DefaultValue = "Bob", DefaultValueCallback = nameof(GetDefaultName))|}] public partial string? {|CS9248:Name|} { get; set; } private static string? GetDefaultName() => "Bob"; @@ -1944,7 +1944,7 @@ namespace MyApp; public partial class MyControl : Control { - [GeneratedDependencyProperty({|WCTDP0014:DefaultValueCallback = "MissingMethod"|})] + [GeneratedDependencyProperty({|WCTDPG0014:DefaultValueCallback = "MissingMethod"|})] public partial string? {|CS9248:Name|} { get; set; } } """; @@ -1965,7 +1965,7 @@ namespace MyApp; public partial class MyControl : Control, IGetDefaultValue { - [GeneratedDependencyProperty({|WCTDP0014:DefaultValueCallback = "GetDefaultValue"|})] + [GeneratedDependencyProperty({|WCTDPG0014:DefaultValueCallback = "GetDefaultValue"|})] public partial string? {|CS9248:Name|} { get; set; } static string? IGetDefaultValue.GetDefaultValue() => "Bob"; @@ -1997,7 +1997,7 @@ namespace MyApp; public partial class MyControl : Control { - [GeneratedDependencyProperty({|WCTDP0015:DefaultValueCallback = "GetDefaultName"|})] + [GeneratedDependencyProperty({|WCTDPG0015:DefaultValueCallback = "GetDefaultName"|})] public partial string? {|CS9248:Name|} { get; set; } {{methodSignature}} => default!; @@ -2064,7 +2064,7 @@ namespace MyApp; public partial class MyControl : Control { - [{|WCTDP0016:GeneratedDependencyProperty|}] + [{|WCTDPG0016:GeneratedDependencyProperty|}] public partial string? {|CS9248:TestProperty|} { get; set; } } """; @@ -2112,7 +2112,7 @@ namespace MyApp; public partial class MyControl : Control { public static readonly DependencyProperty OtherNameProperty = DependencyProperty.Register( - {|WCTDP0027:name: "Name"|}, + {|WCTDPG0027:name: "Name"|}, propertyType: typeof(string), ownerType: typeof(MyControl), typeMetadata: null); @@ -2129,7 +2129,7 @@ public string? Name } [TestMethod] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_WCTDP0026_DoesNotWarn() + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_WCTDPG0026_DoesNotWarn() { const string source = """ using Windows.UI.Xaml; @@ -2141,7 +2141,7 @@ namespace MyApp; public partial class MyControl : Control { - public static readonly DependencyProperty {|WCTDP0026:NameField|} = DependencyProperty.Register( + public static readonly DependencyProperty {|WCTDPG0026:NameField|} = DependencyProperty.Register( name: "Name", propertyType: typeof(string), ownerType: typeof(MyControl), @@ -2204,7 +2204,7 @@ public string? Name [DataRow("MyControl", "Control")] [DataRow("MyControl", "DependencyObject")] [DataRow("Control", "DependencyObject")] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_DoesNotWarn( + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_DoesNotWarn( string dependencyPropertyType, string propertyType) { @@ -2219,7 +2219,7 @@ public partial class MyControl : Control { public static readonly DependencyProperty NameProperty = DependencyProperty.Register( name: "Name", - {|WCTDP0030:propertyType: typeof({{dependencyPropertyType}})|}, + {|WCTDPG0030:propertyType: typeof({{dependencyPropertyType}})|}, ownerType: typeof(MyControl), typeMetadata: null); @@ -2236,7 +2236,7 @@ public partial class MyControl : Control // Regression test for a case found in the Microsoft Store [TestMethod] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithObjectInitialization_DoesNotWarn() + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithObjectInitialization_DoesNotWarn() { const string source = """ using Windows.UI.Xaml; @@ -2248,9 +2248,9 @@ public partial class MyControl : Control { public static readonly DependencyProperty MarginProperty = DependencyProperty.Register( nameof(Margin), - {|WCTDP0030:typeof(double)|}, + {|WCTDPG0030:typeof(double)|}, typeof(MyControl), - new PropertyMetadata({|WCTDP0032:new Thickness(0)|})); + new PropertyMetadata({|WCTDPG0032:new Thickness(0)|})); private Thickness Margin { @@ -2264,7 +2264,7 @@ private Thickness Margin } [TestMethod] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithExplicitDefaultValueNull_DoesNotWarn() + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithExplicitDefaultValueNull_DoesNotWarn() { const string source = """ using Windows.UI.Xaml; @@ -2276,9 +2276,9 @@ public partial class MyControl : Control { public static readonly DependencyProperty MarginProperty = DependencyProperty.Register( nameof(Margin), - {|WCTDP0030:typeof(double)|}, + {|WCTDPG0030:typeof(double)|}, typeof(MyControl), - new PropertyMetadata({|WCTDP0031:null|})); + new PropertyMetadata({|WCTDPG0031:null|})); private Thickness Margin { @@ -2293,7 +2293,7 @@ private Thickness Margin // Regression test for a case found in https://github.com/jenius-apps/ambie [TestMethod] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0030_WithInvalidPropertyName_DoesNotWarn() + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithInvalidPropertyName_DoesNotWarn() { string source = $$""" using Windows.UI.Xaml; @@ -2304,16 +2304,16 @@ namespace AmbientSounds.Controls; public sealed partial class PlayerControl : UserControl { public static readonly DependencyProperty PlayVisibleProperty = DependencyProperty.Register( - {|WCTDP0027:nameof(PlayButtonVisible)|}, - {|WCTDP0030:typeof(bool)|}, + {|WCTDPG0027:nameof(PlayButtonVisible)|}, + {|WCTDPG0030:typeof(bool)|}, typeof(PlayerControl), - new PropertyMetadata({|WCTDP0032:Visibility.Visible|})); + new PropertyMetadata({|WCTDPG0032:Visibility.Visible|})); public static readonly DependencyProperty VolumeVisibleProperty = DependencyProperty.Register( nameof(VolumeVisible), - {|WCTDP0030:typeof(bool)|}, + {|WCTDPG0030:typeof(bool)|}, typeof(PlayerControl), - new PropertyMetadata({|WCTDP0032:Visibility.Visible|})); + new PropertyMetadata({|WCTDPG0032:Visibility.Visible|})); public Visibility PlayButtonVisible { @@ -2336,7 +2336,7 @@ public Visibility VolumeVisible [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0029_DoesNotWarn( + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0029_DoesNotWarn( string name, string propertyType, string ownerType, @@ -2355,7 +2355,7 @@ public partial class MyControl : Control public static readonly DependencyProperty NameProperty = DependencyProperty.Register( name: {{name}}, propertyType: {{propertyType}}, - {|WCTDP0029:ownerType: {{ownerType}}|}, + {|WCTDPG0029:ownerType: {{ownerType}}|}, typeMetadata: {{typeMetadata}}); public string? Name @@ -2372,7 +2372,7 @@ public string? Name [TestMethod] [DataRow("\"NameProperty\"", "typeof(string)", "typeof(MyControl)", "null")] [DataRow("\"OtherName\"", "typeof(string)", "typeof(MyControl)", "null")] - public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDP0027_WCTDP0028_DoesNotWarn( + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0027_WCTDPG0028_DoesNotWarn( string name, string propertyType, string ownerType, @@ -2389,7 +2389,7 @@ namespace MyApp; public partial class MyControl : Control { public static readonly DependencyProperty NameProperty = DependencyProperty.Register( - {|WCTDP0027:{|WCTDP0028:name: {{name}}|}|}, + {|WCTDPG0027:{|WCTDPG0028:name: {{name}}|}|}, propertyType: {{propertyType}}, ownerType: {{ownerType}}, typeMetadata: {{typeMetadata}}); @@ -2553,7 +2553,7 @@ public partial class MyControl : Control await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } - // Same as above, but this one also produces some WCTDP0030 warnings, so we need to split these cases out + // Same as above, but this one also produces some WCTDPG0030 warnings, so we need to split these cases out [TestMethod] [DataRow("T1", "object", "new PropertyMetadata(default(T1))")] [DataRow("T1?", "object", "new PropertyMetadata(default(T1))")] @@ -2579,7 +2579,7 @@ public partial class MyControl : Control { public static readonly DependencyProperty NameProperty = DependencyProperty.Register( name: "Name", - {|WCTDP0030:propertyType: typeof({{dependencyPropertyType}})|}, + {|WCTDPG0030:propertyType: typeof({{dependencyPropertyType}})|}, ownerType: typeof(MyControl), typeMetadata: {{propertyMetadataExpression}}); @@ -2670,7 +2670,7 @@ public partial class MyControl : Control ownerType: typeof(MyControl), typeMetadata: null); - public {{propertyType}} {|WCTDP0017:Name|} + public {{propertyType}} {|WCTDPG0017:Name|} { get => ({{propertyType}})GetValue(NameProperty); set => SetValue(NameProperty, value); @@ -2708,7 +2708,7 @@ public partial class MyControl : Control ownerType: typeof(MyControl), typeMetadata: null); - public string? {|WCTDP0017:Name|} + public string? {|WCTDPG0017:Name|} { get => (string?)GetValue(NameProperty); set => SetValue(NameProperty, value); @@ -2804,7 +2804,7 @@ public partial class MyControl : Control ownerType: typeof(MyControl), typeMetadata: new PropertyMetadata({{defaultValueExpression}})); - public {{propertyType}} {|WCTDP0017:Name|} + public {{propertyType}} {|WCTDPG0017:Name|} { get => ({{propertyType}})GetValue(NameProperty); set => SetValue(NameProperty, value); @@ -2857,7 +2857,7 @@ public partial class MyControl : Control ownerType: typeof(MyControl), typeMetadata: {{propertyMetadataExpression}}); - public {{propertyType}} {|WCTDP0017:Name|} + public {{propertyType}} {|WCTDPG0017:Name|} { get => ({{propertyType}})GetValue(NameProperty); set => SetValue(NameProperty, value); @@ -2891,7 +2891,7 @@ public partial class MyControl : Control ownerType: typeof(MyControl), typeMetadata: null); - public {{declaredType}} {|WCTDP0017:Name|} + public {{declaredType}} {|WCTDPG0017:Name|} { get => ({{declaredType}})GetValue(NameProperty); set => SetValue(NameProperty, value); @@ -2933,7 +2933,7 @@ namespace MyApp; public class MyObject : DependencyObject { - public static readonly DependencyProperty {|WCTDP0026:NameField|} = DependencyProperty.Register( + public static readonly DependencyProperty {|WCTDPG0026:NameField|} = DependencyProperty.Register( name: "Name", propertyType: typeof(string), ownerType: typeof(MyObject), @@ -2955,7 +2955,7 @@ namespace MyApp; public class MyObject : DependencyObject { public static readonly DependencyProperty NameProperty = DependencyProperty.Register( - {|WCTDP0027:name: "Text"|}, + {|WCTDPG0027:name: "Text"|}, propertyType: typeof(string), ownerType: typeof(MyObject), typeMetadata: null); @@ -2978,7 +2978,7 @@ public class MyObject : DependencyObject public static readonly DependencyProperty NameProperty = DependencyProperty.Register( name: "Name", propertyType: typeof(string), - {|WCTDP0029:ownerType: typeof(MyOtherObject)|}, + {|WCTDPG0029:ownerType: typeof(MyOtherObject)|}, typeMetadata: null); } @@ -3042,7 +3042,7 @@ public class MyObject : DependencyObject name: "Name", propertyType: typeof({{propertyType}}), typeof(MyObject), - typeMetadata: new PropertyMetadata({|WCTDP0031:null|})); + typeMetadata: new PropertyMetadata({|WCTDPG0031:null|})); } """; @@ -3071,7 +3071,7 @@ public class MyObject : DependencyObject name: "Name", propertyType: typeof({{propertyType}}), typeof(MyObject), - typeMetadata: new PropertyMetadata({|WCTDP0032:{{defaultValue}}|})); + typeMetadata: new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); } """; @@ -3106,7 +3106,7 @@ public partial class MyControl : Control ownerType: typeof(MyControl), typeMetadata: null); - public {{propertyType}} {|WCTDP0017:Name|} + public {{propertyType}} {|WCTDPG0017:Name|} { get => ({{propertyType}})GetValue(NameProperty); set => SetValue(NameProperty, value); @@ -3228,7 +3228,7 @@ namespace MyApp public class MyControl : Control { [GeneratedDependencyProperty] - [static: {|WCTDP0018:Test|}] + [static: {|WCTDPG0018:Test|}] public string? Name { get; set; } } } @@ -3254,7 +3254,7 @@ public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_TypoInAtt public class MyControl : Control { [GeneratedDependencyProperty] - [static: {|WCTDP0018:Testt|}] + [static: {|WCTDPG0018:Testt|}] public string? Name { get; set; } } @@ -3277,7 +3277,7 @@ public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_InvalidEx public class MyControl : Control { [GeneratedDependencyProperty] - [static: {|WCTDP0019:Test(TestAttribute.M)|}] + [static: {|WCTDPG0019:Test(TestAttribute.M)|}] public string? Name { get; set; } } @@ -3302,7 +3302,7 @@ public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_InvalidEx public class MyControl : Control { [GeneratedDependencyProperty] - [static: {|WCTDP0019:Test(TestAttribute.M)|}] + [static: {|WCTDPG0019:Test(TestAttribute.M)|}] public string? Name { get; set; } } @@ -3359,7 +3359,7 @@ public async Task UseFieldDeclarationCorrectlyAnalyzer_Warns(string fieldDeclara public class MyObject : DependencyObject { - {{fieldDeclaration}} {|WCTDP0020:TestProperty|}; + {{fieldDeclaration}} {|WCTDPG0020:TestProperty|}; } """; @@ -3485,9 +3485,9 @@ public async Task UseFieldDeclarationAnalyzer_NormalProperty_Warns() public class MyObject : DependencyObject { - public static DependencyProperty {|WCTDP0021:Test1Property|} => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); - public static DependencyProperty {|WCTDP0021:Test2Property|} { get; } = DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); - public static DependencyProperty {|WCTDP0021:Test3Property|} { get; set; } + public static DependencyProperty {|WCTDPG0021:Test1Property|} => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null); + public static DependencyProperty {|WCTDPG0021:Test2Property|} { get; } = DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null); + public static DependencyProperty {|WCTDPG0021:Test3Property|} { get; set; } } """; @@ -3549,7 +3549,7 @@ public async Task ExplicitPropertyMetadataTypeAnalyzer_SameType_Warns(string typ public class MyObject : DependencyObject, IMyInterface { - [GeneratedDependencyProperty({|WCTDP0022:PropertyType = typeof({{type}})|})] + [GeneratedDependencyProperty({|WCTDPG0022:PropertyType = typeof({{type}})|})] public {{type}} Name { get; set; } } @@ -3575,7 +3575,7 @@ public async Task ExplicitPropertyMetadataTypeAnalyzer_IncompatibleType_Warns(st public class MyObject : DependencyObject { - [GeneratedDependencyProperty({|WCTDP0023:PropertyType = typeof({{propertyType}})|})] + [GeneratedDependencyProperty({|WCTDPG0023:PropertyType = typeof({{propertyType}})|})] public {{declaredType}} Name { get; set; } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs index 10aaa50b4..fc7960744 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseFieldDeclarationCodeFixer.cs @@ -189,7 +189,7 @@ public class TestAttribute : Attribute; test.FixedState.ExpectedDiagnostics.AddRange( [ - // /0/Test0.cs(29,38): warning WCTDP0021: The property 'MyApp.MyObject.Test5Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) + // /0/Test0.cs(29,38): warning WCTDPG0021: The property 'MyApp.MyObject.Test5Property' is a dependency property, which is not the correct declaration type (all dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types) CSharpCodeFixVerifier.Diagnostic().WithSpan(29, 38, 29, 51).WithArguments("MyApp.MyObject.Test5Property") ]); diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index b7c3be135..5f05ca959 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2429,7 +2429,7 @@ public partial class MyObject : DependencyObject nameof(Name), typeof({{propertyType}}), typeof(MyObject), - new PropertyMetadata({|WCTDP0031:null|})); + new PropertyMetadata({|WCTDPG0031:null|})); public {{declaredType}} [|Name|] { @@ -2655,7 +2655,7 @@ public class MyObject : DependencyObject name: "Name", propertyType: typeof({{propertyType}}), ownerType: typeof(MyObject), - typeMetadata: new PropertyMetadata({|WCTDP0031:null|})); + typeMetadata: new PropertyMetadata({|WCTDPG0031:null|})); public {{declaredType}} [|Name|] { From f43c0e457b295315f27a7f599efafefb08ce065e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 5 Jan 2025 18:18:30 +0100 Subject: [PATCH 190/200] Update the embedded attribute type source file --- .../GeneratedDependencyPropertyAttribute.cs | 23 +++++++++++++++++-- .../GeneratedDependencyPropertyAttribute.cs | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs index 0e7c0fafe..16081e777 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/EmbeddedResources/GeneratedDependencyPropertyAttribute.cs @@ -47,7 +47,7 @@ namespace CommunityToolkit.WinUI internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attribute { /// - /// Gets a value indicating the default value to set for the property. + /// Gets or sets a value indicating the default value to set for the property. /// /// /// @@ -77,7 +77,7 @@ internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attr public string? DefaultValueCallback { get; init; } = null!; /// - /// Gets a value indicating whether or not property values should be cached locally, to improve performance. + /// Gets or sets a value indicating whether or not property values should be cached locally, to improve performance. /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. /// /// @@ -85,6 +85,25 @@ internal sealed class GeneratedDependencyPropertyAttribute : global::System.Attr /// properties might also be set outside of the partial property implementation, meaning caching would be invalid. /// public bool IsLocalCacheEnabled { get; init; } = false; + + /// + /// Gets or sets the type to use to register the property in metadata. The default value will exactly match the property type. + /// + /// + /// + /// This property allows customizing the property type in metadata, in advanced scenarios. For instance, it can be used to define + /// properties of a type (e.g. ) as just using in metadata. + /// This allows working around some issues primarily around classic (reflection-based) binding in XAML. + /// + /// + /// This property should only be set when actually required (e.g. to ensure a specific scenario can work). The default behavior + /// (i.e. the property type in metadata matching the declared property type) should work correctly in the vast majority of cases. + /// + /// +#if NET8_0_OR_GREATER + [global::System.Diagnostics.CodeAnalysis.DisallowNull] +#endif + public global::System.Type? PropertyType { get; init; } = null!; } } diff --git a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs index 05bfa86b5..2291e55ed 100644 --- a/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs +++ b/components/DependencyPropertyGenerator/src/GeneratedDependencyPropertyAttribute.cs @@ -71,7 +71,7 @@ public sealed class GeneratedDependencyPropertyAttribute : Attribute public string? DefaultValueCallback { get; init; } = null!; /// - /// Gets a value indicating whether or not property values should be cached locally, to improve performance. + /// Gets or sets a value indicating whether or not property values should be cached locally, to improve performance. /// This allows completely skipping boxing (for value types) and all WinRT marshalling when setting properties. /// /// From c8835d3cac34a0b321bf9b6b8e1c6c8ad385819a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jan 2025 12:27:43 +0100 Subject: [PATCH 191/200] Analyze default values even if callback is present --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 9 +- .../Test_Analyzers.cs | 102 ++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index a66f3e7d9..893f491d1 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -511,10 +511,15 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla return; } - // If we have a second argument, a 'null' literal is the only supported value for it + // If we have a second argument, a 'null' literal is the only supported value for it. If that's not the case, + // we mark the propertry as not valid, but don't stop here. The reason is that even if we do have a callback, + // meaning we cannot enable the code fixer, we still want to analyze the default value argument. if (objectCreationOperation.Arguments is not ([_] or [_, { Value.ConstantValue: { HasValue: true, Value: null } }])) { - return; + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } } bool isDependencyPropertyUnsetValue = false; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 5edf3dd4d..a9d7878ff 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -2332,6 +2332,108 @@ public Visibility VolumeVisible await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Regression test for a case found in the Microsoft Store + [TestMethod] + [DataRow("default(float)")] + [DataRow("1.0F")] + [DataRow("(float)1.0")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithMismatchedNullableUnderlyingType_DoesNotWarn(string defaultValue) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public static readonly DependencyProperty Value1Property = DependencyProperty.Register( + nameof(Value1), + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + + public static readonly DependencyProperty Value2Property = DependencyProperty.Register( + nameof(Value2), + {|WCTDPG0030:typeof(int?)|}, + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + + public static readonly DependencyProperty Value3Property = DependencyProperty.Register( + "Value3", + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|})); + + public int? {|WCTDPG0017:Value1|} + { + get => (int?)GetValue(Value1Property); + set => SetValue(Value1Property, value); + } + + public float Value2 + { + get => (float)GetValue(Value2Property); + set => SetValue(Value2Property, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + // Same as above, but with property changed callbacks too + [TestMethod] + [DataRow("default(float)")] + [DataRow("1.0F")] + [DataRow("(float)1.0")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_WCTDPG0030_WithMismatchedNullableUnderlyingType_WithCallbacks_DoesNotWarn(string defaultValue) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + public static readonly DependencyProperty Value1Property = DependencyProperty.Register( + nameof(Value1), + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|}, ItemSourcePropertyChanged)); + + public static readonly DependencyProperty Value2Property = DependencyProperty.Register( + nameof(Value2), + {|WCTDPG0030:typeof(int?)|}, + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|}, ItemSourcePropertyChanged)); + + public static readonly DependencyProperty Value3Property = DependencyProperty.Register( + "Value3", + typeof(int?), + typeof(MyObject), + new PropertyMetadata({|WCTDPG0032:{{defaultValue}}|}, ItemSourcePropertyChanged)); + + public int? Value1 + { + get => (int?)GetValue(Value1Property); + set => SetValue(Value1Property, value); + } + + public float Value2 + { + get => (float)GetValue(Value2Property); + set => SetValue(Value2Property, value); + } + + private static void ItemSourcePropertyChanged(object sender, DependencyPropertyChangedEventArgs args) + { + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")] [DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")] From f7f3e861f80bc9f0480aa59c2377f65e566f9738 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jan 2025 12:54:25 +0100 Subject: [PATCH 192/200] Analyze more cases even with invalid field declarations --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 16 ++++- .../Test_Analyzers.cs | 58 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 893f491d1..58d816a7b 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -191,16 +191,16 @@ public override void Initialize(AnalysisContext context) } else if (memberSymbol is IFieldSymbol { - DeclaredAccessibility: Accessibility.Public, IsStatic: true, - IsReadOnly: true, IsFixedSizeBuffer: false, IsRequired: false, Type.IsReferenceType: true, IsVolatile: false } fieldSymbol) { - // We only care about fields that are 'DependencyProperty' + // We only care about fields that are 'DependencyProperty'. Note that we should also be checking the declared + // accessibility here and the fact the field is readonly, but we delay that in the initialization callback. + // This is done so that we can still emit more diagnostics when analyzing the symbol completes below. if (!SymbolEqualityComparer.Default.Equals(dependencyPropertySymbol, fieldSymbol.Type)) { continue; @@ -395,6 +395,16 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.FieldSymbol = fieldSymbol; } + // Do the accessibility and readonly check here, to still enable diagnostics at the end in more scenarios. + // If the field is invalid, just mark it as such, but continue executing the rest of the analysis here. + if (fieldSymbol is { DeclaredAccessibility: not Accessibility.Public } or { IsReadOnly: false }) + { + if (fieldFlags is not null) + { + fieldFlags.HasAnyDiagnostics = true; + } + } + // Additional diagnostic #1: the dependency property field name should have the "Property" suffix if (!fieldSymbol.Name.EndsWith("Property")) { diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index a9d7878ff..1cf252227 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -3226,6 +3226,64 @@ public void Dispose() await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("private static readonly")] + [DataRow("public static")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_DoesNotWarn(string fieldDeclaration) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + {{fieldDeclaration}} DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(string), + ownerType: typeof(MyObject), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("private static readonly")] + [DataRow("public static")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidFieldDeclaration_EmitsAdditionalDiagnosticsToo_DoesNotWarn(string fieldDeclaration) + { + string source = $$""" + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + {{fieldDeclaration}} DependencyProperty NameProperty = DependencyProperty.Register( + {|WCTDPG0027:{|WCTDPG0028:name: "Name2"|}|}, + {|WCTDPG0030:propertyType: typeof(int?)|}, + ownerType: typeof(MyObject), + typeMetadata: null); + + public string? Name + { + get => (string?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] public async Task InvalidPropertyForwardedAttributeDeclarationAnalyzer_NoDependencyPropertyAttribute_DoesNotWarn() { From fd9faeafc51d6b5fa71e1946e11bf41db6979275 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jan 2025 17:35:12 +0100 Subject: [PATCH 193/200] Fix nullability checks in type parameter constraints --- .../ITypeParameterSymbolExtensions.cs | 60 ++++++++ .../Extensions/ITypeSymbolExtensions.cs | 2 +- .../Test_Analyzers.cs | 141 ++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs new file mode 100644 index 000000000..0596f1650 --- /dev/null +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeParameterSymbolExtensions.cs @@ -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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.GeneratedDependencyProperty.Extensions; + +/// +/// Extension methods for types. +/// +internal static class ITypeParameterSymbolExtensions +{ + /// + /// Checks whether a given type parameter is a reference type. + /// + /// The input instance to check. + /// Whether the input type parameter is a reference type. + public static bool IsReferenceTypeOrIndirectlyConstrainedToReferenceType(this ITypeParameterSymbol symbol) + { + // The type is definitely a reference type (e.g. it has the 'class' constraint) + if (symbol.IsReferenceType) + { + return true; + } + + // The type is definitely a value type (e.g. it has the 'struct' constraint) + if (symbol.IsValueType) + { + return false; + } + + foreach (ITypeSymbol constraintType in symbol.ConstraintTypes) + { + // Recurse on the type parameter first (e. g. we might indirectly be getting a 'class' constraint) + if (constraintType is ITypeParameterSymbol typeParameter && + typeParameter.IsReferenceTypeOrIndirectlyConstrainedToReferenceType()) + { + return true; + } + + // Special constraint type that type parameters can derive from. Note that for concrete enum + // types, the 'Enum' constraint isn't sufficient, they'd also have e.g. 'struct', which is + // already checked before. If a type parameter only has 'Enum', then it should be considered + // a reference type. + if (constraintType.SpecialType is SpecialType.System_Delegate or SpecialType.System_Enum) + { + return true; + } + + // Only check for classes (an interface doesn't guarantee the type argument will be a reference type) + if (constraintType.TypeKind is TypeKind.Class) + { + return true; + } + } + + return false; + } +} diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 54c9197a8..340192841 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -25,7 +25,7 @@ public static bool IsDefaultValueNull(this ITypeSymbol symbol) // If we do have a type parameter, check that it does have some reference type constraint on it. if (symbol is ITypeParameterSymbol typeParameter) { - return typeParameter.HasReferenceTypeConstraint; + return typeParameter.IsReferenceTypeOrIndirectlyConstrainedToReferenceType(); } return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }; diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 1cf252227..2b0769059 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1765,6 +1765,68 @@ public partial class MyObject : DependencyObject await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + [TestMethod] + [DataRow("where T : class", "null")] + [DataRow("where T : class", "default(T)")] + [DataRow("where T : TOther where TOther : class", "null")] + [DataRow("where T : class where TOther : class", "default(TOther)")] + [DataRow("where T : Delegate", "null")] + [DataRow("where T : Enum", "null")] + [DataRow("where T : DependencyObject", "null")] + [DataRow("where T : DependencyObject", "default(T)")] + [DataRow("where T : TOther where TOther : Delegate", "null")] + [DataRow("where T : TOther where TOther : Enum", "null")] + [DataRow("where T : TOther where TOther : DependencyObject", "null")] + [DataRow("where T : DependencyObject where TOther : class", "default(TOther)")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ConstrainedExplicitNull_DoesNotWarn( + string typeConstraints, + string defaultValue) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + [GeneratedDependencyProperty(DefaultValue = {{defaultValue}})] + public partial T {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("where T : struct")] + [DataRow("where T : unmanaged")] + [DataRow("where T : struct, Enum")] + [DataRow("where T : unmanaged, Enum")] + public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ConstrainedExplicitNull_Warns(string typeConstraints) + { + string source = $$""" + using System; + using CommunityToolkit.WinUI; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + [GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})] + public partial T {|CS9248:Name|} { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("string", "42")] [DataRow("string", "3.14")] @@ -3226,6 +3288,85 @@ public void Dispose() await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Regression test for a case found in the Microsoft Store + [TestMethod] + [DataRow("where T : class", "null")] + [DataRow("where T : class", "default(T)")] + [DataRow("where T : TOther where TOther : class", "null")] + [DataRow("where T : class where TOther : class", "default(TOther)")] + [DataRow("where T : Delegate", "null")] + [DataRow("where T : Enum", "null")] + [DataRow("where T : DependencyObject", "null")] + [DataRow("where T : DependencyObject", "default(T)")] + [DataRow("where T : TOther where TOther : Delegate", "null")] + [DataRow("where T : TOther where TOther : Enum", "null")] + [DataRow("where T : TOther where TOther : DependencyObject", "null")] + [DataRow("where T : DependencyObject where TOther : class", "default(TOther)")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithNullConstrainedGeneric_Warns( + string typeConstraints, + string defaultValue) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(T), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({{defaultValue}})); + + public T {|WCTDPG0017:Name|} + { + get => (T?)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + + [TestMethod] + [DataRow("where T : struct")] + [DataRow("where T : unmanaged")] + [DataRow("where T : struct, Enum")] + [DataRow("where T : unmanaged, Enum")] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithNullConstrainedGeneric_WCTDPG0031_DoesNotWarn(string typeConstraints) + { + string source = $$""" + using System; + using Windows.UI.Xaml; + + #nullable enable + + namespace MyApp; + + public partial class MyObject : DependencyObject {{typeConstraints}} + { + public static readonly DependencyProperty NameProperty = DependencyProperty.Register( + name: "Name", + propertyType: typeof(T), + ownerType: typeof(MyObject), + typeMetadata: new PropertyMetadata({|WCTDPG0031:null|})); + + public T {|WCTDPG0017:Name|} + { + get => (T)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("private static readonly")] [DataRow("public static")] From 9a56de0c40a57513842f0a174215082b12e6dab5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 7 Jan 2025 00:04:06 +0100 Subject: [PATCH 194/200] Add more nullability test cases --- .../Test_Analyzers.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 2b0769059..475d95063 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -1318,6 +1318,7 @@ public partial class MyObject : DependencyObject [DataRow("T1?")] [DataRow("T2?")] [DataRow("T3?")] + [DataRow("T4")] [DataRow("T4?")] public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NullableType_DoesNotWarn(string declaredType) { @@ -1547,6 +1548,8 @@ public sealed partial class KeyFrameCollection : DependencyOb [DataRow("T1")] [DataRow("T2")] [DataRow("T3")] + [DataRow("T5")] + [DataRow("T6")] public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NotNullableType_Warns(string declaredType) { string source = $$""" @@ -1558,10 +1561,12 @@ public async Task InvalidPropertyNullableAnnotationAnalyzer_TypeParameter_NotNul namespace MyApp; - public partial class MyObject : DependencyObject + public partial class MyObject : DependencyObject where T1 : class where T3 : T2, new() where T4 : unmanaged + where T5 : DependencyObject + where T6 : T5 { [GeneratedDependencyProperty] public partial {{declaredType}} {|WCTDPG0009:{|CS9248:Value|}|} { get; set; } From 1d9ba0adbc8c6e2179faa41db838a1b0db3b13a4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 7 Jan 2025 00:52:44 +0100 Subject: [PATCH 195/200] Support target-typed 'new()' expressions for metadata --- ...endencyPropertyOnManualPropertyAnalyzer.cs | 11 +++- .../Test_Analyzers.cs | 29 +++++++++++ ...ndencyPropertyOnManualPropertyCodeFixer.cs | 50 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index 58d816a7b..ddd86eac3 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -512,7 +512,16 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla // Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument, ..] } objectCreationOperation) { - return; + // Before failing, check whether the argument is 'new (...)`. In this case, the target type is 'PropertyMetadata' anyway, + // which we also validate right after this check as well anyway. With 'new()', we expect a conversion operation instead. + if (propertyMetadataArgument.Value is not IConversionOperation { IsImplicit: true } objectCreationConversionOperation || + objectCreationConversionOperation.Operand is not IObjectCreationOperation { Arguments: [_, ..] } conversionObjectCreationOperation) + { + return; + } + + defaultValueArgument = conversionObjectCreationOperation.Arguments[0]; + objectCreationOperation = conversionObjectCreationOperation; } // Make sure the object being created is actually 'PropertyMetadata' diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs index 475d95063..d66660615 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs @@ -3338,6 +3338,35 @@ public partial class MyObject : DependencyObject {{typeConstraints}} await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); } + // Regression test for a case found in the Microsoft Store + [TestMethod] + public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithTargetTypedPropertyMetadataNew_Warns() + { + const string source = """ + using Windows.Foundation; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty VisibleAreaProperty = DependencyProperty.Register( + nameof(VisibleArea), + typeof(Rect), + typeof(MyObject), + new(default(Rect))); + + public Rect {|WCTDPG0017:VisibleArea|} + { + get => (Rect)GetValue(VisibleAreaProperty); + private set => SetValue(VisibleAreaProperty, value); + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13); + } + [TestMethod] [DataRow("where T : struct")] [DataRow("where T : unmanaged")] diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs index 5f05ca959..33d9474fc 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs @@ -2755,4 +2755,54 @@ public enum MyEnum { A, B } await test.RunAsync(); } + + // Regression test for a case found in the Microsoft Store + [TestMethod] + public async Task SimpleProperty_WithTargetTypedPropertyMetadataNew() + { + const string original = """ + using Windows.Foundation; + using Windows.UI.Xaml; + + namespace MyApp; + + public class MyObject : DependencyObject + { + public static readonly DependencyProperty VisibleAreaProperty = DependencyProperty.Register( + nameof(VisibleArea), + typeof(Rect), + typeof(MyObject), + new(default(Rect))); + + public Rect {|WCTDPG0017:VisibleArea|} + { + get => (Rect)GetValue(VisibleAreaProperty); + private set => SetValue(VisibleAreaProperty, value); + } + } + """; + + const string @fixed = """ + using CommunityToolkit.WinUI; + using Windows.Foundation; + using Windows.UI.Xaml; + + namespace MyApp; + + public partial class MyObject : DependencyObject + { + [GeneratedDependencyProperty] + public partial Rect {|CS9248:VisibleArea|} { get; private set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + MarkupOptions = MarkupOptions.UseFirstDescriptor + }; + + await test.RunAsync(); + } } From 74109d65665c8a764ce29c222b53b7abee0677f8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 7 Jan 2025 18:05:42 +0100 Subject: [PATCH 196/200] Fix some incorrect diagnostic messages --- ...atedDependencyPropertyOnManualPropertyAnalyzer.cs | 11 ++--------- .../Diagnostics/DiagnosticDescriptors.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs index ddd86eac3..f2264ee3a 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs @@ -431,9 +431,6 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyTypeExpressionLocation = propertyTypeArgument.Syntax.GetLocation(); } - // Best effort name to use to interpolate any diagnostics below to emit if the default value is not valid - string propertyNameForMessageFormat = "___"; - // We cannot validate the property name from here yet, but let's check it's a constant, and save it for later if (nameArgument.Value.ConstantValue is { HasValue: true, Value: string propertyName }) { @@ -442,8 +439,6 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla fieldFlags.PropertyName = propertyName; } - propertyNameForMessageFormat = propertyName; - // Additional diagnostic #2: the property name should be the same as the field name, without the "Property" suffix (as per convention) if (fieldSymbol.Name.EndsWith("Property") && propertyName != fieldSymbol.Name[..^"Property".Length]) { @@ -555,8 +550,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla InvalidDefaultValueNullOnDependencyPropertyField, defaultValueArgument.Syntax.GetLocation(), fieldSymbol, - propertyTypeSymbol, - propertyNameForMessageFormat)); + propertyTypeSymbol)); } } else @@ -592,8 +586,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla defaultValueArgument.Syntax.GetLocation(), fieldSymbol, operandTypeSymbol, - propertyTypeSymbol, - propertyNameForMessageFormat)); + propertyTypeSymbol)); } } } diff --git a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index e35bd53b8..87a5d7c61 100644 --- a/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -422,28 +422,28 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior). + /// The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, setting the property value to a non 'null' value upon object construction, and/or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidDefaultValueNullOnDependencyPropertyField = new( id: "WCTDPG0031", title: "Invalid 'null' default value in dependency property field metadata", - messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, implementing the 'On{2}Get(ref object)' partial method to handle the type mismatch, or suppressing the diagnostic if this is the intended behavior)", + messageFormat: "The field '{0}' is registering a dependency property, but its default value is set to 'null', which is not compatible with the property type '{1}' declared in metadata (consider changing the default value, setting the property value to a non 'null' value upon object construction, and/or suppressing the diagnostic if this is the intended behavior)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the property value should be set to a non 'null' value upon object construction.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); /// - /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'On{3}Get(ref object)' partial method to handle the type mismatch). + /// The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or suppressing the diagnostic if this is the intended behavior). /// public static readonly DiagnosticDescriptor InvalidDefaultValueTypeOnDependencyPropertyField = new( id: "WCTDPG0032", title: "Invalid default value type in dependency property field metadata", - messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or implementing the 'On{3}Get(ref object)' partial method to handle the type mismatch)", + messageFormat: "The field '{0}' is registering a dependency property, but its default value has type '{1}', which is not compatible with the property type '{2}' declared in metadata (consider fixing the default value, or suppressing the diagnostic if this is the intended behavior)", category: DiagnosticCategory, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type. Alternatively, the generated getter method (eg. 'OnNameGet', if the property is called 'Name') should be implemented to handle the type mismatch.", + description: "All dependency property fields setting an explicit default value in metadata should do so with an expression of a type comparible with the property type.", helpLinkUri: "https://aka.ms/toolkit/labs/windows"); } From f68b3692cad87e3e222fd1d1a4bc3f75ca5f382b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 17 Jan 2025 19:58:15 -0800 Subject: [PATCH 197/200] Fix UWP detection on modern .NET --- .../AppServiceGenerator.Helpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs index ed86c952a..3cdd1dc31 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs @@ -34,7 +34,7 @@ public static bool IsUwpTarget(Compilation compilation, AnalyzerConfigOptions an { if (bool.TryParse(propertyValue, out bool useUwpTools)) { - return true; + return useUwpTools; } } From 70c49d1398471b5aaaba2dc7cc87ba0de924b76d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 19 Jan 2025 11:28:30 -0800 Subject: [PATCH 198/200] Add '[GeneratedAppServiceHost]' attribute type --- .../AppServiceGenerator.cs | 52 +++++++++++++++++-- .../src/GeneratedAppServiceHostAttribute.cs | 28 ++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 components/AppServices/src/GeneratedAppServiceHostAttribute.cs diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs index f85e2a73b..9764606a8 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs @@ -78,7 +78,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); // Gather all interfaces, and only enable this branch if the target is a UWP app (the host) - IncrementalValuesProvider<(HierarchyInfo Hierarchy, AppServiceInfo Info)> appServiceHostInfo = + IncrementalValuesProvider<(HierarchyInfo, AppServiceInfo)> appServiceHostInfo = context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.AppServices.AppServiceAttribute", static (node, _) => node is InterfaceDeclarationSyntax, @@ -105,6 +105,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); + // Gather all methods for the app service type ImmutableArray methods = MethodInfo.From(typeSymbol, token); token.ThrowIfCancellationRequested(); @@ -113,8 +114,47 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }) .Where(static item => item.Hierarchy is not null); - // Produce the host type - context.RegisterSourceOutput(appServiceHostInfo, static (context, item) => + // Also gather all explicitly requested host implementation types + IncrementalValuesProvider<(HierarchyInfo, AppServiceInfo)> additionalAppServiceHostInfo = + context.ForAttributeWithMetadataNameAndOptions( + "CommunityToolkit.AppServices.GeneratedAppServiceHostAttribute", + static (node, _) => true, + static (context, token) => + { + // Only retrieve host info if the target is a UWP application + if (!Helpers.IsUwpTarget(context.SemanticModel.Compilation, context.GlobalOptions)) + { + return default; + } + + // Get the target interface + if (context.Attributes[0].ConstructorArguments is not [{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol appServiceType }]) + { + return default; + } + + // Check if the current interface is in fact an app service type + if (!appServiceType.TryGetAppServicesNameFromAttribute(out string? appServiceName)) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + HierarchyInfo hierarchy = HierarchyInfo.From(appServiceType, appServiceType.Name.Substring(1)); + + token.ThrowIfCancellationRequested(); + + ImmutableArray methods = MethodInfo.From(appServiceType, token); + + token.ThrowIfCancellationRequested(); + + return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName, appServiceType.GetFullyQualifiedName())); + }) + .Where(static item => item.Hierarchy is not null); + + // Shared helper to emit all discovered types + static void GenerateAppServiceHostType(SourceProductionContext context, (HierarchyInfo Hierarchy, AppServiceInfo Info) item) { ConstructorDeclarationSyntax constructorSyntax = Host.GetConstructorSyntax(item.Hierarchy, item.Info); ImmutableArray methodDeclarations = Host.GetMethodDeclarationsSyntax(item.Info); @@ -126,6 +166,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) $"/// A generated host implementation for the interface."); context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8)); - }); + } + + // Produce the host types + context.RegisterSourceOutput(appServiceHostInfo, GenerateAppServiceHostType); + context.RegisterSourceOutput(additionalAppServiceHostInfo, GenerateAppServiceHostType); } } diff --git a/components/AppServices/src/GeneratedAppServiceHostAttribute.cs b/components/AppServices/src/GeneratedAppServiceHostAttribute.cs new file mode 100644 index 000000000..9b5d251af --- /dev/null +++ b/components/AppServices/src/GeneratedAppServiceHostAttribute.cs @@ -0,0 +1,28 @@ +// 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; + +namespace CommunityToolkit.AppServices; + +/// +/// An attribute that can be used to request the generator to emit a host implementation of a given app service. +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] +public sealed class GeneratedAppServiceHostAttribute : Attribute +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The type of the app service. + public GeneratedAppServiceHostAttribute(Type appServiceType) + { + AppServiceType = appServiceType; + } + + /// + /// Gets the type of the app service. + /// + public Type AppServiceType { get; } +} From 7ccd2f593d84973e3b84bce78bcb91f4fc461d1e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 22 Jan 2025 23:02:20 -0800 Subject: [PATCH 199/200] Add 'AppServiceHost.OnCloseRequested' method --- components/AppServices/src/AppServiceHost.cs | 55 +++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/components/AppServices/src/AppServiceHost.cs b/components/AppServices/src/AppServiceHost.cs index 28cb0b727..165abbf4e 100644 --- a/components/AppServices/src/AppServiceHost.cs +++ b/components/AppServices/src/AppServiceHost.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.AppServices.Helpers; using Windows.ApplicationModel; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.AppService; @@ -15,7 +16,7 @@ using Windows.Foundation.Collections; using Windows.Foundation.Metadata; using Windows.System.Profile; -using CommunityToolkit.AppServices.Helpers; +using Windows.UI.Core.Preview; #pragma warning disable CA1068 @@ -143,6 +144,58 @@ public bool OnBackgroundActivated(BackgroundActivatedEventArgs args) return true; } + /// + /// Handles the app service host shutdown when is raised. + /// + /// The args for the close request. + /// + /// + /// This method should be used as follows (from App.xaml.cs): + /// + /// private void OnCloseRequested(object? sender, SystemNavigationCloseRequestedPreviewEventArgs e) + /// { + /// // Any other work, possibly marking the request as handled + /// + /// DesktopExtension.OnCloseRequested(e); + /// } + /// + /// + /// + /// The app might be holding a deferral for the app service connection to the extension process, which is currently only completed when the + /// connection is closed. This means that when the application is closed, that deferral will actually try to keep the connection alive, until + /// the OS will eventually force terminate it. This will cause following launches of the app to be delayed until the previous process is + /// completely gone, meaning that closing the app and immediately reopening it will cause it to remain stuck at the splash screen for a few + /// seconds. Note that during this time, no app code is actually executed, it's just that the OS is waiting to terminate the existing connection + /// and fully close the previous instance before allowing a new one to be started. To avoid this issue, this method takes care of fully closing + /// any existing connection (by canceling its associated deferral), when the app is about to exit. This avoids the OS timeout for the connection. + /// + /// + public void OnCloseRequested(SystemNavigationCloseRequestedPreviewEventArgs args) + { + // Do nothing if the close request has been handled + if (args.Handled) + { + return; + } + + // Remove the registered connection handlers + if (_appServiceConnection is { } appServiceConnection) + { + appServiceConnection.ServiceClosed -= AppServiceConnection_ServiceClosed; + appServiceConnection.RequestReceived -= AppServiceConnection_RequestReceived; + + _appServiceConnection = null; + } + + // Cancel the deferral, if present + if (_appServiceDeferral is { } appServiceDeferral) + { + appServiceDeferral.Complete(); + + _appServiceDeferral = null; + } + } + /// /// Creates a new for a given operation. /// From 909b748fc9cd4dfb9d7ba38ecb01762a36710f9e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 27 Jan 2025 14:19:13 -0800 Subject: [PATCH 200/200] Fix incorrect XML doc --- components/AppServices/src/AppServiceHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/AppServices/src/AppServiceHost.cs b/components/AppServices/src/AppServiceHost.cs index 165abbf4e..cd193e855 100644 --- a/components/AppServices/src/AppServiceHost.cs +++ b/components/AppServices/src/AppServiceHost.cs @@ -145,7 +145,7 @@ public bool OnBackgroundActivated(BackgroundActivatedEventArgs args) } /// - /// Handles the app service host shutdown when is raised. + /// Handles the app service host shutdown when is raised. /// /// The args for the close request. ///