Browse Source

Added Avalonia.Analyzers and ten diagonstics

pull/10244/head
Tom Edwards 3 years ago
parent
commit
1fbd4ab801
  1. 3
      Avalonia.Desktop.slnf
  2. 14
      Avalonia.sln
  3. 5
      build/DevAnalyzers.props
  4. 1
      samples/Directory.Build.props
  5. 4
      src/tools/DevAnalyzers/DevAnalyzers.csproj
  6. 4
      src/tools/DevGenerators/DevGenerators.csproj
  7. 17
      src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj
  8. 555
      src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs
  9. 253
      src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs

3
Avalonia.Desktop.slnf

@ -8,9 +8,9 @@
"samples\\GpuInterop\\GpuInterop.csproj", "samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"samples\\SampleControls\\ControlSamples.csproj", "samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj", "samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj", "src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj", "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj", "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
@ -42,6 +42,7 @@
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",

14
Avalonia.sln

@ -231,7 +231,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
EndProject EndProject
@ -560,6 +567,10 @@ Global
{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -626,6 +637,7 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {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} {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

5
build/DevAnalyzers.props

@ -5,5 +5,10 @@
ReferenceOutputAssembly="false" ReferenceOutputAssembly="false"
OutputItemType="Analyzer" OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/> SetTargetFramework="TargetFramework=netstandard2.0"/>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer"
SetTargetFramework="TargetFramework=netstandard2.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

1
samples/Directory.Build.props

@ -6,4 +6,5 @@
<LangVersion>11</LangVersion> <LangVersion>11</LangVersion>
</PropertyGroup> </PropertyGroup>
<Import Project="..\build\SharedVersion.props" /> <Import Project="..\build\SharedVersion.props" />
<Import Project="..\build\DevAnalyzers.props" />
</Project> </Project>

4
src/tools/DevAnalyzers/DevAnalyzers.csproj

@ -6,11 +6,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

4
src/tools/DevGenerators/DevGenerators.csproj

@ -7,11 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
<Compile Include="..\..\Shared\SourceGeneratorAttributes.cs" /> <Compile Include="..\..\Shared\SourceGeneratorAttributes.cs" />
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" /> <Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup> </ItemGroup>

17
src/tools/PublicAnalyzers/Avalonia.Analyzers.csproj

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>

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

@ -0,0 +1,555 @@
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
{
/// <summary>
/// A dictionary that maps field/property symbols to the AvaloniaProperty objects assigned to them.
/// </summary>
private readonly ConcurrentDictionary<ISymbol, AvaloniaPropertyDescription> _avaloniaProperyDescriptions = new(SymbolEqualityComparer.Default);
private readonly ConcurrentDictionary<IPropertySymbol, ImmutableArray<AvaloniaPropertyDescription>> _clrPropertyToAvaloniaProperties = new(SymbolEqualityComparer.Default);
private readonly INamedTypeSymbol _avaloniaObjectType;
private readonly ImmutableHashSet<IMethodSymbol> _getValueMethods;
private readonly ImmutableHashSet<IMethodSymbol> _setValueMethods;
private readonly INamedTypeSymbol _avaloniaPropertyType;
private readonly INamedTypeSymbol _styledPropertyType;
private readonly INamedTypeSymbol _attachedPropertyType;
private readonly INamedTypeSymbol _directPropertyType;
private readonly ImmutableHashSet<IMethodSymbol> _avaloniaPropertyRegisterMethods;
private readonly ImmutableHashSet<IMethodSymbol> _avaloniaPropertyAddOwnerMethods;
public CompileAnalyzer(CompilationStartAnalysisContext context)
{
_avaloniaObjectType = GetTypeOrThrow("Avalonia.AvaloniaObject");
_getValueMethods = _avaloniaObjectType.GetMembers("GetValue").OfType<IMethodSymbol>().ToImmutableHashSet<IMethodSymbol>(SymbolEqualityComparer.Default);
_setValueMethods = _avaloniaObjectType.GetMembers("SetValue").OfType<IMethodSymbol>().ToImmutableHashSet<IMethodSymbol>(SymbolEqualityComparer.Default);
_avaloniaPropertyType = GetTypeOrThrow("Avalonia.AvaloniaProperty");
_styledPropertyType = GetTypeOrThrow("Avalonia.StyledProperty`1");
_attachedPropertyType = GetTypeOrThrow("Avalonia.AttachedProperty`1");
_directPropertyType = GetTypeOrThrow("Avalonia.DirectProperty`2");
_avaloniaPropertyRegisterMethods = _avaloniaPropertyType.GetMembers()
.OfType<IMethodSymbol>().Where(m => m.Name.StartsWith("Register")).ToImmutableHashSet<IMethodSymbol>(SymbolEqualityComparer.Default);
_avaloniaPropertyAddOwnerMethods = new[] { _styledPropertyType, _attachedPropertyType, _directPropertyType }
.SelectMany(t => t.GetMembers("AddOwner").OfType<IMethodSymbol>()).ToImmutableHashSet<IMethodSymbol>(SymbolEqualityComparer.Default);
FindAvaloniaPropertySymbols(context.Compilation, context.CancellationToken);
context.RegisterOperationAction(AnalyzeFieldInitializer, OperationKind.FieldInitializer);
context.RegisterOperationAction(AnalyzePropertyInitializer, OperationKind.PropertyInitializer);
context.RegisterSymbolStartAction(StartPropertySymbolAnalysis, 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.");
}
private void FindAvaloniaPropertySymbols(Compilation compilation, CancellationToken cancellationToken)
{
var namespaceStack = new Stack<INamespaceSymbol>();
namespaceStack.Push(compilation.GlobalNamespace);
var types = new List<INamedTypeSymbol>();
while (namespaceStack.Count > 0)
{
var current = namespaceStack.Pop();
foreach (var type in current.GetTypeMembers())
{
if (DerivesFrom(type, _avaloniaObjectType))
{
types.Add(type);
}
}
foreach (var child in current.GetNamespaceMembers())
{
namespaceStack.Push(child);
}
}
var references = new ConcurrentBag<(ISymbol symbol, Func<ISymbolInitializerOperation, IEnumerable<ISymbol>> getInits)>();
var parallelOptions = new ParallelOptions() { CancellationToken = cancellationToken };
Parallel.ForEach(types, parallelOptions, type =>
{
foreach (var member in type.GetMembers())
{
switch (member)
{
case IFieldSymbol fieldSymbol when IsValidAvaloniaPropertyStorage(fieldSymbol):
references.Add((fieldSymbol, so => ((IFieldInitializerOperation)so).InitializedFields));
break;
case IPropertySymbol propertySymbol when IsValidAvaloniaPropertyStorage(propertySymbol):
references.Add((propertySymbol, so => ((IPropertyInitializerOperation)so).InitializedProperties));
break;
}
}
});
// key initializes value
var fieldInitializations = new ConcurrentDictionary<ISymbol, ISymbol>(SymbolEqualityComparer.Default);
Parallel.ForEach(references, parallelOptions, tuple =>
{
foreach (var syntaxRef in tuple.symbol.DeclaringSyntaxReferences)
{
var node = syntaxRef.GetSyntax(cancellationToken);
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();
if (operation == null)
{
return;
}
var operationValue = operation.Value;
while (operationValue is IConversionOperation conversion)
{
operationValue = conversion.Operand;
}
switch (operationValue)
{
case IInvocationOperation invocation:
RegisterInitializer_Invocation(tuple.getInits(operation), invocation, tuple.symbol);
break;
case IFieldReferenceOperation fieldRef when IsValidAvaloniaPropertyStorage(fieldRef.Field):
fieldInitializations[fieldRef.Field] = tuple.symbol;
break;
case IPropertyReferenceOperation propRef when IsValidAvaloniaPropertyStorage(propRef.Property):
fieldInitializations[propRef.Property] = tuple.symbol;
break;
}
}
});
// we have recorded every Register and AddOwner call. Now follow assignment chains.
foreach (var root in fieldInitializations.Keys.Intersect(_avaloniaProperyDescriptions.Keys, SymbolEqualityComparer.Default).ToArray())
{
var propertyDescription = _avaloniaProperyDescriptions[root];
var owner = propertyDescription.AssignedTo[root];
var current = root;
do
{
var target = fieldInitializations[current];
propertyDescription.AssignedTo[target] = owner; // This loop handles simple assignment operations, so do NOT change the owner
_avaloniaProperyDescriptions[target] = propertyDescription;
fieldInitializations.TryGetValue(target, out current);
}
while(current != null);
}
}
private void RegisterInitializer_Invocation(IEnumerable<ISymbol> initializedSymbols, IInvocationOperation invocation, ISymbol target)
{
try
{
if (invocation.TargetMethod.ReturnType is not INamedTypeSymbol propertyType)
{
return;
}
if (_avaloniaPropertyRegisterMethods.Contains(invocation.TargetMethod.OriginalDefinition)) // This is a call to one of the AvaloniaProperty.Register* methods
{
if (!invocation.TargetMethod.IsGenericMethod)
{
return;
}
var typeParamLookup = invocation.TargetMethod.TypeParameters.Select((s, i) => (param: s, index: i))
.ToDictionary(t => t.param.Name, t => (INamedTypeSymbol)invocation.TargetMethod.TypeArguments[t.index]);
if (!typeParamLookup.TryGetValue("TOwner", out var ownerType) && // 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 (argument.Value)
{
case ITypeOfOperation typeOf:
ownerType = (INamedTypeSymbol)typeOf.Type!;
break;
}
}
if (ownerType == null || !typeParamLookup.TryGetValue("TValue", out var propertyValueType))
{
return;
}
foreach (var symbol in initializedSymbols)
{
string name;
switch (invocation.Arguments[0].Value)
{
case INameOfOperation nameof when nameof.Argument is IPropertyReferenceOperation propertyReference:
name = propertyReference.Property.Name;
break;
case IAssignmentOperation assignment when assignment.ConstantValue is { HasValue: true } stringLiteral:
name = (string)stringLiteral.Value!;
break;
default:
return;
}
var description = _avaloniaProperyDescriptions.GetOrAdd(symbol, s => new AvaloniaPropertyDescription(name, propertyType, propertyValueType));
description.Name = name;
description.AssignedTo[symbol] = ownerType;
description.OwnerTypes.Add(ownerType);
}
}
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)
{
return;
}
ISymbol sourceSymbol;
switch (invocation.Instance)
{
case IFieldReferenceOperation fieldReference:
sourceSymbol = fieldReference.Field;
break;
case IPropertyReferenceOperation propertyReference:
sourceSymbol = propertyReference.Property;
break;
default:
return;
}
var propertyValueType = AvaloniaPropertyType_GetValueType(propertyType);
foreach (var symbol in initializedSymbols)
{
var description = _avaloniaProperyDescriptions.GetOrAdd(symbol, s =>
{
string inferredName = target.Name;
var match = Regex.Match(target.Name, "(?<name>.*)Property$");
if (match.Success)
{
inferredName = match.Groups["name"].Value;
}
return new AvaloniaPropertyDescription(inferredName, (INamedTypeSymbol)invocation.TargetMethod.ReturnType, propertyValueType);
});
description.AssignedTo[symbol] = ownerType;
description.OwnerTypes.Add(ownerType);
}
}
}
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 (!_avaloniaProperyDescriptions.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 (!_avaloniaProperyDescriptions.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 AnalyzeInitializer_Shared(OperationAnalysisContext context, ISymbol assignmentSymbol, AvaloniaPropertyDescription description)
{
if (!assignmentSymbol.Name.Contains(description.Name))
{
context.ReportDiagnostic(Diagnostic.Create(PropertyNameMismatch, assignmentSymbol.Locations[0],
description.Name, assignmentSymbol));
}
try
{
var ownerType = description.AssignedTo[assignmentSymbol];
if (!IsAvaloniaPropertyType(description.PropertyType, _attachedPropertyType) && !SymbolEquals(ownerType, assignmentSymbol.ContainingType))
{
context.ReportDiagnostic(Diagnostic.Create(OwnerDoesNotMatchOuterType, assignmentSymbol.Locations[0], ownerType));
}
}
catch (KeyNotFoundException)
{
return; // WIP
throw new KeyNotFoundException($"Assignment operation for {assignmentSymbol} was not recorded.");
}
}
private void StartPropertySymbolAnalysis(SymbolStartAnalysisContext context)
{
var property = (IPropertySymbol)context.Symbol;
try
{
var avaloniaPropertyDescriptions = GetAvaloniaPropertiesForType(property.ContainingType).ToLookup(d => d.Name);
var candidateTargetProperties = avaloniaPropertyDescriptions[property.Name].ToImmutableArray();
switch (candidateTargetProperties.Length)
{
case 0:
return; // does not refer to an AvaloniaProperty
case 1:
candidateTargetProperties[0].PropertyWrappers.Add(property);
break;
}
_clrPropertyToAvaloniaProperties[property] = candidateTargetProperties;
context.RegisterSymbolEndAction(context =>
{
if (candidateTargetProperties.Length > 1)
{
var candidateSymbols = candidateTargetProperties.Select(d => d.ClosestAssignmentFor(property.ContainingType)).Where(s => s != null);
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 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)
{
var argumentValue = operation.Arguments[0].Value;
if (argumentValue is IConversionOperation conversion)
{
argumentValue = conversion.Operand;
}
switch (argumentValue)
{
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(argumentValue.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);
}
}
private INamedTypeSymbol AvaloniaPropertyType_GetValueType(INamedTypeSymbol type)
{
var compareType = type.IsGenericType ? type.ConstructUnboundGenericType().OriginalDefinition : type;
if (SymbolEquals(compareType, _styledPropertyType) || SymbolEquals(compareType, _attachedPropertyType))
{
return (INamedTypeSymbol)type.TypeArguments[0];
}
else if (SymbolEquals(compareType, _directPropertyType))
{
return (INamedTypeSymbol)type.TypeArguments[1];
}
throw new ArgumentException($"{type} is not a recognised AvaloniaProperty ({_styledPropertyType}, {_attachedPropertyType}, {_directPropertyType}).", nameof(type));
}
private ImmutableHashSet<AvaloniaPropertyDescription> GetAvaloniaPropertiesForType(ITypeSymbol type)
{
var properties = new List<AvaloniaPropertyDescription>();
var current = type;
while (current != null)
{
properties.AddRange(current.GetMembers().Intersect(_avaloniaProperyDescriptions.Keys, SymbolEqualityComparer.Default).Select(s => _avaloniaProperyDescriptions[s]));
current = current.BaseType;
}
return properties.ToImmutableHashSet();
}
}
}

253
src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.cs

@ -0,0 +1,253 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Avalonia.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public partial class AvaloniaPropertyAnalyzer : DiagnosticAnalyzer
{
private const string Category = "AvaloniaProperty";
private const string TypeMismatchTag = "TypeMismatch";
private const string NameCollisionTag = "NameCollision";
private const string AssociatedPropertyTag = "AssociatedProperty";
private static readonly DiagnosticDescriptor AssociatedAvaloniaProperty = new(
"AVP0001",
"Identify 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.",
AssociatedPropertyTag);
private static readonly DiagnosticDescriptor InappropriatePropertyAssignment = new(
"AVP1000",
"Store AvaloniaProperty objects 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. Do not multiply construct the same property.");
private static readonly DiagnosticDescriptor OwnerDoesNotMatchOuterType = new(
"AVP1010",
"Avaloniaproperty objects should declare their owner to be 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 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",
"Ensure an umabiguous relationship between CLR properties and Avalonia properties within the same 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 AddOwner method and provide new metadata.",
NameCollisionTag);
private static readonly DiagnosticDescriptor PropertyNameMismatch = new(
"AVP1022",
"Store each AvaloniaProperty object 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\".",
NameCollisionTag);
private static readonly DiagnosticDescriptor AccessorSideEffects = new(
"AVP1030",
"Do not add side effects to StyledProperty accessors",
"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, create a Coerce method or a PropertyChanged subscriber.",
AssociatedPropertyTag);
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.",
AssociatedPropertyTag);
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} accessiblity 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 acessibility from its associated AvaloniaProperty is ineffective.",
AssociatedPropertyTag);
private static readonly DiagnosticDescriptor PropertyTypeMismatch = new(
"AVP1040",
"CLR property type should match 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, AssociatedPropertyTag);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
AssociatedAvaloniaProperty,
InappropriatePropertyAssignment,
OwnerDoesNotMatchOuterType,
DuplicatePropertyName,
AmbiguousPropertyName,
PropertyNameMismatch,
AccessorSideEffects,
MissingAccessor,
InconsistentAccessibility,
PropertyTypeMismatch);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(c => new CompileAnalyzer(c));
}
private static bool IsAvaloniaPropertyType(INamedTypeSymbol type, params INamedTypeSymbol[] propertyTypes)
{
if (type.IsGenericType)
{
type = type.ConstructUnboundGenericType().OriginalDefinition;
}
return propertyTypes.Any(t => SymbolEquals(type, t));
}
private static bool DerivesFrom(ITypeSymbol? type, ITypeSymbol baseType)
{
while (type != null)
{
if (SymbolEquals(type, baseType))
{
return true;
}
type = type.BaseType;
}
return false;
}
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
{
/// <summary>
/// Gets the name that was assigned to this property when it was registered.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public string Name { get; set; }
/// <summary>
/// Gets the type of the AvaloniaProperty itself: Styled, Direct, or Attached
/// </summary>
public INamedTypeSymbol PropertyType { get; }
/// <summary>
/// Gets the TValue type that the property stores.
/// </summary>
public INamedTypeSymbol ValueType { get; }
/// <summary>
/// Gets the type which registered the property, and all types which have added themselves as owners.
/// </summary>
public ConcurrentBag<INamedTypeSymbol> OwnerTypes { get; } = new();
/// <summary>
/// Gets a dictionary which maps fields and properties which were initialized with this AvaloniaProperty to the TOwner specified at each assignment.
/// </summary>
public ConcurrentDictionary<ISymbol, INamedTypeSymbol> AssignedTo { get; } = new(SymbolEqualityComparer.Default);
/// <summary>
/// Gets properties which provide convenient access to the AvaloniaProperty on an instance of an AvaloniaObject.
/// </summary>
public ConcurrentBag<IPropertySymbol> PropertyWrappers { get; } = new();
public AvaloniaPropertyDescription(string name, INamedTypeSymbol propertyType, INamedTypeSymbol valueType)
{
Name = name;
PropertyType = propertyType;
ValueType = valueType;
}
/// <summary>
/// Searches the inheritance hierarchy of the given type for a field or property to which this AvaloniaProperty is assigned.
/// </summary>
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;
}
}
}
[Serializable]
public class AvaloniaAnalysisException : Exception
{
public AvaloniaAnalysisException(string message, Exception? innerException = null) : base(message, innerException)
{
}
protected AvaloniaAnalysisException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
Loading…
Cancel
Save