Browse Source
* Add TemplatePart.IsRequired property and make attribute inherited * Implement TemplatePart XAML diagnostics + tests * Fix new errors in our XAML files * Ignore nested metadata scopes in TemplatePart validator * Update docs commentpull/14736/head
committed by
GitHub
40 changed files with 388 additions and 97 deletions
@ -0,0 +1,136 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
|||
|
|||
internal class AvaloniaXamlIlControlTemplatePartsChecker : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (!(node is AvaloniaXamlIlTargetTypeMetadataNode on |
|||
&& on.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate |
|||
// Styles with template selector will also return ScopeTypes.ControlTemplate, so we need to double check.
|
|||
&& on.Value.Type.GetClrType() == context.GetAvaloniaTypes().ControlTemplate)) |
|||
return node; |
|||
|
|||
var targetType = on.TargetType.GetClrType(); |
|||
var templateParts = ResolveTemplateParts(targetType); |
|||
|
|||
if (templateParts.Count == 0) |
|||
return node; |
|||
|
|||
var visitor = new TemplatePartVisitor(); |
|||
node.VisitChildren(visitor); |
|||
|
|||
foreach (var pair in templateParts) |
|||
{ |
|||
var name = pair.Key; |
|||
var (expectedType, isRequired) = pair.Value; |
|||
|
|||
if (!visitor.TryGetValue(name, out var res)) |
|||
{ |
|||
if (isRequired) |
|||
{ |
|||
context.ReportDiagnostic(new XamlDiagnostic( |
|||
AvaloniaXamlDiagnosticCodes.RequiredTemplatePartMissing, |
|||
XamlDiagnosticSeverity.Error, |
|||
$"Required template part with x:Name '{name}' must be defined on '{targetType.Name}' ControlTemplate.", |
|||
node)); |
|||
} |
|||
else |
|||
{ |
|||
context.ReportDiagnostic(new XamlDiagnostic( |
|||
AvaloniaXamlDiagnosticCodes.OptionalTemplatePartMissing, |
|||
XamlDiagnosticSeverity.None, |
|||
$"Optional template part with x:Name '{name}' can be defined on '{targetType.Name}' ControlTemplate.", |
|||
node)); |
|||
} |
|||
|
|||
continue; |
|||
} |
|||
|
|||
if (expectedType is not null |
|||
&& !expectedType.IsAssignableFrom(res.type)) |
|||
{ |
|||
context.ReportDiagnostic(new XamlDiagnostic( |
|||
AvaloniaXamlDiagnosticCodes.TemplatePartWrongType, |
|||
XamlDiagnosticSeverity.Error, |
|||
$"Template part '{name}' is expected to be assignable to '{expectedType.Name}', but actual type is {res.type.Name}.", |
|||
res.line)); |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
private static Dictionary<string, (IXamlType? type, bool isRequired)> ResolveTemplateParts(IXamlType targetType) |
|||
{ |
|||
var dictionary = new Dictionary<string, (IXamlType? type, bool isRequired)>(); |
|||
// Custom Attributes go in order from current type to base type. It should be possible to override parent template parts.
|
|||
foreach (var attr in targetType.GetAllCustomAttributes()) |
|||
{ |
|||
if (attr.Type.Name == "TemplatePartAttribute") |
|||
{ |
|||
if (!attr.Properties.TryGetValue("Name", out var nameObj)) |
|||
{ |
|||
nameObj = attr.Parameters.FirstOrDefault(); |
|||
} |
|||
|
|||
if (!attr.Properties.TryGetValue("Type", out var typeObj)) |
|||
{ |
|||
typeObj = attr.Parameters.Skip(1).FirstOrDefault(); |
|||
} |
|||
|
|||
attr.Properties.TryGetValue("IsRequired", out var isRequiredObj); |
|||
|
|||
if (nameObj is string { Length :> 0 } name |
|||
&& !dictionary.ContainsKey(name)) |
|||
{ |
|||
dictionary.Add(name, (typeObj as IXamlType, isRequiredObj as bool? == true)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return dictionary; |
|||
} |
|||
|
|||
private class TemplatePartVisitor : Dictionary<string, (IXamlType type, IXamlLineInfo line)>, IXamlAstVisitor |
|||
{ |
|||
private int _metadataScopeLevel = 0; |
|||
private Stack<IXamlAstNode> _parents = new(); |
|||
|
|||
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
|||
{ |
|||
if (_metadataScopeLevel == 1 |
|||
&& node is AvaloniaNameScopeRegistrationXamlIlNode nameScopeRegistration |
|||
&& nameScopeRegistration.Name is XamlAstTextNode textNode) |
|||
{ |
|||
this[textNode.Text] = (nameScopeRegistration.TargetType, textNode); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
void IXamlAstVisitor.Push(IXamlAstNode node) |
|||
{ |
|||
_parents.Push(node); |
|||
if (node is NestedScopeMetadataNode) |
|||
{ |
|||
_metadataScopeLevel++; |
|||
} |
|||
} |
|||
|
|||
void IXamlAstVisitor.Pop() |
|||
{ |
|||
var oldParent = _parents.Pop(); |
|||
if (oldParent is NestedScopeMetadataNode) |
|||
{ |
|||
_metadataScopeLevel--; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue