diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf
index 741570061b..1d182b1357 100644
--- a/Avalonia.Desktop.slnf
+++ b/Avalonia.Desktop.slnf
@@ -8,9 +8,9 @@
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
+ "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
- "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
@@ -42,6 +42,7 @@
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
+ "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",
diff --git a/Avalonia.sln b/Avalonia.sln
index 525e01c891..1e8ee85ffb 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Analyzers", "src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj", "{C692FE73-43DB-49CE-87FC-F03ED61F25C9}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
EndProject
@@ -560,6 +567,10 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -626,6 +637,7 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
index 14e4f6a563..7d021d051f 100644
--- a/build/DevAnalyzers.props
+++ b/build/DevAnalyzers.props
@@ -5,5 +5,10 @@
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
+
diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props
index 3b14f0ce12..ac78d9c739 100644
--- a/samples/Directory.Build.props
+++ b/samples/Directory.Build.props
@@ -6,4 +6,5 @@
11
+
diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj
index e5c2fc6cf6..2d9331b5dc 100644
--- a/src/tools/DevAnalyzers/DevAnalyzers.csproj
+++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj
@@ -6,11 +6,11 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj
index 30da940514..7e63987d1b 100644
--- a/src/tools/DevGenerators/DevGenerators.csproj
+++ b/src/tools/DevGenerators/DevGenerators.csproj
@@ -7,11 +7,11 @@
-
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
+
diff --git a/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj
new file mode 100644
index 0000000000..31b8d08541
--- /dev/null
+++ b/src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ enable
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
new file mode 100644
index 0000000000..0a27602604
--- /dev/null
+++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
@@ -0,0 +1,795 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Avalonia.Analyzers;
+
+public partial class AvaloniaPropertyAnalyzer
+{
+ public class CompileAnalyzer
+ {
+ ///
+ /// A dictionary that maps field/property symbols to the AvaloniaProperty objects assigned to them.
+ ///
+ private ImmutableDictionary _avaloniaPropertyDescriptions = null!;
+
+ ///
+ /// Maps properties onto all AvaloniaProperty objects that they may be intended to represent.
+ ///
+ private ImmutableDictionary> _clrPropertyToAvaloniaProperties = null!;
+
+ private readonly INamedTypeSymbol _stringType;
+ 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;
+ private readonly INamedTypeSymbol _directPropertyType;
+ private readonly INamedTypeSymbol? _userControlType;
+ private readonly INamedTypeSymbol? _topLevelType;
+ private readonly ImmutableHashSet _allAvaloniaPropertyTypes;
+ private readonly ImmutableDictionary _propertyValueTypeParams;
+ 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;
+ private readonly ImmutableDictionary _ownerParams;
+
+ public CompileAnalyzer(CompilationStartAnalysisContext context, INamedTypeSymbol avaloniaObjectType)
+ {
+ var methodComparer = SymbolEqualityComparer.Default;
+
+ _stringType = GetTypeOrThrow("System.String");
+ _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");
+ _attachedPropertyType = GetTypeOrThrow("Avalonia.AttachedProperty`1");
+ _directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2");
+
+ _userControlType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.UserControl");
+ _topLevelType = context.Compilation.GetTypeByMetadataName("Avalonia.Controls.TopLevel");
+
+ _avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers()
+ .OfType().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet(methodComparer);
+
+ _allAvaloniaPropertyTypes = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }.ToImmutableHashSet(SymbolEqualityComparer.Default);
+
+ _propertyValueTypeParams = _allAvaloniaPropertyTypes.Select(p => p.TypeParameters.First(t => t.Name == "TValue"))
+ .Where(p => p != null).Cast()
+ .ToImmutableDictionary(p => p.ContainingType, SymbolEqualityComparer.Default);
+
+ _avaloniaPropertyAddOwnerMethods = _allAvaloniaPropertyTypes
+ .SelectMany(t => t.GetMembers("AddOwner").OfType()).ToImmutableHashSet(methodComparer);
+
+ _allAvaloniaPropertyMethods = _avaloniaPropertyRegisterMethods.Concat(_avaloniaPropertyAddOwnerMethods).ToImmutableHashSet(methodComparer);
+
+ _ownerTypeParams = GetParamDictionary("TOwner", m => m.TypeParameters);
+ _valueTypeParams = GetParamDictionary("TValue", m => m.TypeParameters);
+ _hostTypeParams = GetParamDictionary("THost", m => m.TypeParameters);
+ _inheritsParams = GetParamDictionary("inherits", m => m.Parameters);
+ _ownerParams = GetParamDictionary("ownerType", m => m.Parameters);
+
+ RegisterAvaloniaPropertySymbols(context.Compilation, context.CancellationToken);
+
+ context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer);
+ context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer);
+ context.RegisterOperationAction(AnalyzePropertyStorageAssignment, OperationKind.SimpleAssignment);
+ context.RegisterOperationAction(AnalyzePropertyWrapperAssignment, OperationKind.SimpleAssignment);
+ context.RegisterOperationAction(AnalyzeMethodInvocation, OperationKind.Invocation);
+
+ context.RegisterSymbolAction(AnalyzeWrapperCrlProperty, SymbolKind.Property);
+
+ if (context.Compilation.Language == LanguageNames.CSharp)
+ {
+ context.RegisterCodeBlockAction(AnalyzePropertyMethods);
+ }
+
+ 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);
+ private bool IsAvaloniaPropertyStorage(IPropertySymbol symbol) => symbol.Type is INamedTypeSymbol namedType && IsAvaloniaPropertyType(namedType, _allAvaloniaPropertyTypes);
+
+ private void RegisterAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken)
+ {
+ var namespaceStack = new Stack();
+ namespaceStack.Push(compilation.GlobalNamespace);
+
+ var types = new List();
+
+ while (namespaceStack.Count > 0)
+ {
+ var current = namespaceStack.Pop();
+
+ types.AddRange(current.GetTypeMembers());
+
+ foreach (var child in current.GetNamespaceMembers())
+ {
+ namespaceStack.Push(child);
+ }
+ }
+
+ var avaloniaPropertyStorageSymbols = new ConcurrentBag();
+
+ var propertyDescriptions = new ConcurrentDictionary(SymbolEqualityComparer.Default);
+
+ // key initializes value
+ var fieldInitializations = new ConcurrentDictionary(SymbolEqualityComparer.Default);
+
+ var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken };
+
+ var semanticModels = new ConcurrentDictionary();
+
+ Parallel.ForEach(types, parallelOptions, type =>
+ {
+ try
+ {
+ foreach (var member in type.GetMembers())
+ {
+ switch (member)
+ {
+ case IFieldSymbol fieldSymbol when IsAvaloniaPropertyStorage(fieldSymbol):
+ avaloniaPropertyStorageSymbols.Add(fieldSymbol);
+ break;
+ case IPropertySymbol propertySymbol when IsAvaloniaPropertyStorage(propertySymbol):
+ avaloniaPropertyStorageSymbols.Add(propertySymbol);
+ break;
+ }
+ }
+
+ foreach (var constructor in type.StaticConstructors)
+ {
+ foreach (var syntaxRef in constructor.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree)))
+ {
+ var (node, model) = GetNodeAndModel(syntaxRef);
+
+ foreach (var descendant in node.DescendantNodes().Where(n => n.IsKind(SyntaxKind.SimpleAssignmentExpression)))
+ {
+ var assignmentOperation = (IAssignmentOperation)model.GetOperation(descendant, cancellationToken)!;
+
+ if (GetReferencedFieldOrProperty(assignmentOperation.Target) is { } target)
+ {
+ RegisterAssignment(target, assignmentOperation.Value);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ throw new AvaloniaAnalysisException($"Failed to find AvaloniaProperty objects in {type}.", ex);
+ }
+ });
+
+ Parallel.ForEach(avaloniaPropertyStorageSymbols, parallelOptions, symbol =>
+ {
+ foreach (var syntaxRef in symbol.DeclaringSyntaxReferences.Where(sr => compilation.ContainsSyntaxTree(sr.SyntaxTree)))
+ {
+ var (node, model) = GetNodeAndModel(syntaxRef);
+
+ var operation = node.ChildNodes().Select(n => model.GetOperation(n, cancellationToken)).OfType().FirstOrDefault();
+
+ if (operation == null)
+ {
+ return;
+ }
+
+ RegisterAssignment(symbol, operation.Value);
+ }
+ });
+
+ // we have recorded every Register and AddOwner call. Now follow assignment chains.
+ Parallel.ForEach(fieldInitializations.Keys.Intersect(propertyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray(), root =>
+ {
+ var propertyDescription = propertyDescriptions[root];
+ var owner = propertyDescription.AssignedTo[root];
+
+ var current = root;
+ do
+ {
+ var target = fieldInitializations[current];
+
+ propertyDescription.SetAssignment(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)>();
+
+ var propertyDescriptionsByName = propertyDescriptions.Values.ToLookup(p => p.Name, p => (property: p, owners: p.OwnerTypes.Select(t => t.Type).ToImmutableHashSet(SymbolEqualityComparer.Default)));
+
+ // Detect CLR properties that provide syntatic wrapping around an AvaloniaProperty (or potentially multiple, which leads to a warning diagnostic)
+ Parallel.ForEach(propertyDescriptions.Values, propertyDescription =>
+ {
+ var nameMatches = propertyDescriptionsByName[propertyDescription.Name];
+
+ foreach (var ownerType in propertyDescription.OwnerTypes.Select(o => o.Type).Distinct(SymbolEqualityComparer.Default))
+ {
+ if (ownerType.GetMembers(propertyDescription.Name).OfType().SingleOrDefault() is not { IsStatic: false } clrProperty)
+ {
+ continue;
+ }
+
+ propertyDescription.AddPropertyWrapper(clrProperty);
+ clrPropertyWrapCandidates.Add((clrProperty, propertyDescription));
+
+ var current = ownerType.BaseType;
+ while (current != null)
+ {
+ foreach (var otherProp in nameMatches.Where(t => t.owners.Contains(current)).Select(t => t.property))
+ {
+ clrPropertyWrapCandidates.Add((clrProperty, otherProp));
+ }
+
+ current = current.BaseType;
+ }
+ }
+ });
+
+ // convert our dictionaries to immutable form
+ _clrPropertyToAvaloniaProperties = clrPropertyWrapCandidates.ToLookup(t => t.Item1, t => t.Item2, 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)
+ {
+ switch (ResolveOperationSource(value))
+ {
+ case IInvocationOperation invocation:
+ RegisterInitializer_Invocation(invocation, target, propertyDescriptions);
+ break;
+ case IFieldReferenceOperation fieldRef when IsAvaloniaPropertyStorage(fieldRef.Field):
+ fieldInitializations[fieldRef.Field] = target;
+ break;
+ case IPropertyReferenceOperation propRef when IsAvaloniaPropertyStorage(propRef.Property):
+ fieldInitializations[propRef.Property] = target;
+ 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
+ {
+ if (invocation.TargetMethod.ReturnType is not INamedTypeSymbol propertyType)
+ {
+ return;
+ }
+
+ var originalMethod = invocation.TargetMethod.OriginalDefinition;
+
+ if (_avaloniaPropertyRegisterMethods.Contains(originalMethod)) // This is a call to one of the AvaloniaProperty.Register* methods
+ {
+ TypeReference ownerTypeRef;
+
+ if (_ownerTypeParams.TryGetValue(originalMethod, out var ownerTypeParam))
+ {
+ ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam);
+ }
+ else if (_ownerParams.TryGetValue(originalMethod, out var ownerParam) && // try extracting the runtime argument
+ ResolveOperationSource(invocation.Arguments[ownerParam.Ordinal].Value) is ITypeOfOperation { Type: ITypeSymbol type } typeOf)
+ {
+ ownerTypeRef = new TypeReference(type, typeOf.Syntax.GetLocation());
+ }
+ else
+ {
+ return;
+ }
+
+ TypeReference valueTypeRef;
+ if (_valueTypeParams.TryGetValue(originalMethod, out var valueTypeParam))
+ {
+ valueTypeRef = TypeReference.FromInvocationTypeParameter(invocation, valueTypeParam);
+ }
+ else
+ {
+ return;
+ }
+
+ string name;
+ switch (ResolveOperationSource(invocation.Arguments[0].Value))
+ {
+ case ILiteralOperation literal when SymbolEquals(literal.Type, _stringType):
+ name = (string)literal.ConstantValue.Value!;
+ break;
+ case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference:
+ name = propertyReference.Property.Name;
+ break;
+ case IFieldReferenceOperation fieldRef when SymbolEquals(fieldRef.Type, _stringType) && fieldRef.ConstantValue is { HasValue: true } constantValue:
+ name = (string)fieldRef.ConstantValue.Value!;
+ break;
+ default:
+ return;
+ }
+
+ 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.SetAssignment(target, ownerTypeRef);
+ description.AddOwner(ownerTypeRef);
+ }
+ else if (_avaloniaPropertyAddOwnerMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AddOwner methods
+ {
+ if (!_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var ownerTypeParam))
+ {
+ return;
+ }
+
+ if (GetReferencedFieldOrProperty(invocation.Instance) is not { } sourceSymbol)
+ {
+ return;
+ }
+
+ var description = propertyDescriptions[target] = propertyDescriptions.GetOrAdd(sourceSymbol, s =>
+ {
+ string inferredName = s.Name;
+
+ var match = Regex.Match(s.Name, "(?.*)Property$");
+ if (match.Success)
+ {
+ inferredName = match.Groups["name"].Value;
+ }
+
+ if (!_propertyValueTypeParams.TryGetValue(propertyType.OriginalDefinition, out var propertyValueType))
+ {
+ throw new InvalidOperationException($"{propertyType} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType}).");
+ }
+
+ var valueType = propertyType.TypeArguments[propertyValueType.Ordinal];
+
+ 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
+ }
+
+ var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef };
+
+ // assume that the property is owned by its containing type at the point of assignment, until we find its registration
+ result.SetAssignment(s, new(s.ContainingType, Location.None));
+
+ return result;
+ });
+
+ var ownerTypeRef = TypeReference.FromInvocationTypeParameter(invocation, ownerTypeParam);
+ description.SetAssignment(target, ownerTypeRef);
+ description.AddOwner(ownerTypeRef);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new AvaloniaAnalysisException($"Failed to register the initializer of '{target}'.", ex);
+ }
+ }
+
+ ///
+ private void AnalyzeFieldInitializer(OperationAnalysisContext context)
+ {
+ var operation = (IFieldInitializerOperation)context.Operation;
+
+ foreach (var field in operation.InitializedFields)
+ {
+ try
+ {
+ if (!_avaloniaPropertyDescriptions.TryGetValue(field, out var description))
+ {
+ continue;
+ }
+
+ if (!IsValidAvaloniaPropertyStorage(field))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, field.Locations[0], field));
+ }
+
+ AnalyzeInitializer_Shared(context, field, description);
+
+ }
+ catch (Exception ex)
+ {
+ throw new AvaloniaAnalysisException($"Failed to process initialization of field '{field}'.", ex);
+ }
+ }
+ }
+
+ ///
+ private void AnalyzePropertyInitializer(OperationAnalysisContext context)
+ {
+ var operation = (IPropertyInitializerOperation)context.Operation;
+
+ foreach (var property in operation.InitializedProperties)
+ {
+ try
+ {
+ if (!_avaloniaPropertyDescriptions.TryGetValue(property, out var description))
+ {
+ continue;
+ }
+
+ if (!IsValidAvaloniaPropertyStorage(property))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, property.Locations[0], property));
+ }
+
+ AnalyzeInitializer_Shared(context, property, description);
+ }
+ catch (Exception ex)
+ {
+ throw new AvaloniaAnalysisException($"Failed to process initialization of property '{property}'.", ex);
+ }
+ }
+ }
+
+ ///
+ private void AnalyzePropertyStorageAssignment(OperationAnalysisContext context)
+ {
+ var operation = (IAssignmentOperation)context.Operation;
+
+ try
+ {
+ var (target, isValid) = ResolveOperationSource(operation.Target) switch
+ {
+ IFieldReferenceOperation fieldRef => (fieldRef.Field, IsValidAvaloniaPropertyStorage(fieldRef.Field)),
+ IPropertyReferenceOperation propertyRef => (propertyRef.Property, IsValidAvaloniaPropertyStorage(propertyRef.Property)),
+ _ => (default(ISymbol), false),
+ };
+
+ if (target == null || !_avaloniaPropertyDescriptions.TryGetValue(target, out var description))
+ {
+ return;
+ }
+
+ if (!isValid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyAssignment, target.Locations[0], target));
+ }
+
+ AnalyzeInitializer_Shared(context, target, description);
+ }
+ catch (Exception ex)
+ {
+ throw new AvaloniaAnalysisException($"Failed to process assignment '{operation}'.", ex);
+ }
+ }
+
+ ///
+ ///
+ private void AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description)
+ {
+ if (!assignmentSymbol.Name.Contains(description.Name) && assignmentSymbol.DeclaredAccessibility != Accessibility.Private)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(PropertyNameMismatch, assignmentSymbol.Locations[0],
+ description.Name, assignmentSymbol));
+ }
+
+ try
+ {
+ var ownerType = description.AssignedTo[assignmentSymbol];
+
+ if (ownerType.Type.TypeKind != TypeKind.Error &&
+ !IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) &&
+ !SymbolEquals(ownerType.Type, assignmentSymbol.ContainingType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, ownerType.Location, ownerType.Type));
+ }
+ }
+ catch (KeyNotFoundException)
+ {
+ throw new KeyNotFoundException($"Assignment operation for {assignmentSymbol} was not recorded.");
+ }
+ }
+
+ ///
+ private void AnalyzePropertyWrapperAssignment(OperationAnalysisContext context)
+ {
+ var operation = (IAssignmentOperation)context.Operation;
+
+ if (ResolveOperationSource(operation) is IParameterReferenceOperation && context.ContainingSymbol is IMethodSymbol { MethodKind: MethodKind.Constructor })
+ {
+ // We can consider `new MyType(myValue)` functionally equivalent to `new MyType() { Value = myValue }`. Both set a local value with an external parameter.
+ return;
+ }
+
+ if (ResolveOperationTarget(operation) is IPropertyReferenceOperation propertyRef &&
+ propertyRef.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } &&
+ _clrPropertyToAvaloniaProperties.TryGetValue(propertyRef.Property, out var propertyDescriptions) &&
+ propertyDescriptions.Any(p => !SymbolEquals(p.PropertyType.OriginalDefinition, _directPropertyType)))
+ {
+ if (DerivesFrom(propertyRef.Instance.Type, _userControlType) || DerivesFrom(propertyRef.Instance.Type, _topLevelType))
+ {
+ // Special case: don't warn about local value assignment on a UserControl or TopLevel type.
+ // 1. We don't want to annoy new users, who start with these two types and don't understand binding priorities yet
+ // 2. Such controls either have no consumers, or are treated largely as a black box (i.e. no styles setting dynamic values)
+ return;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(SettingOwnStyledPropertyValue, operation.Syntax.GetLocation()));
+ }
+ }
+
+ ///
+ ///
+ ///
+ private void AnalyzeMethodInvocation(OperationAnalysisContext context)
+ {
+ var invocation = (IInvocationOperation)context.Operation;
+
+ var originalMethod = invocation.TargetMethod.OriginalDefinition;
+
+ if (_allGetSetMethods.Contains(originalMethod))
+ {
+ if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } &&
+ GetReferencedProperty(invocation.Arguments[0]) is { } refProp &&
+ refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) &&
+ !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) &&
+ !DerivesFrom(context.ContainingSymbol.ContainingType, refProp.description.HostType?.Type))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(UnexpectedPropertyAccess, invocation.Arguments[0].Syntax.GetLocation(),
+ refProp.storageSymbol, context.ContainingSymbol.ContainingType));
+ }
+ }
+ else if (_allAvaloniaPropertyMethods.Contains(originalMethod))
+ {
+ if (!IsStaticConstructorOrInitializer())
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InappropriatePropertyRegistration, invocation.Syntax.GetLocation(),
+ originalMethod.ToDisplayString(TypeQualifiedName)));
+ }
+
+ if (_ownerTypeParams.TryGetValue(invocation.TargetMethod.OriginalDefinition, out var typeParam) &&
+ invocation.TargetMethod.TypeArguments[typeParam.Ordinal] is { } newOwnerType)
+ {
+ if (newOwnerType is INamedTypeSymbol { IsGenericType: true })
+ {
+ context.ReportDiagnostic(Diagnostic.Create(PropertyOwnedByGenericType, TypeReference.FromInvocationTypeParameter(invocation, typeParam).Location));
+ }
+
+ if (_avaloniaPropertyAddOwnerMethods.Contains(originalMethod) && GetReferencedProperty(invocation.Instance!) is { } refProp)
+ {
+ var ownerMatches = refProp.description.AssignedTo.Where(kvp => !SymbolEquals(kvp.Key, context.ContainingSymbol) && DerivesFrom(newOwnerType, kvp.Value.Type)).ToArray();
+
+ if (ownerMatches.Any())
+ {
+ var ownerMatchesExceptBaseTypes = ownerMatches.Where(m => !DerivesFrom(context.ContainingSymbol.ContainingType, m.Key.ContainingType, includeSelf: false)).ToArray();
+ var routesMessage = ownerMatchesExceptBaseTypes.Length switch
+ {
+ 0 => "its base type",
+ 1 => ownerMatchesExceptBaseTypes.Single().Key.ToString(),
+ _ => $"{ownerMatches.Length} routes\n\t{string.Join("\n\t", ownerMatches.Select(kvp => kvp.Key))}"
+ };
+
+ context.ReportDiagnostic(Diagnostic.Create(SuperfluousAddOwnerCall, invocation.Syntax.GetLocation(), ownerMatches.Select(kvp => kvp.Value.Location),
+ newOwnerType, refProp.storageSymbol, routesMessage));
+ }
+ }
+ }
+ }
+
+ 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,
+ };
+ }
+
+ private (AvaloniaPropertyDescription description, ISymbol storageSymbol)? GetReferencedProperty(IOperation operation)
+ {
+ if (GetReferencedFieldOrProperty(operation) is { } storageSymbol && _avaloniaPropertyDescriptions.TryGetValue(storageSymbol, out var result))
+ {
+ return (result, storageSymbol);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void AnalyzeWrapperCrlProperty(SymbolAnalysisContext context)
+ {
+ var property = (IPropertySymbol)context.Symbol;
+
+ if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties))
+ {
+ return; // does not refer to an AvaloniaProperty
+ }
+
+ try
+ {
+ if (candidateTargetProperties.Length > 1)
+ {
+ var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null).OrderBy(s => s!.Name);
+ context.ReportDiagnostic(Diagnostic.Create(AmbiguousPropertyName, property.Locations[0], candidateSymbols.SelectMany(s => s!.Locations),
+ property.ContainingType, property.Name, $"\n\t{string.Join("\n\t", candidateSymbols)}"));
+ return;
+ }
+
+ var avaloniaPropertyDescription = candidateTargetProperties[0];
+ var avaloniaPropertyStorage = avaloniaPropertyDescription.ClosestAssignmentFor(property.ContainingType);
+
+ if (avaloniaPropertyStorage == null)
+ {
+ return;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(AssociatedAvaloniaProperty, property.Locations[0], new[] { avaloniaPropertyStorage.Locations[0] },
+ avaloniaPropertyDescription.PropertyType.Name, avaloniaPropertyStorage));
+
+ if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType, includeNullability: true))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(PropertyTypeMismatch, property.Locations[0],
+ avaloniaPropertyStorage, $"\t\n{string.Join("\t\n", avaloniaPropertyDescription.ValueType, property.Type)}"));
+ }
+
+ if (property.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, property.Locations[0], "property", avaloniaPropertyStorage));
+ }
+
+ VerifyAccessor(property.GetMethod, "readable", "get");
+
+ if (!IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType))
+ {
+ VerifyAccessor(property.SetMethod, "writeable", "set");
+ }
+
+ void VerifyAccessor(IMethodSymbol? method, string verb, string methodName)
+ {
+ if (method == null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName));
+ }
+ else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(InconsistentAccessibility, method.Locations[0], "property accessor", avaloniaPropertyStorage));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new AvaloniaAnalysisException($"Failed to analyse wrapper property '{property}'.", ex);
+ }
+ }
+
+ ///
+ private void AnalyzePropertyMethods(CodeBlockAnalysisContext context)
+ {
+ if (context.OwningSymbol is not IMethodSymbol { AssociatedSymbol: IPropertySymbol property } method)
+ {
+ return;
+ }
+
+ try
+ {
+ if (!_clrPropertyToAvaloniaProperties.TryGetValue(property, out var candidateTargetProperties) ||
+ candidateTargetProperties.Length != 1) // a diagnostic about multiple candidates will have already been reported
+ {
+ return;
+ }
+
+ var avaloniaPropertyDescription = candidateTargetProperties.Single();
+
+ if (IsAvaloniaPropertyType(avaloniaPropertyDescription.PropertyType, _directPropertyType))
+ {
+ return;
+ }
+
+ if (!SymbolEquals(property.Type, avaloniaPropertyDescription.ValueType))
+ {
+ return; // a diagnostic about this will have already been reported, and if the cast is implicit then this message would be confusing anyway
+ }
+
+ var bodyNode = context.CodeBlock.ChildNodes().Single();
+
+ var operation = bodyNode.DescendantNodes()
+ .Where(n => n.IsKind(SyntaxKind.InvocationExpression)) // this line is specific to C#
+ .Select(n => (IInvocationOperation)context.SemanticModel.GetOperation(n)!)
+ .FirstOrDefault();
+
+ var isGetMethod = method.MethodKind == MethodKind.PropertyGet;
+
+ var expectedInvocations = isGetMethod ? _getValueMethods : _setValueMethods;
+
+ if (operation == null || bodyNode.ChildNodes().Count() != 1 || !expectedInvocations.Contains(operation.TargetMethod.OriginalDefinition))
+ {
+ ReportSideEffects();
+ return;
+ }
+
+ if (operation.Arguments.Length != 0)
+ {
+ switch (ResolveOperationSource(operation.Arguments[0].Value))
+ {
+ case IFieldReferenceOperation fieldRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(fieldRef.Field):
+ case IPropertyReferenceOperation propertyRef when avaloniaPropertyDescription.AssignedTo.ContainsKey(propertyRef.Property):
+ break; // the argument is a reference to the correct AvaloniaProperty object
+ default:
+ ReportSideEffects(operation.Arguments[0].Value.Syntax.GetLocation());
+ return;
+ }
+ }
+
+ if (!isGetMethod &&
+ operation.Arguments.Length >= 2 &&
+ operation.Arguments[1].Value.Kind != OperationKind.ParameterReference) // passing something other than `value` to SetValue
+ {
+ ReportSideEffects(operation.Arguments[1].Syntax.GetLocation());
+ }
+
+ void ReportSideEffects(Location? locationOverride = null)
+ {
+ var propertySourceName = avaloniaPropertyDescription.ClosestAssignmentFor(method.ContainingType)?.Name ?? "[unknown]";
+
+ context.ReportDiagnostic(Diagnostic.Create(AccessorSideEffects, locationOverride ?? context.CodeBlock.GetLocation(),
+ avaloniaPropertyDescription.Name,
+ isGetMethod ? "read" : "written to",
+ isGetMethod ? "get" : "set",
+ isGetMethod ? $"GetValue({propertySourceName})" : $"SetValue({propertySourceName}, value)"));
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new AvaloniaAnalysisException($"Failed to process property accessor '{method}'.", ex);
+ }
+ }
+ }
+}
diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs
new file mode 100644
index 0000000000..d1d9071d17
--- /dev/null
+++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs
@@ -0,0 +1,465 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.Serialization;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Avalonia.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
+public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
+{
+ private const string Category = "AvaloniaProperty";
+
+ private const string TypeMismatchTag = "TypeMismatch";
+ private const string NameCollisionTag = "NameCollision";
+ private const string AssociatedClrPropertyTag = "AssociatedClrProperty";
+ private const string InappropriateReadWriteTag = "InappropriateReadWrite";
+
+ private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new(
+ "AVP0001",
+ "Identification of the AvaloniaProperty associated with a CLR property",
+ "Associated AvaloniaProperty: {0} {1}",
+ Category,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: false,
+ "This informational diagnostic identifies which AvaloniaProperty a CLR property is associated with.",
+ AssociatedClrPropertyTag);
+
+ private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new(
+ "AVP1000",
+ "AvaloniaProperty objects should be stored appropriately",
+ "Incorrect AvaloniaProperty storage: {0} should be static and readonly",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "AvaloniaProperty objects have static lifetimes and should be stored accordingly.");
+
+ private static readonly DiagnosticDescriptor InappropriatePropertyRegistration = new(
+ "AVP1001",
+ "The same AvaloniaProperty should not 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 PropertyOwnedByGenericType = new(
+ "AVP1002",
+ "AvaloniaProperty objects should not be owned by a generic type",
+ "Inadvisable registration: Generic types cannot be referenced from XAML. Create a non-generic type to be the owner of this AvaloniaProperty.",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "It is sometimes necessary to refer to an AvaloniaProperty in XAML by providing its class name. This cannot be achieved if property's owner is a generic type." +
+ " Additionally, a new AvaloniaProperty object will be generated each time a new version of the generic owner type is constructed, which may be unexpected.");
+
+ private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new(
+ "AVP1010",
+ "AvaloniaProperty objects should be owned by the type in which they are stored",
+ "Type mismatch: AvaloniaProperty owner is {0}, which is not the containing type",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "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 own 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 SettingOwnStyledPropertyValue = new(
+ "AVP1012",
+ "An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values",
+ "Inappropriate assignment: An AvaloniaObject should use SetCurrentValue when setting its own StyledProperty or AttachedProperty values",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "The standard means of setting an AvaloniaProperty is to call the SetValue method (often via a CLR property setter). This will forcibly overwrite values from sources like styles and templates, " +
+ "which is something that should only be done by consumers of the control, not the control itself. Controls which want to set their own values should instead call the SetCurrentValue method, or " +
+ "refactor the property into a DirectProperty. An assignment is exempt from this diagnostic in two scenarios: when it is forwarding a constructor parameter, and when the target object is derived " +
+ "from UserControl or TopLevel.",
+ InappropriateReadWriteTag);
+
+ private static readonly DiagnosticDescriptor SuperfluousAddOwnerCall = new(
+ "AVP1013",
+ "AvaloniaProperty owners should not be added superfluously",
+ "Superfluous owner: {0} is already an owner of {1} via {2}",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "Ownership of an AvaloniaProperty is inherited along the type hierarchy. There is no need for a derived type to assert ownership over a base type's properties. This diagnostic can be a symptom of an incorrect property owner elsewhere.",
+ InappropriateReadWriteTag);
+
+ private static readonly DiagnosticDescriptor DuplicatePropertyName = new(
+ "AVP1020",
+ "AvaloniaProperty names should be unique within each class",
+ "Name collision: {0} has the same name as {1}",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "Querying for an AvaloniaProperty by name requires that each property associated with a type have a unique name.",
+ NameCollisionTag);
+
+ private static readonly DiagnosticDescriptor AmbiguousPropertyName = new(
+ "AVP1021",
+ "There should be an unambiguous relationship between the CLR properties and Avalonia properties of a class",
+ "Name collision: {0} owns multiple Avalonia properties with the name '{1}' {2}",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "It is unclear which AvaloniaProperty this CLR property refers to. Ensure that each AvaloniaProperty associated with a type has a unique name. If you need to change behaviour of a base property in your class, call its OverrideMetadata or OverrideDefaultValue methods.",
+ NameCollisionTag);
+
+ private static readonly DiagnosticDescriptor PropertyNameMismatch = new(
+ "AVP1022",
+ "An AvaloniaProperty object should be stored in a field or CLR property which reflects its name",
+ "Bad name: An AvaloniaProperty named '{0}' is being assigned to {1}. These names do not relate.",
+ Category,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ "An AvaloniaProperty should be stored in a field or property which contains its name. For example, a property named \"Brush\" should be assigned to a field called \"BrushProperty\".\nPrivate symbols are exempt from this diagnostic.",
+ NameCollisionTag);
+
+ private static readonly DiagnosticDescriptor AccessorSideEffects = new(
+ "AVP1030",
+ "StyledProperty accessors should not have side effects",
+ "Side effects: '{0}' is an AvaloniaProperty which can be {1} without the use of this CLR property. This {2} accessor should do nothing except call {3}.",
+ Category,
+ 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, consider: 1) adding a Coercion method, b) adding a static observer with AvaloniaProperty.Changed.AddClassHandler, and/or c) overriding the AvaloniaObject.OnPropertyChanged method.",
+ AssociatedClrPropertyTag);
+
+ private static readonly DiagnosticDescriptor MissingAccessor = new(
+ "AVP1031",
+ "A CLR property should support the same get/set operations as its associated AvaloniaProperty",
+ "Missing accessor: {0} is {1}, but this CLR property lacks a {2} accessor",
+ Category,
+ 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.",
+ AssociatedClrPropertyTag);
+
+ private static readonly DiagnosticDescriptor InconsistentAccessibility = new(
+ "AVP1032",
+ "A CLR property and its accessors should be equally accessible as its associated AvaloniaProperty",
+ "Inconsistent accessibility: CLR {0} accessibility does not match accessibility of {1}",
+ Category,
+ 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 accessibility from its associated AvaloniaProperty is ineffective.",
+ AssociatedClrPropertyTag);
+
+ private static readonly DiagnosticDescriptor PropertyTypeMismatch = new(
+ "AVP1040",
+ "A CLR property type should match the associated AvaloniaProperty type",
+ "Type mismatch: CLR property type differs from the value type of {0} {1}",
+ Category,
+ 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, AssociatedClrPropertyTag);
+
+ private static readonly SymbolDisplayFormat TypeQualifiedName = new(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
+ memberOptions: SymbolDisplayMemberOptions.IncludeContainingType);
+
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(
+ AssociatedAvaloniaProperty,
+ InappropriatePropertyAssignment,
+ InappropriatePropertyRegistration,
+ PropertyOwnedByGenericType,
+ OwnerDoesNotMatchOuterType,
+ UnexpectedPropertyAccess,
+ SettingOwnStyledPropertyValue,
+ SuperfluousAddOwnerCall,
+ DuplicatePropertyName,
+ AmbiguousPropertyName,
+ PropertyNameMismatch,
+ AccessorSideEffects,
+ MissingAccessor,
+ InconsistentAccessibility,
+ PropertyTypeMismatch);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(c =>
+ {
+ if (c.Compilation.GetTypeByMetadataName("Avalonia.AvaloniaObject") is { } avaloniaObjectType)
+ {
+ new CompileAnalyzer(c, avaloniaObjectType);
+ }
+ });
+ }
+
+ private static bool IsAvaloniaPropertyType(ITypeSymbol type, params INamedTypeSymbol[] propertyTypes) => IsAvaloniaPropertyType(type, propertyTypes.AsEnumerable());
+
+ private static bool IsAvaloniaPropertyType(ITypeSymbol type, IEnumerable propertyTypes)
+ {
+ type = type.OriginalDefinition;
+
+ return propertyTypes.Any(t => SymbolEquals(type, t));
+ }
+
+ private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol? baseType, bool includeSelf = true)
+ {
+ if (baseType != null)
+ {
+ if (!includeSelf)
+ {
+ type = type?.BaseType;
+ }
+
+ while (type != null)
+ {
+ if (SymbolEquals(type, baseType))
+ {
+ return true;
+ }
+
+ type = type.BaseType;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Follows assignments and conversions back to their source.
+ ///
+ private static IOperation ResolveOperationSource(IOperation operation)
+ {
+ while (true)
+ {
+ switch (operation)
+ {
+ case IConversionOperation conversion:
+ operation = conversion.Operand;
+ break;
+ case ISimpleAssignmentOperation assignment:
+ operation = assignment.Value;
+ break;
+ default:
+ return operation;
+ }
+ }
+ }
+
+ 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,
+ IArgumentOperation argument => GetReferencedFieldOrProperty(argument.Value),
+ _ => null,
+ };
+
+ private static bool IsValidAvaloniaPropertyStorage(IFieldSymbol field) => field.IsStatic && field.IsReadOnly;
+ private static bool IsValidAvaloniaPropertyStorage(IPropertySymbol field) => field.IsStatic && field.IsReadOnly;
+
+ private static bool SymbolEquals(ISymbol? x, ISymbol? y, bool includeNullability = false)
+ {
+ // The current version of Microsoft.CodeAnalysis includes an "IncludeNullability" comparer,
+ // but it overshoots the target and tries to compare EVERYTHING. This leads to two symbols for
+ // the same type not being equal if they were imported into different compile units (i.e. assemblies).
+ // So for now, we will just discard this parameter.
+ _ = includeNullability;
+
+ return SymbolEqualityComparer.Default.Equals(x, y);
+ }
+
+ private class AvaloniaPropertyDescription
+ {
+ ///
+ /// Gets the name that was assigned to this property when it was registered.
+ ///
+ ///
+ /// If the property was not registered within the current compile context, this value will be inferred from
+ /// the name of the field (or CLR property) in which the AvaloniaProperty object is stored.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets the type of the AvaloniaProperty itself: Styled, Direct, or Attached
+ ///
+ public INamedTypeSymbol PropertyType { get; }
+
+ ///
+ /// Gets the TValue type that the property stores.
+ ///
+ public ITypeSymbol 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.
+ ///
+ public IReadOnlyDictionary AssignedTo { get; private set; }
+ private ConcurrentDictionary? _assignedTo = new(SymbolEqualityComparer.Default);
+
+ ///
+ /// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject.
+ ///
+ public IReadOnlyCollection PropertyWrappers { get; private set; }
+ private ConcurrentBag? _propertyWrappers = new();
+
+ public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, ITypeSymbol valueType)
+ {
+ Name = name;
+ PropertyType = propertyType;
+ ValueType = valueType;
+
+ OwnerTypes = _ownerTypes;
+ PropertyWrappers = _propertyWrappers;
+ AssignedTo = _assignedTo;
+ }
+
+ private const string SealedError = "PropertyDescription has been sealed.";
+
+ public void AddOwner(TypeReference owner) => (_ownerTypes ?? throw new InvalidOperationException(SealedError)).Add(owner);
+
+ public void AddPropertyWrapper(IPropertySymbol property) => (_propertyWrappers ?? throw new InvalidOperationException(SealedError)).Add(property);
+
+ public void SetAssignment(ISymbol assignmentTarget, TypeReference ownerType) => (_assignedTo ?? throw new InvalidOperationException(SealedError))[assignmentTarget] = ownerType;
+
+ public AvaloniaPropertyDescription Seal()
+ {
+ if (_ownerTypes == null || _propertyWrappers == null || _assignedTo == null)
+ {
+ return this;
+ }
+
+ OwnerTypes = _ownerTypes.ToImmutableHashSet();
+ _ownerTypes = null;
+
+ PropertyWrappers = _propertyWrappers.ToImmutableHashSet(SymbolEqualityComparer.Default);
+ _propertyWrappers = null;
+
+ AssignedTo = new ReadOnlyDictionary(_assignedTo);
+ _assignedTo = null;
+
+ return this;
+ }
+
+ ///
+ /// Searches the inheritance hierarchy of the given type for a field or property to which this AvaloniaProperty is assigned.
+ ///
+ public ISymbol? ClosestAssignmentFor(ITypeSymbol? type)
+ {
+ var assignmentsByType = AssignedTo.Keys.ToLookup(s => s.ContainingType, SymbolEqualityComparer.Default);
+
+ while (type != null)
+ {
+ if (assignmentsByType.Contains(type))
+ {
+ return assignmentsByType[type].First();
+ }
+ type = type.BaseType;
+ }
+
+ return null;
+ }
+ }
+
+ private readonly struct TypeReference
+ {
+ public ITypeSymbol Type { get; }
+ public Location Location { get; }
+
+ public TypeReference(ITypeSymbol type, Location location)
+ {
+ 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(argument, typeArgumentSyntax.GetLocation());
+ }
+ }
+
+ private class SymbolEqualityComparer : IEqualityComparer where T : ISymbol
+ {
+ public bool Equals(T x, T y) => SymbolEqualityComparer.Default.Equals(x, y);
+ public int GetHashCode(T obj) => SymbolEqualityComparer.Default.GetHashCode(obj);
+
+ public static SymbolEqualityComparer Default { get; } = new();
+ }
+}
+
+[Serializable]
+public class AvaloniaAnalysisException : Exception
+{
+ public AvaloniaAnalysisException(string message, Exception? innerException = null) : base(message, innerException)
+ {
+ }
+
+ protected AvaloniaAnalysisException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+}