Browse Source

Added InappropriatePropertyRegistration and UnexpectedPropertyAccess

Fixed AttachedProperty registrations in non-AvaloniaObject types not being detected
Optimised parameter lookup
pull/10244/head
Tom Edwards 3 years ago
parent
commit
2b68e42e31
  1. 259
      src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
  2. 98
      src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs

259
src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs

@ -31,6 +31,7 @@ public partial class AvaloniaPropertyAnalyzer
private readonly INamedTypeSymbol _avaloniaObjectType; private readonly INamedTypeSymbol _avaloniaObjectType;
private readonly ImmutableHashSet<IMethodSymbol> _getValueMethods; private readonly ImmutableHashSet<IMethodSymbol> _getValueMethods;
private readonly ImmutableHashSet<IMethodSymbol> _setValueMethods; private readonly ImmutableHashSet<IMethodSymbol> _setValueMethods;
private readonly ImmutableHashSet<IMethodSymbol> _allGetSetMethods;
private readonly INamedTypeSymbol _avaloniaPropertyType; private readonly INamedTypeSymbol _avaloniaPropertyType;
private readonly INamedTypeSymbol _styledPropertyType; private readonly INamedTypeSymbol _styledPropertyType;
private readonly INamedTypeSymbol _attachedPropertyType; private readonly INamedTypeSymbol _attachedPropertyType;
@ -38,6 +39,11 @@ public partial class AvaloniaPropertyAnalyzer
private readonly ImmutableArray<INamedTypeSymbol> _allAvaloniaPropertyTypes; private readonly ImmutableArray<INamedTypeSymbol> _allAvaloniaPropertyTypes;
private readonly ImmutableHashSet<IMethodSymbol> _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet<IMethodSymbol> _avaloniaPropertyRegisterMethods;
private readonly ImmutableHashSet<IMethodSymbol> _avaloniaPropertyAddOwnerMethods; private readonly ImmutableHashSet<IMethodSymbol> _avaloniaPropertyAddOwnerMethods;
private readonly ImmutableHashSet<IMethodSymbol> _allAvaloniaPropertyMethods;
private readonly ImmutableDictionary<IMethodSymbol, ITypeParameterSymbol> _ownerTypeParams;
private readonly ImmutableDictionary<IMethodSymbol, ITypeParameterSymbol> _valueTypeParams;
private readonly ImmutableDictionary<IMethodSymbol, ITypeParameterSymbol> _hostTypeParams;
private readonly ImmutableDictionary<IMethodSymbol, IParameterSymbol> _inheritsParams;
public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType)
{ {
@ -47,6 +53,7 @@ public partial class AvaloniaPropertyAnalyzer
_avaloniaObjectType = avaloniaObjectType; _avaloniaObjectType = avaloniaObjectType;
_getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType<IMethodSymbol>().ToImmutableHashSet(methodComparer); _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType<IMethodSymbol>().ToImmutableHashSet(methodComparer);
_setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType<IMethodSymbol>().ToImmutableHashSet(methodComparer); _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType<IMethodSymbol>().ToImmutableHashSet(methodComparer);
_allGetSetMethods = _getValueMethods.Concat(_setValueMethods).ToImmutableHashSet(methodComparer);
_avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty");
_styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1");
@ -61,11 +68,19 @@ public partial class AvaloniaPropertyAnalyzer
_avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes
.SelectMany(t => t.GetMembers("AddOwner").OfType<IMethodSymbol>()).ToImmutableHashSet(methodComparer); .SelectMany(t => t.GetMembers("AddOwner").OfType<IMethodSymbol>()).ToImmutableHashSet(methodComparer);
_allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(SymbolEqualityComparer<IMethodSymbol>.Default);
_ownerTypeParams = GetParamDictionary("TOwner", m => m.TypeParameters);
_valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters);
_hostTypeParams = GetParamDictionary("THost", m => m.TypeParameters);
_inheritsParams = GetParamDictionary("inherits", m => m.Parameters);
RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken); RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken);
context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer);
context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer);
context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment);
context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation);
context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property); context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, SymbolKind.Property);
@ -75,6 +90,11 @@ public partial class AvaloniaPropertyAnalyzer
} }
INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context."); INamedTypeSymbol GetTypeOrThrow(string name) => context.Compilation.GetTypeByMetadataName(name) ?? throw new KeyNotFoundException($"Could not locate {name} in the compilation context.");
ImmutableDictionary<IMethodSymbol, TSymbol> GetParamDictionary<TSymbol>(string name, Func<IMethodSymbol, IEnumerable<TSymbol>> methodSymbolSelector) where TSymbol : ISymbol => _allAvaloniaPropertyMethods
.Select(m => methodSymbolSelector(m).SingleOrDefault(p => p.Name == name))
.Where(p => p != null).Cast<TSymbol>()
.ToImmutableDictionary(p => (IMethodSymbol)p.ContainingSymbol, SymbolEqualityComparer<IMethodSymbol>.Default);
} }
private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes); private bool IsAvaloniaPropertyStorage(IFieldSymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes);
@ -91,13 +111,7 @@ public partial class AvaloniaPropertyAnalyzer
{ {
var current = namespaceStack.Pop(); var current = namespaceStack.Pop();
foreach (var type in current.GetTypeMembers()) types.AddRange(current.GetTypeMembers());
{
if (DerivesFrom(type, _avaloniaObjectType))
{
types.Add(type);
}
}
foreach (var child in current.GetNamespaceMembers()) foreach (var child in current.GetNamespaceMembers())
{ {
@ -114,6 +128,8 @@ public partial class AvaloniaPropertyAnalyzer
var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken };
var semanticModels = new ConcurrentDictionary<SyntaxTree, SemanticModel>();
Parallel.ForEach(types, parallelOptions, type => Parallel.ForEach(types, parallelOptions, type =>
{ {
try try
@ -133,38 +149,17 @@ public partial class AvaloniaPropertyAnalyzer
foreach (var constructor in type.StaticConstructors) foreach (var constructor in type.StaticConstructors)
{ {
foreach (var syntaxRef in constructor.DeclaringSyntaxReferences) foreach (var syntaxRef in constructor.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree)))
{ {
var node = syntaxRef.GetSyntax(cancellationToken); var (node, model) = GetNodeAndModel(syntaxRef);
if (!compilation.ContainsSyntaxTree(node.SyntaxTree))
{
continue;
}
var model = compilation.GetSemanticModel(node.SyntaxTree);
foreach (var descendant in node.DescendantNodes()) foreach (var descendant in node.DescendantNodes().Where(n => n.IsKind(SyntaxKind.SimpleAssignmentExpression)))
{ {
switch (descendant.Kind()) var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!;
{
case SyntaxKind.SimpleAssignmentExpression:
var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!;
var target = assignmentOperation.Target switch
{
IFieldReferenceOperation fieldRef => fieldRef.Field,
IPropertyReferenceOperation propertyRef => propertyRef.Property,
_ => default(ISymbol),
};
if (target == null)
{
break;
}
RegisterAssignment(target, assignmentOperation.Value); if (GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target)
{
break; RegisterAssignment(target, assignmentOperation.Value);
} }
} }
} }
@ -178,15 +173,10 @@ public partial class AvaloniaPropertyAnalyzer
Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol => Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol =>
{ {
foreach (var syntaxRef in symbol.DeclaringSyntaxReferences) foreach (var syntaxRef in symbol.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree)))
{ {
var node = syntaxRef.GetSyntax(cancellationToken); var (node, model) = GetNodeAndModel(syntaxRef);
if (!compilation.ContainsSyntaxTree(node.SyntaxTree))
{
continue;
}
var model = compilation.GetSemanticModel(node.SyntaxTree);
var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType<ISymbolInitializerOperation>().FirstOrDefault(); var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType<ISymbolInitializerOperation>().FirstOrDefault();
if (operation == null) if (operation == null)
@ -199,7 +189,7 @@ public partial class AvaloniaPropertyAnalyzer
}); });
// we have recorded every Register and AddOwner call. Now follow assignment chains. // we have recorded every Register and AddOwner call. Now follow assignment chains.
foreach (var root in fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray()) Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), root =>
{ {
var propertyDescription = propertyDescriptions[root]; var propertyDescription = propertyDescriptions[root];
var owner = propertyDescription.AssignedTo[root]; var owner = propertyDescription.AssignedTo[root];
@ -209,13 +199,13 @@ public partial class AvaloniaPropertyAnalyzer
{ {
var target = fieldInitializations[current]; var target = fieldInitializations[current];
propertyDescription.AddAssignment(target, owner); // This loop handles simple assignment operations, so do NOT change the owner propertyDescription.AddAssignment(target, new(owner.Type, target.Locations[0])); // This loop handles simple assignment operations, so do NOT change the owner type
propertyDescriptions[target] = propertyDescription; propertyDescriptions[target] = propertyDescription;
fieldInitializations.TryGetValue(target, out current); fieldInitializations.TryGetValue(target, out current);
} }
while (current != null); while (current != null);
} });
var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>(); var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>();
@ -251,7 +241,7 @@ public partial class AvaloniaPropertyAnalyzer
// convert our dictionaries to immutable form // convert our dictionaries to immutable form
_clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer<IPropertySymbol>.Default) _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer<IPropertySymbol>.Default)
.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.ToImmutableArray(), SymbolEqualityComparer<IPropertySymbol>.Default); .ToImmutableDictionary(g => g.Key, g => g.Distinct().ToImmutableArray(), SymbolEqualityComparer<IPropertySymbol>.Default);
_avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default); _avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default);
void RegisterAssignment(ISymbol target, IOperation value) void RegisterAssignment(ISymbol target, IOperation value)
@ -269,8 +259,12 @@ public partial class AvaloniaPropertyAnalyzer
break; break;
} }
} }
(SyntaxNode, SemanticModel) GetNodeAndModel(SyntaxReference syntaxRef) =>
(syntaxRef.GetSyntax(cancellationToken), semanticModels.GetOrAdd(syntaxRef.SyntaxTree, st => compilation.GetSemanticModel(st)));
} }
// This method handles registration of a new AvaloniaProperty, and calls to AddOwner.
private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary<ISymbol, AvaloniaPropertyDescription> propertyDescriptions) private void RegisterInitializer_Invocation(IInvocationOperation invocation, ISymbol target, ConcurrentDictionary<ISymbol, AvaloniaPropertyDescription> propertyDescriptions)
{ {
try try
@ -280,49 +274,38 @@ public partial class AvaloniaPropertyAnalyzer
return; return;
} }
if (_avaloniaPropertyRegisterMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AvaloniaProperty.Register* methods var originalMethod = invocation.TargetMethod.OriginalDefinition;
if (_avaloniaPropertyRegisterMethods.Contains(originalMethod)) // This is a call to one of the AvaloniaProperty.Register* methods
{ {
if (!invocation.TargetMethod.IsGenericMethod) TypeReference ownerTypeRef;
if (_ownerTypeParams.TryGetValue(originalMethod, out var ownerTypeParam))
{ {
return; ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam);
} }
else if (invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is { } ownerParam) // try extracting the runtime argument
var typeParamLookup = invocation.TargetMethod.TypeParameters.Select((s, i) => (param: s, index: i))
.ToDictionary(t => t.param.Name, t =>
{
var argument = invocation.TargetMethod.TypeArguments[t.index];
var typeArgumentSyntax = invocation.Syntax;
if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them
{
try
{
typeArgumentSyntax = invocation.Syntax.DescendantNodes()
.First(n => n.IsKind(SyntaxKind.TypeArgumentList))
.DescendantNodes().ElementAt(t.index);
}
catch
{
// ignore, this is unimportant
}
}
return new TypeReference((INamedTypeSymbol)argument, typeArgumentSyntax.GetLocation());
});
if (!typeParamLookup.TryGetValue("TOwner", out var ownerTypeRef) && // if it's NOT a generic parameter, try to work out the runtime value
invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is INamedTypeSymbol ownerTypeParam &&
invocation.Arguments.FirstOrDefault(a => SymbolEquals(a.Parameter, ownerTypeParam)) is IArgumentOperation argument)
{ {
switch (ResolveOperationSource(argument.Value)) switch (ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value))
{ {
case ITypeOfOperation typeOf when typeOf.Type is INamedTypeSymbol type: case ITypeOfOperation { Type: INamedTypeSymbol type } typeOf:
ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation()); ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation());
break; break;
default:
return;
} }
} }
else
{
return;
}
if (ownerTypeRef.Type == null || !typeParamLookup.TryGetValue("TValue", out var propertyValueTypeRef)) TypeReference valueTypeRef;
if (_valueTypeParams.TryGetValue(originalMethod, out var valueTypeParam))
{
valueTypeRef = TypeReference.FromInvocationTypeParameter(invocation, valueTypeParam);
}
else
{ {
return; return;
} }
@ -343,47 +326,69 @@ public partial class AvaloniaPropertyAnalyzer
return; return;
} }
var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueTypeRef.Type)); var inherits = false;
if (_inheritsParams.TryGetValue(originalMethod, out var inheritsParam) &&
invocation.Arguments[inheritsParam.Ordinal].Value is ILiteralOperation literalOp &&
literalOp.ConstantValue.Value is bool constValue)
{
inherits = constValue;
}
TypeReference? hostTypeRef = null;
if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType))
{
if (_hostTypeParams.TryGetValue(originalMethod, out var hostTypeParam))
{
hostTypeRef = TypeReference.FromInvocationTypeParameter(invocation, hostTypeParam);
}
else
{
hostTypeRef = new(_avaloniaObjectType, Location.None);
}
}
var description = propertyDescriptions.GetOrAdd(target, s => new AvaloniaPropertyDescription(name, propertyType, valueTypeRef.Type));
description.Name = name; description.Name = name;
description.HostType = hostTypeRef;
description.Inherits = inherits;
description.AddAssignment(target, ownerTypeRef); description.AddAssignment(target, ownerTypeRef);
description.AddOwner(ownerTypeRef); description.AddOwner(ownerTypeRef);
} }
else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods
{ {
if (invocation.TargetMethod.TypeArguments[0] is not INamedTypeSymbol ownerType) if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam) ||
invocation.TargetMethod.TypeArguments[ownerTypeParam.Ordinal] is not INamedTypeSymbol)
{ {
return; return;
} }
var ownerTypeRef = new TypeReference(ownerType, invocation.TargetMethod.TypeArguments[0].Locations[0]); if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol)
ISymbol sourceSymbol;
switch (invocation.Instance)
{ {
case IFieldReferenceOperation fieldReference: return;
sourceSymbol = fieldReference.Field;
break;
case IPropertyReferenceOperation propertyReference:
sourceSymbol = propertyReference.Property;
break;
default:
return;
} }
var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); var description = propertyDescriptions.GetOrAdd(sourceSymbol, s =>
var description = propertyDescriptions.GetOrAdd(target, s =>
{ {
string inferredName = target.Name; string inferredName = s.Name;
var match = Regex.Match(target.Name, "(?<name>.*)Property$"); var match = Regex.Match(s.Name, "(?<name>.*)Property$");
if (match.Success) if (match.Success)
{ {
inferredName = match.Groups["name"].Value; inferredName = match.Groups["name"].Value;
} }
return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType);
var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType);
TypeReference? hostTypeRef = null;
if (SymbolEquals(propertyType.OriginalDefinition, _attachedPropertyType))
{
hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration
}
return new AvaloniaPropertyDescription(inferredName, propertyType, propertyValueType) { HostType = hostTypeRef };
}); });
var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam);
description.AddAssignment(target, ownerTypeRef); description.AddAssignment(target, ownerTypeRef);
description.AddOwner(ownerTypeRef); description.AddOwner(ownerTypeRef);
} }
@ -458,7 +463,7 @@ public partial class AvaloniaPropertyAnalyzer
try try
{ {
var (target, isValid) = operation.Target switch var (target, isValid) = ResolveOperationSource(operation.Target) switch
{ {
IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)),
IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)),
@ -508,6 +513,58 @@ public partial class AvaloniaPropertyAnalyzer
} }
} }
/// <seealso cref="UnexpectedPropertyAccess"/>
/// <seealso cref="InappropriatePropertyRegistration"/>
private void AnalyzeMethodInvocation(OperationAnalysisContext context)
{
var invocation = (IInvocationOperation)context.Operation;
var originalMethod = invocation.TargetMethod.OriginalDefinition;
if (_allGetSetMethods.Contains(originalMethod))
{
var avaloniaPropertyOperation = invocation.Arguments[0].Value;
var propertyStorageSymbol = GetReferencedFieldOrProperty(ResolveOperationSource(avaloniaPropertyOperation));
if (propertyStorageSymbol == null || !_avaloniaPropertyDescriptions.TryGetValue(propertyStorageSymbol, out var propertyDescription))
{
return;
}
TypeReference ownerOrHostType;
if (SymbolEquals(propertyDescription.PropertyType.OriginalDefinition, _attachedPropertyType))
{
ownerOrHostType = propertyDescription.HostType!.Value;
}
else if (!propertyDescription.AssignedTo.TryGetValue(propertyStorageSymbol, out ownerOrHostType))
{
return;
}
if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } &&
!DerivesFrom(context.ContainingSymbol.ContainingType, ownerOrHostType.Type))
{
context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(),
GetReferencedFieldOrProperty(avaloniaPropertyOperation), context.ContainingSymbol.ContainingType));
}
}
else if (!IsStaticConstructorOrInitializer() && _allAvaloniaPropertyMethods.Contains(originalMethod))
{
context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(),
originalMethod.ToDisplayString(TypeQualifiedName)));
}
bool IsStaticConstructorOrInitializer() =>
context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.StaticConstructor } ||
ResolveOperationTarget(invocation.Parent!) switch
{
IFieldInitializerOperation fieldInit when fieldInit.InitializedFields.All(f => f.IsStatic) => true,
IPropertyInitializerOperation propInit when propInit.InitializedProperties.All(p => p.IsStatic) => true,
_ => false,
};
}
/// <seealso cref="AmbiguousPropertyName"/> /// <seealso cref="AmbiguousPropertyName"/>
/// <seealso cref="PropertyTypeMismatch"/> /// <seealso cref="PropertyTypeMismatch"/>
/// <seealso cref="AssociatedAvaloniaProperty"/> /// <seealso cref="AssociatedAvaloniaProperty"/>
@ -520,7 +577,7 @@ public partial class AvaloniaPropertyAnalyzer
{ {
if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties))
{ {
return; // does not refer to an AvaloniaProperty return; // does not refer to an AvaloniaProperty
} }
context.RegisterSymbolEndAction(context => context.RegisterSymbolEndAction(context =>

98
src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs

@ -6,6 +6,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Operations;
@ -18,7 +19,8 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
private const string TypeMismatchTag = "TypeMismatch"; private const string TypeMismatchTag = "TypeMismatch";
private const string NameCollisionTag = "NameCollision"; private const string NameCollisionTag = "NameCollision";
private const string AssociatedPropertyTag = "AssociatedProperty"; private const string AssociatedClrPropertyTag = "AssociatedClrProperty";
private const string InappropriateReadWriteTag = "InappropriateReadWrite";
private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new( private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new(
"AVP0001", "AVP0001",
@ -28,7 +30,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Info, DiagnosticSeverity.Info,
isEnabledByDefault: false, isEnabledByDefault: false,
"This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.", "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.",
AssociatedPropertyTag); AssociatedClrPropertyTag);
private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new(
"AVP1000", "AVP1000",
@ -39,9 +41,18 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true, isEnabledByDefault: true,
"AvaloniaProperty objects have static lifetimes and should be stored accordingly. Do not multiply construct the same property."); "AvaloniaProperty objects have static lifetimes and should be stored accordingly. Do not multiply construct the same property.");
private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new(
"AVP1001",
"Ensure that the same AvaloniaProperty cannot be registered twice",
"Unsafe registration: {0} should be called only in static constructors or static initializers",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
"AvaloniaProperty objects have static lifetimes and should be created only once. To ensure this, only call Register or AddOwner in static constructors or static initializers.");
private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new( private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new(
"AVP1010", "AVP1010",
"Avaloniaproperty objects should declare their owner to be the type in which they are stored", "AvaloniaProperty objects should be owned be the type in which they are stored",
"Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type", "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type",
Category, Category,
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
@ -49,6 +60,16 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
"The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.", "The owner of an AvaloniaProperty should generally be the containing type. This ensures that the property can be used as expected in XAML.",
TypeMismatchTag); TypeMismatchTag);
private static readonly DiagnosticDescriptor UnexpectedPropertyAccess = new(
"AVP1011",
"An AvaloniaObject should be the owner of each AvaloniaProperty it reads or writes on itself",
"Unexpected property use: {0} is neither owned by nor attached to {1}",
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
"It is possible to use any AvaloniaProperty with any AvaloniaObject. However, each AvaloniaProperty an object uses on itself should be either owned by that object, or attached to that object.",
InappropriateReadWriteTag);
private static readonly DiagnosticDescriptor DuplicatePropertyName = new( private static readonly DiagnosticDescriptor DuplicatePropertyName = new(
"AVP1020", "AVP1020",
"AvaloniaProperty names should be unique within each class", "AvaloniaProperty names should be unique within each class",
@ -87,7 +108,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
"The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, create a Coerce method or a PropertyChanged subscriber.", "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call any user CLR properties. To execute code before or after the property is set, create a Coerce method or a PropertyChanged subscriber.",
AssociatedPropertyTag); AssociatedClrPropertyTag);
private static readonly DiagnosticDescriptor MissingAccessor = new( private static readonly DiagnosticDescriptor MissingAccessor = new(
"AVP1031", "AVP1031",
@ -97,7 +118,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
"The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.", "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Not providing both CLR property accessors is ineffective.",
AssociatedPropertyTag); AssociatedClrPropertyTag);
private static readonly DiagnosticDescriptor InconsistentAccessibility = new( private static readonly DiagnosticDescriptor InconsistentAccessibility = new(
"AVP1032", "AVP1032",
@ -107,7 +128,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
"The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different acessibility from its associated AvaloniaProperty is ineffective.", "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. Defining a CLR property with different acessibility from its associated AvaloniaProperty is ineffective.",
AssociatedPropertyTag); AssociatedClrPropertyTag);
private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( private static readonly DiagnosticDescriptor PropertyTypeMismatch = new(
"AVP1040", "AVP1040",
@ -117,12 +138,18 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
"The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.", "The AvaloniaObject.GetValue and AvaloniaObject.SetValue methods are public, and do not call CLR properties on the owning type. A CLR property changing the value type (even when an implicit cast is possible) is ineffective and can lead to InvalidCastException to be thrown.",
TypeMismatchTag, AssociatedPropertyTag); TypeMismatchTag, AssociatedClrPropertyTag);
private static readonly SymbolDisplayFormat TypeQualifiedName = new(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
AssociatedAvaloniaProperty, AssociatedAvaloniaProperty,
InappropriatePropertyAssignment, InappropriatePropertyAssignment,
InappropriatePropertyRegistration,
OwnerDoesNotMatchOuterType, OwnerDoesNotMatchOuterType,
UnexpectedPropertyAccess,
DuplicatePropertyName, DuplicatePropertyName,
AmbiguousPropertyName, AmbiguousPropertyName,
PropertyNameMismatch, PropertyNameMismatch,
@ -193,6 +220,31 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
} }
} }
private static IOperation ResolveOperationTarget(IOperation operation)
{
while (true)
{
switch (operation)
{
case IConversionOperation conversion:
operation = conversion.Parent!;
break;
case ISimpleAssignmentOperation assignment:
operation = assignment.Target;
break;
default:
return operation;
}
}
}
private static ISymbol? GetReferencedFieldOrProperty(IOperation? operation) => operation == null ? null : ResolveOperationSource(operation) switch
{
IFieldReferenceOperation fieldRef => fieldRef.Field,
IPropertyReferenceOperation propertyRef => propertyRef.Property,
_ => null,
};
private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly; private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly;
private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly;
@ -228,12 +280,22 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
/// </summary> /// </summary>
public INamedTypeSymbol ValueType { get; } public INamedTypeSymbol ValueType { get; }
/// <summary>
/// Gets whether the value of this property is inherited from the parent AvaloniaObject.
/// </summary>
public bool Inherits { get; set; }
/// <summary> /// <summary>
/// Gets the type which registered the property, and all types which have added themselves as owners. /// Gets the type which registered the property, and all types which have added themselves as owners.
/// </summary> /// </summary>
public IReadOnlyCollection<TypeReference> OwnerTypes { get; private set; } public IReadOnlyCollection<TypeReference> OwnerTypes { get; private set; }
private ConcurrentBag<TypeReference>? _ownerTypes = new(); private ConcurrentBag<TypeReference>? _ownerTypes = new();
/// <summary>
/// Gets the type to which an AttachedProperty is attached, or null if the property is StyledProperty or DirectProperty.
/// </summary>
public TypeReference? HostType { get; set; }
/// <summary> /// <summary>
/// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment.
/// </summary> /// </summary>
@ -314,6 +376,28 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
Type = type; Type = type;
Location = location; Location = location;
} }
public static TypeReference FromInvocationTypeParameter(IInvocationOperation invocation, ITypeParameterSymbol typeParameter)
{
var argument = invocation.TargetMethod.TypeArguments[typeParameter.Ordinal];
var typeArgumentSyntax = invocation.Syntax;
if (invocation.Language == LanguageNames.CSharp) // type arguments do not appear in the invocation, so search the code for them
{
try
{
typeArgumentSyntax = invocation.Syntax.DescendantNodes()
.First(n => n.IsKind(SyntaxKind.TypeArgumentList))
.DescendantNodes().ElementAt(typeParameter.Ordinal);
}
catch
{
// ignore, this is just a nicety
}
}
return new TypeReference((INamedTypeSymbol)argument, typeArgumentSyntax.GetLocation());
}
} }
private class SymbolEqualityComparer<T> : IEqualityComparer<T> where T : ISymbol private class SymbolEqualityComparer<T> : IEqualityComparer<T> where T : ISymbol

Loading…
Cancel
Save