diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 018b1d6020..179105a1f5 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -31,6 +31,7 @@ public partial class AvaloniaPropertyAnalyzer private readonly INamedTypeSymbol _avaloniaObjectType; private readonly ImmutableHashSet _getValueMethods; private readonly ImmutableHashSet _setValueMethods; + private readonly ImmutableHashSet _allGetSetMethods; private readonly INamedTypeSymbol _avaloniaPropertyType; private readonly INamedTypeSymbol _styledPropertyType; private readonly INamedTypeSymbol _attachedPropertyType; @@ -38,6 +39,11 @@ public partial class AvaloniaPropertyAnalyzer private readonly ImmutableArray _allAvaloniaPropertyTypes; private readonly ImmutableHashSet _avaloniaPropertyRegisterMethods; private readonly ImmutableHashSet _avaloniaPropertyAddOwnerMethods; + private readonly ImmutableHashSet _allAvaloniaPropertyMethods; + private readonly ImmutableDictionary _ownerTypeParams; + private readonly ImmutableDictionary _valueTypeParams; + private readonly ImmutableDictionary _hostTypeParams; + private readonly ImmutableDictionary _inheritsParams; public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType) { @@ -47,6 +53,7 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaObjectType = avaloniaObjectType; _getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType().ToImmutableHashSet(methodComparer); _setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType().ToImmutableHashSet(methodComparer); + _allGetSetMethods = _getValueMethods.Concat(_setValueMethods).ToImmutableHashSet(methodComparer); _avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty"); _styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1"); @@ -61,11 +68,19 @@ public partial class AvaloniaPropertyAnalyzer _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer); + _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(SymbolEqualityComparer.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); context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer); context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer); context.RegisterOperationAction(AnalyzeAssignment, OperationKind.SimpleAssignment); + context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation); 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."); + + ImmutableDictionary GetParamDictionary(string name, Func> methodSymbolSelector) where TSymbol : ISymbol => _allAvaloniaPropertyMethods + .Select(m => methodSymbolSelector(m).SingleOrDefault(p => p.Name == name)) + .Where(p => p != null).Cast() + .ToImmutableDictionary(p => (IMethodSymbol)p.ContainingSymbol, SymbolEqualityComparer.Default); } 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(); - foreach (var type in current.GetTypeMembers()) - { - if (DerivesFrom(type, _avaloniaObjectType)) - { - types.Add(type); - } - } + types.AddRange(current.GetTypeMembers()); foreach (var child in current.GetNamespaceMembers()) { @@ -114,6 +128,8 @@ public partial class AvaloniaPropertyAnalyzer var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken }; + var semanticModels = new ConcurrentDictionary(); + Parallel.ForEach(types, parallelOptions, type => { try @@ -133,38 +149,17 @@ public partial class AvaloniaPropertyAnalyzer 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); - if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) - { - continue; - } - - var model = compilation.GetSemanticModel(node.SyntaxTree); + var (node, model) = GetNodeAndModel(syntaxRef); - foreach (var descendant in node.DescendantNodes()) + foreach (var descendant in node.DescendantNodes().Where(n => n.IsKind(SyntaxKind.SimpleAssignmentExpression))) { - switch (descendant.Kind()) - { - 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; - } + var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!; - RegisterAssignment(target, assignmentOperation.Value); - - break; + if (GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target) + { + RegisterAssignment(target, assignmentOperation.Value); } } } @@ -178,15 +173,10 @@ public partial class AvaloniaPropertyAnalyzer 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); - if (!compilation.ContainsSyntaxTree(node.SyntaxTree)) - { - continue; - } + var (node, model) = GetNodeAndModel(syntaxRef); - var model = compilation.GetSemanticModel(node.SyntaxTree); var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType().FirstOrDefault(); if (operation == null) @@ -199,7 +189,7 @@ public partial class AvaloniaPropertyAnalyzer }); // 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 owner = propertyDescription.AssignedTo[root]; @@ -209,13 +199,13 @@ public partial class AvaloniaPropertyAnalyzer { 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; fieldInitializations.TryGetValue(target, out current); } while (current != null); - } + }); var clrPropertyWrapCandidates = new ConcurrentBag<(IPropertySymbol, AvaloniaPropertyDescription)>(); @@ -251,7 +241,7 @@ public partial class AvaloniaPropertyAnalyzer // convert our dictionaries to immutable form _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, SymbolEqualityComparer.Default) - .ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.ToImmutableArray(), SymbolEqualityComparer.Default); + .ToImmutableDictionary(g => g.Key, g => g.Distinct().ToImmutableArray(), SymbolEqualityComparer.Default); _avaloniaPropertyDescriptions = propertyDescriptions.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.Seal(), SymbolEqualityComparer.Default); void RegisterAssignment(ISymbol target, IOperation value) @@ -269,8 +259,12 @@ public partial class AvaloniaPropertyAnalyzer 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 propertyDescriptions) { try @@ -280,49 +274,38 @@ public partial class AvaloniaPropertyAnalyzer 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); } - - 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) + else if (invocation.TargetMethod.Parameters.FirstOrDefault(p => p.Name == "ownerType") is { } ownerParam) // try extracting the runtime 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()); 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; } @@ -343,47 +326,69 @@ public partial class AvaloniaPropertyAnalyzer 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.HostType = hostTypeRef; + description.Inherits = inherits; description.AddAssignment(target, ownerTypeRef); description.AddOwner(ownerTypeRef); } 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; } - var ownerTypeRef = new TypeReference(ownerType, invocation.TargetMethod.TypeArguments[0].Locations[0]); - - ISymbol sourceSymbol; - switch (invocation.Instance) + if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol) { - case IFieldReferenceOperation fieldReference: - sourceSymbol = fieldReference.Field; - break; - case IPropertyReferenceOperation propertyReference: - sourceSymbol = propertyReference.Property; - break; - default: - return; + return; } - var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType); - - var description = propertyDescriptions.GetOrAdd(target, s => + var description = propertyDescriptions.GetOrAdd(sourceSymbol, s => { - string inferredName = target.Name; + string inferredName = s.Name; - var match = Regex.Match(target.Name, "(?.*)Property$"); + var match = Regex.Match(s.Name, "(?.*)Property$"); if (match.Success) { 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.AddOwner(ownerTypeRef); } @@ -458,7 +463,7 @@ public partial class AvaloniaPropertyAnalyzer try { - var (target, isValid) = operation.Target switch + var (target, isValid) = ResolveOperationSource(operation.Target) switch { IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)), IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)), @@ -508,6 +513,58 @@ public partial class AvaloniaPropertyAnalyzer } } + /// + /// + 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, + }; + } + /// /// /// @@ -520,7 +577,7 @@ public partial class AvaloniaPropertyAnalyzer { if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties)) { - return; // does not refer to an AvaloniaProperty + return; // does not refer to an AvaloniaProperty } context.RegisterSymbolEndAction(context => diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs index 0a59f43d6a..3b35e97810 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Runtime.Serialization; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -18,7 +19,8 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer private const string TypeMismatchTag = "TypeMismatch"; 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( "AVP0001", @@ -28,7 +30,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Info, isEnabledByDefault: false, "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new( "AVP1000", @@ -39,9 +41,18 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, "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( "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", Category, 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.", 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( "AVP1020", "AvaloniaProperty names should be unique within each class", @@ -87,7 +108,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, 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.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor MissingAccessor = new( "AVP1031", @@ -97,7 +118,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, 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.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor InconsistentAccessibility = new( "AVP1032", @@ -107,7 +128,7 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, 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.", - AssociatedPropertyTag); + AssociatedClrPropertyTag); private static readonly DiagnosticDescriptor PropertyTypeMismatch = new( "AVP1040", @@ -117,12 +138,18 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, 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.", - TypeMismatchTag, AssociatedPropertyTag); + TypeMismatchTag, AssociatedClrPropertyTag); + + private static readonly SymbolDisplayFormat TypeQualifiedName = new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + memberOptions: SymbolDisplayMemberOptions.IncludeContainingType); public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( AssociatedAvaloniaProperty, InappropriatePropertyAssignment, + InappropriatePropertyRegistration, OwnerDoesNotMatchOuterType, + UnexpectedPropertyAccess, DuplicatePropertyName, AmbiguousPropertyName, 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(IPropertySymbol field) => field.IsStatic && field.IsReadOnly; @@ -228,12 +280,22 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer /// public INamedTypeSymbol ValueType { get; } + /// + /// Gets whether the value of this property is inherited from the parent AvaloniaObject. + /// + public bool Inherits { get; set; } + /// /// Gets the type which registered the property, and all types which have added themselves as owners. /// public IReadOnlyCollection OwnerTypes { get; private set; } private ConcurrentBag? _ownerTypes = new(); + /// + /// Gets the type to which an AttachedProperty is attached, or null if the property is StyledProperty or DirectProperty. + /// + public TypeReference? HostType { get; set; } + /// /// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment. /// @@ -314,6 +376,28 @@ public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer Type = type; 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 : IEqualityComparer where T : ISymbol