Skip to content

Commit c37fb2f

Browse files
committed
Tweak 'InvalidPropertyNullableAnnotationAnalyzer'
1 parent 761f87c commit c37fb2f

File tree

4 files changed

+340
-82
lines changed

4 files changed

+340
-82
lines changed

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

Lines changed: 0 additions & 71 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 CommunityToolkit.GeneratedDependencyProperty.Constants;
7+
using CommunityToolkit.GeneratedDependencyProperty.Extensions;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
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 when a property with <c>[GeneratedDependencyProperty]</c> would generate a nullability annotations violation.
17+
/// </summary>
18+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
19+
public sealed class InvalidPropertyNullableAnnotationAnalyzer : DiagnosticAnalyzer
20+
{
21+
/// <inheritdoc/>
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [NonNullablePropertyDeclarationIsNotEnforced];
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 '[GeneratedDependencyProperty]' symbol (there might be multiples, due to embedded mode)
33+
ImmutableArray<INamedTypeSymbol> generatedDependencyPropertyAttributeSymbols = context.Compilation.GetTypesByMetadataName(WellKnownTypeNames.GeneratedDependencyPropertyAttribute);
34+
35+
// Attempt to also get the '[MaybeNull]', '[NotNull]', '[AllowNull]' and '[DisallowNull]' symbols (there might be multiples, due to polyfills)
36+
ImmutableArray<INamedTypeSymbol> maybeNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.MaybeNullAttribute");
37+
ImmutableArray<INamedTypeSymbol> notNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.NotNullAttribute");
38+
ImmutableArray<INamedTypeSymbol> allowNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.AllowNullAttribute");
39+
ImmutableArray<INamedTypeSymbol> disallowNullAttributeSymbols = context.Compilation.GetTypesByMetadataName("System.Diagnostics.CodeAnalysis.DisallowNullAttribute");
40+
41+
context.RegisterSymbolAction(context =>
42+
{
43+
// Validate that we have a property that is of some type that could potentially become 'null'
44+
if (context.Symbol is not IPropertySymbol { Type.IsValueType: false, NullableAnnotation: not NullableAnnotation.None } propertySymbol)
45+
{
46+
return;
47+
}
48+
49+
// If the property is not using '[GeneratedDependencyProperty]', there's nothing to do
50+
if (!propertySymbol.TryGetAttributeWithAnyType(generatedDependencyPropertyAttributeSymbols, out AttributeData? attributeData))
51+
{
52+
return;
53+
}
54+
55+
// Handle nullable and non-null properties differently
56+
if (propertySymbol.NullableAnnotation is NullableAnnotation.Annotated)
57+
{
58+
// If we don't have '[NotNull]', we'll never need to emit a diagnostic.
59+
// That is, the default nullable state will always be correct already.
60+
if (!propertySymbol.HasAttributeWithAnyType(notNullAttributeSymbols))
61+
{
62+
return;
63+
}
64+
65+
// If we have '[NotNull]', it means the property getter must always ensure that a non-null value is returned.
66+
// This can be achieved in two different ways:
67+
// 1) By implementing one of the 'On___Get' methods, and adding '[NotNull]' on the parameter.
68+
// 2) By having '[DisallowNull]' on the property, and either marking the property as required, or providing a non-null default value.
69+
if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) &&
70+
!(propertySymbol.HasAttributeWithAnyType(disallowNullAttributeSymbols) &&
71+
(propertySymbol.IsRequired || IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols))))
72+
{
73+
context.ReportDiagnostic(Diagnostic.Create(
74+
NonNullablePropertyDeclarationIsNotEnforced,
75+
attributeData.GetLocation(),
76+
propertySymbol));
77+
}
78+
}
79+
else
80+
{
81+
// If the property is not nullable and it has '[MaybeNull]', we never need to emit a diagnostic.
82+
// That is, setting 'null' is valid, and the initial state doesn't matter, as the return is nullable.
83+
if (propertySymbol.HasAttributeWithAnyType(maybeNullAttributeSymbols))
84+
{
85+
return;
86+
}
87+
88+
// If setting 'null' values is allowed, then the initial state (and the default value) don't matter anymore.
89+
// In order to be correct, we must have '[NotNull]' on any implemented getter or setter methods (same as above).
90+
if (propertySymbol.HasAttributeWithAnyType(allowNullAttributeSymbols))
91+
{
92+
if (!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.GetAccessorDeclaration, notNullAttributeSymbols) &&
93+
!IsAccessorMethodMarkedAsNotNull(propertySymbol, SyntaxKind.SetAccessorDeclaration, notNullAttributeSymbols))
94+
{
95+
context.ReportDiagnostic(Diagnostic.Create(
96+
NonNullablePropertyDeclarationIsNotEnforced,
97+
attributeData.GetLocation(),
98+
propertySymbol));
99+
}
100+
}
101+
else
102+
{
103+
// Otherwise, we need to check that either the property is required, or that the default value is not 'null'.
104+
// This is because when the nullability of the setter is correct, then the default value takes precedence.
105+
if (!propertySymbol.IsRequired && !IsDefaultValueNotNull(propertySymbol, attributeData, maybeNullAttributeSymbols, notNullAttributeSymbols))
106+
{
107+
context.ReportDiagnostic(Diagnostic.Create(
108+
NonNullablePropertyDeclarationIsNotEnforced,
109+
attributeData.GetLocation(),
110+
propertySymbol));
111+
}
112+
}
113+
}
114+
}, SymbolKind.Property);
115+
});
116+
}
117+
/// <summary>
118+
/// Checks whether a given generated accessor method has <c>[NotNull]</c> on its parameter.
119+
/// </summary>
120+
/// <param name="propertySymbol">The <see cref="IPropertySymbol"/> instance to inspect.</param>
121+
/// <param name="accessorKind">The syntax kind for the accessor method to look for.</param>
122+
/// <param name="notNullAttributeSymbols">The <see cref="INamedTypeSymbol"/> instances for <c>[NotNull]</c>.</param>
123+
/// <returns>Whether <paramref name="propertySymbol"/> has a generated accessor method with its parameter marked with <c>[NotNull]</c>.</returns>
124+
private static bool IsAccessorMethodMarkedAsNotNull(IPropertySymbol propertySymbol, SyntaxKind accessorKind, ImmutableArray<INamedTypeSymbol> notNullAttributeSymbols)
125+
{
126+
string suffix = accessorKind == SyntaxKind.GetAccessorDeclaration ? "Get" : "Set";
127+
128+
foreach (ISymbol symbol in propertySymbol.ContainingType.GetMembers($"On{propertySymbol.Name}{suffix}"))
129+
{
130+
// We really only expect to match our own generated methods, but do some basic filtering just in case
131+
if (symbol is not IMethodSymbol { IsStatic: false, ReturnsVoid: true, Parameters: [{ Type: INamedTypeSymbol, RefKind: RefKind.Ref } propertyValue] })
132+
{
133+
continue;
134+
}
135+
136+
// Check if the parameter has '[NotNull]' on it
137+
if (propertyValue.HasAttributeWithAnyType(notNullAttributeSymbols))
138+
{
139+
return true;
140+
}
141+
}
142+
143+
return false;
144+
}
145+
146+
/// <summary>
147+
/// Checks whether a given property has a default value that is not <see langword="null"/>.
148+
/// </summary>
149+
/// <param name="propertySymbol">The <see cref="IPropertySymbol"/> instance to inspect.</param>
150+
/// <param name="attributeData">The <see cref="AttributeData"/> instance on <paramref name="propertySymbol"/>.</param>
151+
/// <param name="maybeNullAttributeSymbols">The <see cref="INamedTypeSymbol"/> instances for <c>[MaybeNull]</c>.</param>
152+
/// <param name="notNullAttributeSymbols">The <see cref="INamedTypeSymbol"/> instances for <c>[NotNull]</c>.</param>
153+
/// <returns></returns>
154+
private static bool IsDefaultValueNotNull(
155+
IPropertySymbol propertySymbol,
156+
AttributeData attributeData,
157+
ImmutableArray<INamedTypeSymbol> maybeNullAttributeSymbols,
158+
ImmutableArray<INamedTypeSymbol> notNullAttributeSymbols)
159+
{
160+
// If we have a default value, check that it's not 'null'
161+
if (attributeData.TryGetNamedArgument("DefaultValue", out TypedConstant defaultValue))
162+
{
163+
return !defaultValue.IsNull;
164+
}
165+
166+
// If we have a callback, validate its return type
167+
if (attributeData.TryGetNamedArgument("DefaultValueCallback", out TypedConstant defaultValueCallback))
168+
{
169+
// Find the target method (same logic here as in the generator)
170+
if (defaultValueCallback is { Type.SpecialType: SpecialType.System_String, Value: string { Length: > 0 } methodName } &&
171+
InvalidPropertyDefaultValueCallbackTypeAnalyzer.TryFindDefaultValueCallbackMethod(propertySymbol, methodName, out IMethodSymbol? methodSymbol) &&
172+
InvalidPropertyDefaultValueCallbackTypeAnalyzer.IsDefaultValueCallbackValid(propertySymbol, methodSymbol))
173+
{
174+
// Verify that the return type can't possibly be 'null', including using attributes
175+
return
176+
(methodSymbol.ReturnNullableAnnotation is NullableAnnotation.NotAnnotated && !methodSymbol.HasReturnAttributeWithAnyType(maybeNullAttributeSymbols)) ||
177+
(methodSymbol.ReturnNullableAnnotation is NullableAnnotation.Annotated && methodSymbol.HasReturnAttributeWithAnyType(notNullAttributeSymbols));
178+
}
179+
}
180+
181+
return false;
182+
}
183+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.Diagnostics.CodeAnalysis;
7+
using Microsoft.CodeAnalysis;
8+
9+
namespace CommunityToolkit.GeneratedDependencyProperty.Extensions;
10+
11+
/// <summary>
12+
/// Extension methods for <see cref="IMethodSymbol"/> types.
13+
/// </summary>
14+
internal static class IMethodSymbolExtensions
15+
{
16+
/// <summary>
17+
/// Checks whether or not a given symbol has a return attribute with the specified type.
18+
/// </summary>
19+
/// <param name="symbol">The input <see cref="IMethodSymbol"/> instance to check.</param>
20+
/// <param name="typeSymbols">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
21+
/// <returns>Whether or not <paramref name="symbol"/> has a return attribute with the specified type.</returns>
22+
public static bool HasReturnAttributeWithAnyType(this IMethodSymbol symbol, ImmutableArray<INamedTypeSymbol> typeSymbols)
23+
{
24+
return TryGetReturnAttributeWithAnyType(symbol, typeSymbols, out _);
25+
}
26+
27+
/// <summary>
28+
/// Tries to get a return attribute with any of the specified types.
29+
/// </summary>
30+
/// <param name="symbol">The input <see cref="IMethodSymbol"/> instance to check.</param>
31+
/// <param name="typeSymbols">The <see cref="ITypeSymbol"/> instance for the attribute type to look for.</param>
32+
/// <param name="attributeData">The first return attribute of a type matching any type in <paramref name="typeSymbols"/>, if found.</param>
33+
/// <returns>Whether or not <paramref name="symbol"/> has a return attribute with the specified type.</returns>
34+
public static bool TryGetReturnAttributeWithAnyType(this IMethodSymbol symbol, ImmutableArray<INamedTypeSymbol> typeSymbols, [NotNullWhen(true)] out AttributeData? attributeData)
35+
{
36+
foreach (AttributeData attribute in symbol.GetReturnTypeAttributes())
37+
{
38+
if (typeSymbols.Contains(attribute.AttributeClass!, SymbolEqualityComparer.Default))
39+
{
40+
attributeData = attribute;
41+
42+
return true;
43+
}
44+
}
45+
46+
attributeData = null;
47+
48+
return false;
49+
}
50+
}

0 commit comments

Comments
 (0)