Skip to content

Commit 618e6bb

Browse files
committed
Add 'UseFieldDeclarationAnalyzer' and tests
1 parent 59e4306 commit 618e6bb

File tree

9 files changed

+290
-4
lines changed

9 files changed

+290
-4
lines changed

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ WCTDP0017 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenera
2727
WCTDP0018 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error |
2828
WCTDP0019 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Error |
2929
WCTDP0020 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning |
30+
WCTDP0021 | CommunityToolkit.GeneratedDependencyPropertyDependencyPropertyGenerator | Warning |

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Constants/WellKnownPropertyNames.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ internal static class WellKnownPropertyNames
1313
/// The MSBuild property to control the XAML mode.
1414
/// </summary>
1515
public const string DependencyPropertyGeneratorUseWindowsUIXaml = nameof(DependencyPropertyGeneratorUseWindowsUIXaml);
16+
17+
/// <summary>
18+
/// The MSBuild property to control whether the project is a WinRT component.
19+
/// </summary>
20+
public const string CsWinRTComponent = nameof(CsWinRTComponent);
1621
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using System.Linq;
7+
using CommunityToolkit.GeneratedDependencyProperty.Constants;
8+
using CommunityToolkit.GeneratedDependencyProperty.Extensions;
9+
using Microsoft.CodeAnalysis;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using static CommunityToolkit.GeneratedDependencyProperty.Diagnostics.DiagnosticDescriptors;
12+
13+
namespace CommunityToolkit.GeneratedDependencyProperty;
14+
15+
/// <summary>
16+
/// A diagnostic analyzer that generates a warning whenever a dependency property is declared as a property.
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public sealed class UseFieldDeclarationAnalyzer : DiagnosticAnalyzer
20+
{
21+
/// <inheritdoc/>
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [DependencyPropertyFieldDeclaration];
23+
24+
/// <inheritdoc/>
25+
public override void Initialize(AnalysisContext context)
26+
{
27+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
28+
context.EnableConcurrentExecution();
29+
30+
context.RegisterCompilationStartAction(static context =>
31+
{
32+
// Get the XAML mode to use
33+
bool useWindowsUIXaml = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.DependencyPropertyGeneratorUseWindowsUIXaml);
34+
35+
// Get the 'DependencyProperty' symbol
36+
if (context.Compilation.GetTypeByMetadataName(WellKnownTypeNames.DependencyProperty(useWindowsUIXaml)) is not { } dependencyPropertySymbol)
37+
{
38+
return;
39+
}
40+
41+
// Check whether the current project is a WinRT component (modern .NET uses CsWinRT, legacy .NET produces .winmd files directly)
42+
bool isWinRTComponent =
43+
context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.GetMSBuildBooleanPropertyValue(WellKnownPropertyNames.CsWinRTComponent) ||
44+
context.Compilation.Options.OutputKind is OutputKind.WindowsRuntimeMetadata;
45+
46+
context.RegisterSymbolAction(context =>
47+
{
48+
IPropertySymbol propertySymbol = (IPropertySymbol)context.Symbol;
49+
50+
// We only care about properties which are of type 'DependencyProperty'
51+
if (!SymbolEqualityComparer.Default.Equals(propertySymbol.Type, dependencyPropertySymbol))
52+
{
53+
return;
54+
}
55+
56+
// If the property is an explicit interface implementation, allow it
57+
if (propertySymbol.ExplicitInterfaceImplementations.Length > 0)
58+
{
59+
return;
60+
}
61+
62+
// Next, make sure this property isn't (implicitly) implementing any interface properties.
63+
// If that's the case, we'll also allow it, as otherwise fixing this would break things.
64+
foreach (INamedTypeSymbol interfaceSymbol in propertySymbol.ContainingType.AllInterfaces)
65+
{
66+
// Go over all properties (we can filter to just those with the same name) in each interface
67+
foreach (IPropertySymbol interfacePropertySymbol in interfaceSymbol.GetMembers(propertySymbol.Name).OfType<IPropertySymbol>())
68+
{
69+
// The property must have the same type to possibly be an interface implementation
70+
if (!SymbolEqualityComparer.Default.Equals(interfacePropertySymbol.Type, propertySymbol.Type))
71+
{
72+
continue;
73+
}
74+
75+
// If the property is not implemented at all, ignore it
76+
if (propertySymbol.ContainingType.FindImplementationForInterfaceMember(interfacePropertySymbol) is not IPropertySymbol implementationSymbol)
77+
{
78+
continue;
79+
}
80+
81+
// If the current property is the one providing the implementation, then we allow it and stop here
82+
if (SymbolEqualityComparer.Default.Equals(implementationSymbol, propertySymbol))
83+
{
84+
return;
85+
}
86+
}
87+
}
88+
89+
// Make an exception for WinRT components: in this case declaring properties is valid, as they're needed for WinRT
90+
if (isWinRTComponent && propertySymbol.GetEffectiveAccessibility() is Accessibility.Public)
91+
{
92+
return;
93+
}
94+
95+
// At this point, we know for sure the property isn't valid, so emit a diagnostic
96+
context.ReportDiagnostic(Diagnostic.Create(
97+
DependencyPropertyFieldDeclaration,
98+
propertySymbol.Locations.First(),
99+
propertySymbol));
100+
}, SymbolKind.Property);
101+
});
102+
}
103+
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseFieldDeclarationCorrectlyAnalyzer.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public override void Initialize(AnalysisContext context)
2727
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
2828
context.EnableConcurrentExecution();
2929

