Added Avalonia.Analyzers and ten diagonstics
This commit is contained in:
@@ -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",
|
||||
|
||||
+13
-1
@@ -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}
|
||||
|
||||
@@ -5,5 +5,10 @@
|
||||
ReferenceOutputAssembly="false"
|
||||
OutputItemType="Analyzer"
|
||||
SetTargetFramework="TargetFramework=netstandard2.0"/>
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)..\src\tools\PublicAnalyzers\Avalonia.Analyzers.csproj"
|
||||
PrivateAssets="all"
|
||||
ReferenceOutputAssembly="false"
|
||||
OutputItemType="Analyzer"
|
||||
SetTargetFramework="TargetFramework=netstandard2.0"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
<LangVersion>11</LangVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\build\SharedVersion.props" />
|
||||
<Import Project="..\build\DevAnalyzers.props" />
|
||||
</Project>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
|
||||
<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.1.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</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\IsExternalInit.cs" Link="IsExternalInit.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user