30-
3130
context.RegisterCompilationStartAction(static context =>
3231
{
3332
// Get the XAML mode to use
@@ -43,7 +42,7 @@ public override void Initialize(AnalysisContext context)
4342
{
4443
IFieldSymbol fieldSymbol = (IFieldSymbol)context.Symbol;
4544

46-
// We only care about fields with are of type 'DependencyProperty'
45+
// We only care about fields which are of type 'DependencyProperty'
4746
if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.Type, dependencyPropertySymbol))
4847
{
4948
return;

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ internal static class DiagnosticDescriptors
2121
/// </summary>
2222
public const string IncorrectDependencyPropertyFieldDeclarationId = "WCTDP0020";
2323

24+
/// <summary>
25+
/// The diagnostic id for <see cref="DependencyPropertyFieldDeclaration"/>.
26+
/// </summary>
27+
public const string DependencyPropertyFieldDeclarationId = "WCTDP0021";
28+
2429
/// <summary>
2530
/// <c>"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)"</c>.
2631
/// </summary>
@@ -280,4 +285,17 @@ internal static class DiagnosticDescriptors
280285
isEnabledByDefault: true,
281286
description: "All dependency property fields should be declared as 'public static readonly', and not be nullable.",
282287
helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property");
288+
289+
/// <summary>
290+
/// <c>"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)"</c>.
291+
/// </summary>
292+
public static readonly DiagnosticDescriptor DependencyPropertyFieldDeclaration = new(
293+
id: DependencyPropertyFieldDeclarationId,
294+
title: "Dependency property declared as a property",
295+
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)",
296+
category: typeof(DependencyPropertyGenerator).FullName,
297+
defaultSeverity: DiagnosticSeverity.Warning,
298+
isEnabledByDefault: true,
299+
description: "All dependency properties should be declared as fields, unless implementing interface members or in authored WinRT component types.",
300+
helpLinkUri: "https://learn.microsoft.com/windows/uwp/xaml-platform/custom-dependency-properties#checklist-for-defining-a-dependency-property");
283301
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ISymbolExtensions.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,42 @@ public static bool TryGetAttributeWithAnyType(this ISymbol symbol, ImmutableArra
9191

9292
return false;
9393
}
94+
95+
/// <summary>
96+
/// Calculates the effective accessibility for a given symbol.
97+
/// </summary>
98+
/// <param name="symbol">The <see cref="ISymbol"/> instance to check.</param>
99+
/// <returns>The effective accessibility for <paramref name="symbol"/>.</returns>
100+
public static Accessibility GetEffectiveAccessibility(this ISymbol symbol)
101+
{
102+
// Start by assuming it's visible
103+
Accessibility visibility = Accessibility.Public;
104+
105+
// Handle special cases
106+
switch (symbol.Kind)
107+
{
108+
case SymbolKind.Alias: return Accessibility.Private;
109+
case SymbolKind.Parameter: return GetEffectiveAccessibility(symbol.ContainingSymbol);
110+
case SymbolKind.TypeParameter: return Accessibility.Private;
111+
}
112+
113+
// Traverse the symbol hierarchy to determine the effective accessibility
114+
while (symbol is not null && symbol.Kind != SymbolKind.Namespace)
115+
{
116+
switch (symbol.DeclaredAccessibility)
117+
{
118+
case Accessibility.NotApplicable:
119+
case Accessibility.Private:
120+
return Accessibility.Private;
121+
case Accessibility.Internal:
122+
case Accessibility.ProtectedAndInternal:
123+
visibility = Accessibility.Internal;
124+
break;
125+
}
126+
127+
symbol = symbol.ContainingSymbol;
128+
}
129+
130+
return visibility;
131+
}
94132
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
6+
using System.Linq;
7+
using System.Text;
58
using System.Threading;
69
using System.Threading.Tasks;
710
using CommunityToolkit.WinUI;
811
using Microsoft.CodeAnalysis.CSharp.Testing;
912
using Microsoft.CodeAnalysis.CSharp;
1013
using Microsoft.CodeAnalysis.Diagnostics;
1114
using Microsoft.CodeAnalysis.Testing;
15+
using Microsoft.CodeAnalysis.Text;
1216
using Microsoft.CodeAnalysis;
1317
using Windows.Foundation;
1418
using Windows.UI.ViewManagement;
@@ -59,4 +63,34 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe
5963

6064
return test.RunAsync(CancellationToken.None);
6165
}
66+
67+
/// <inheritdoc cref="AnalyzerVerifier{TAnalyzer, TTest, TVerifier}.VerifyAnalyzerAsync"/>
68+
/// <param name="languageVersion">The language version to use to run the test.</param>
69+
public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, (string PropertyName, object PropertyValue)[] editorconfig)
70+
{
71+
CSharpAnalyzerTest<TAnalyzer> test = new(languageVersion) { TestCode = source };
72+
73+
test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
74+
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Point).Assembly.Location));
75+
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location));
76+
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location));
77+
test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location));
78+
79+
// Add any editorconfig properties, if present
80+
if (editorconfig.Length > 0)
81+
{
82+
test.SolutionTransforms.Add((solution, projectId) =>
83+
solution.AddAnalyzerConfigDocument(
84+
DocumentId.CreateNewId(projectId),
85+
"DependencyPropertyGenerator.editorconfig",
86+
SourceText.From($"""
87+
is_global = true
88+
{string.Join(Environment.NewLine, editorconfig.Select(static p => $"build_property.{p.PropertyName} = {p.PropertyValue}"))}
89+
""",
90+
Encoding.UTF8),
91+
filePath: "/DependencyPropertyGenerator.editorconfig"));
92+
}
93+
94+
return test.RunAsync(CancellationToken.None);
95+
}
6296
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1821,7 +1821,7 @@ public class TestAttribute(string P) : Attribute
18211821
[TestMethod]
18221822
public async Task UseFieldDeclarationCorrectlyAnalyzer_NotDependencyProperty_DoesNotWarn()
18231823
{
1824-
string source = $$"""
1824+
const string source = """
18251825
using Windows.UI.Xaml;
18261826
18271827
public class MyObject : DependencyObject
@@ -1869,4 +1869,91 @@ public class MyObject : DependencyObject
18691869

18701870
await CSharpAnalyzerTest<UseFieldDeclarationCorrectlyAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
18711871
}
1872+
1873+
[TestMethod]
1874+
public async Task UseFieldDeclarationAnalyzer_NotDependencyProperty_DoesNotWarn()
1875+
{
1876+
const string source = """
1877+
using Windows.UI.Xaml;
1878+
1879+
public class MyObject : DependencyObject
1880+
{
1881+
public static string TestProperty => "Blah";
1882+
}
1883+
""";
1884+
1885+
await CSharpAnalyzerTest<UseFieldDeclarationAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
1886+
}
1887+
1888+
[TestMethod]
1889+
public async Task UseFieldDeclarationAnalyzer_ExplicitInterfaceImplementation_DoesNotWarn()
1890+
{
1891+
const string source = """
1892+
using Windows.UI.Xaml;
1893+
1894+
public class MyObject : DependencyObject, IMyObject
1895+
{
1896+
static DependencyProperty IMyObject.TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null);
1897+
}
1898+
1899+
public interface IMyObject
1900+
{
1901+
static abstract DependencyProperty TestProperty { get; }
1902+
}
1903+
""";
1904+
1905+
await CSharpAnalyzerTest<UseFieldDeclarationAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
1906+
}
1907+
1908+
[TestMethod]
1909+
public async Task UseFieldDeclarationAnalyzer_ImplicitInterfaceImplementation_DoesNotWarn()
1910+
{
1911+
const string source = """
1912+
using Windows.UI.Xaml;
1913+
1914+
public class MyObject : DependencyObject, IMyObject
1915+
{
1916+
public static DependencyProperty TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null);
1917+
}
1918+
1919+
public interface IMyObject
1920+
{
1921+
static abstract DependencyProperty TestProperty { get; }
1922+
}
1923+
""";
1924+
1925+
await CSharpAnalyzerTest<UseFieldDeclarationAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
1926+
}
1927+
1928+
[TestMethod]
1929+
public async Task UseFieldDeclarationAnalyzer_WinRTComponent_DoesNotWarn()
1930+
{
1931+
const string source = """
1932+
using Windows.UI.Xaml;
1933+
1934+
public class MyObject : DependencyObject
1935+
{
1936+
public static DependencyProperty TestProperty => DependencyProperty.Register("Test", typeof(string), typeof(MyObject), null);
1937+
}
1938+
""";
1939+
1940+
await CSharpAnalyzerTest<UseFieldDeclarationAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13, editorconfig: [("CsWinRTComponent", true)]);
1941+
}
1942+
1943+
[TestMethod]
1944+
public async Task UseFieldDeclarationAnalyzer_NormalProperty_Warns()
1945+
{
1946+
const string source = """
1947+
using Windows.UI.Xaml;
1948+
1949+
public class MyObject : DependencyObject
1950+
{
1951+
public static DependencyProperty {|WCTDP0021:Test1Property|} => DependencyProperty.Register("Test1", typeof(string), typeof(MyObject), null);
1952+
public static DependencyProperty {|WCTDP0021:Test2Property|} { get; } = DependencyProperty.Register("Test2", typeof(string), typeof(MyObject), null);
1953+
public DependencyProperty {|WCTDP0021:Test3Property|} { get; set; }
1954+
}
1955+
""";
1956+
1957+
await CSharpAnalyzerTest<UseFieldDeclarationAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
1958+
}
18721959
}

components/DependencyPropertyGenerator/src/CommunityToolkit.WinUI.DependencyPropertyGenerator.targets

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
<EnableGeneratedDependencyPropertyEmbeddedMode Condition="'$(EnableGeneratedDependencyPropertyEmbeddedMode)' == ''">false</EnableGeneratedDependencyPropertyEmbeddedMode>
1414
</PropertyGroup>
1515

16-
<!-- Allow the source generators to detect the selected XAML mode -->
16+
<!-- Mark all the MSBuild properties that the generators/analyzers might need -->
1717
<ItemGroup>
1818
<CompilerVisibleProperty Include="DependencyPropertyGeneratorUseWindowsUIXaml" />
19+
<CompilerVisibleProperty Include="CsWinRTComponent" />
1920
</ItemGroup>
2021

2122
<!-- Define the build constants depending on the current configuration -->

0 commit comments

Comments
 (0)