Browse Source
* Switch over to using x:Class * The MsBuild property is no longer required * Documentation updates * Bring back the AXN0003 warning * Bring back AXN0002 * Mocks for unit tests * Actually use the mock in unit tests * Bump versionpull/10407/head 0.2.1-preview
committed by
GitHub
31 changed files with 396 additions and 360 deletions
@ -1,5 +1,6 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.NamedControl"> |
|||
<TextBox Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
|
|||
@ -1,5 +1,6 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.NoNamedControls"> |
|||
<TextBox Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
</Window> |
|||
@ -1,5 +1,6 @@ |
|||
<Window xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="Sample.App.xNamedControl"> |
|||
<TextBox x:Name="UserNameTextBox" |
|||
Watermark="Username input" |
|||
UseFloatingWatermark="True" /> |
|||
|
|||
@ -0,0 +1,39 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.NameGenerator.Compiler; |
|||
using Avalonia.NameGenerator.Generator; |
|||
using Avalonia.NameGenerator.Tests.Views; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.NameGenerator.Tests |
|||
{ |
|||
public class XamlXClassResolverTests |
|||
{ |
|||
[Theory] |
|||
[InlineData("Sample.App", "NamedControl", View.NamedControl)] |
|||
[InlineData("Sample.App", "AttachedProps", View.AttachedProps)] |
|||
[InlineData("Sample.App", "CustomControls", View.CustomControls)] |
|||
[InlineData("Sample.App", "DataTemplates", View.DataTemplates)] |
|||
[InlineData("Sample.App", "FieldModifier", View.FieldModifier)] |
|||
[InlineData("Sample.App", "NamedControls", View.NamedControls)] |
|||
[InlineData("Sample.App", "NoNamedControls", View.NoNamedControls)] |
|||
[InlineData("Sample.App", "SignUpView", View.SignUpView)] |
|||
[InlineData("Sample.App", "xNamedControl", View.XNamedControl)] |
|||
[InlineData("Sample.App", "xNamedControls", View.XNamedControls)] |
|||
public async Task Should_Resolve_Base_Class_From_Xaml_File(string nameSpace, string className, string markup) |
|||
{ |
|||
var xaml = await View.Load(markup); |
|||
var compilation = View |
|||
.CreateAvaloniaCompilation() |
|||
.WithCustomTextBox(); |
|||
|
|||
var types = new RoslynTypeSystem(compilation); |
|||
var resolver = new XamlXClassResolver( |
|||
types, |
|||
MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); |
|||
|
|||
var resolvedClass = resolver.ResolveClass(xaml); |
|||
Assert.Equal(className, resolvedClass.ClassName); |
|||
Assert.Equal(nameSpace, resolvedClass.NameSpace); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
namespace Avalonia.NameGenerator.Domain |
|||
{ |
|||
internal interface IClassResolver |
|||
{ |
|||
ResolvedClass ResolveClass(string xaml); |
|||
} |
|||
|
|||
internal record ResolvedClass |
|||
{ |
|||
public string ClassName { get; } |
|||
public string NameSpace { get; } |
|||
|
|||
public ResolvedClass(string className, string nameSpace) |
|||
{ |
|||
ClassName = className; |
|||
NameSpace = nameSpace; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Domain |
|||
{ |
|||
internal interface ICodeGenerator |
|||
{ |
|||
string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.NameGenerator.Domain |
|||
{ |
|||
internal interface INameGenerator |
|||
{ |
|||
IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles); |
|||
} |
|||
|
|||
internal record GeneratedPartialClass |
|||
{ |
|||
public string FileName { get; } |
|||
public string Content { get; } |
|||
|
|||
public GeneratedPartialClass(string fileName, string content) |
|||
{ |
|||
FileName = fileName; |
|||
Content = content; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Domain |
|||
{ |
|||
internal interface INameResolver |
|||
{ |
|||
IReadOnlyList<ResolvedName> ResolveNames(string xaml); |
|||
} |
|||
|
|||
internal record ResolvedName |
|||
{ |
|||
public string TypeName { get; } |
|||
public string Name { get; } |
|||
public string FieldModifier { get; } |
|||
|
|||
public ResolvedName(string typeName, string name, string fieldModifier) |
|||
{ |
|||
TypeName = typeName; |
|||
Name = name; |
|||
FieldModifier = fieldModifier; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.NameGenerator.Domain; |
|||
using Microsoft.CodeAnalysis; |
|||
|
|||
namespace Avalonia.NameGenerator.Generator |
|||
{ |
|||
internal class AvaloniaNameGenerator : INameGenerator |
|||
{ |
|||
private readonly IClassResolver _classes; |
|||
private readonly INameResolver _names; |
|||
private readonly ICodeGenerator _code; |
|||
|
|||
public AvaloniaNameGenerator(IClassResolver classes, INameResolver names, ICodeGenerator code) |
|||
{ |
|||
_classes = classes; |
|||
_names = names; |
|||
_code = code; |
|||
} |
|||
|
|||
public IReadOnlyList<GeneratedPartialClass> GenerateNameReferences(IEnumerable<AdditionalText> additionalFiles) |
|||
{ |
|||
var resolveViewsQuery = |
|||
from file in additionalFiles |
|||
where file.Path.EndsWith(".xaml") || |
|||
file.Path.EndsWith(".paml") || |
|||
file.Path.EndsWith(".axaml") |
|||
let xaml = file.GetText()!.ToString() |
|||
let type = _classes.ResolveClass(xaml) |
|||
where type != null |
|||
let className = type.ClassName |
|||
let nameSpace = type.NameSpace |
|||
select new ResolvedView(className, nameSpace, xaml); |
|||
|
|||
var query = |
|||
from view in resolveViewsQuery.ToList() |
|||
let names = _names.ResolveNames(view.Xaml) |
|||
let code = _code.GenerateCode(view.ClassName, view.NameSpace, names) |
|||
let fileName = $"{view.ClassName}.g.cs" |
|||
select new GeneratedPartialClass(fileName, code); |
|||
|
|||
return query.ToList(); |
|||
} |
|||
|
|||
private record ResolvedView |
|||
{ |
|||
public string ClassName { get; } |
|||
public string NameSpace { get; } |
|||
public string Xaml { get; } |
|||
|
|||
public ResolvedView(string className, string nameSpace, string xaml) |
|||
{ |
|||
ClassName = className; |
|||
NameSpace = nameSpace; |
|||
Xaml = xaml; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,12 +1,12 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.NameGenerator.Domain; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
namespace Avalonia.NameGenerator.Generator |
|||
{ |
|||
internal class FindControlNameGenerator : INameGenerator |
|||
internal class FindControlNameGenerator : ICodeGenerator |
|||
{ |
|||
public string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names) |
|||
public string GenerateCode(string className, string nameSpace, IEnumerable<ResolvedName> names) |
|||
{ |
|||
var namedControls = names |
|||
.Select(info => " " + |
|||
@ -0,0 +1,92 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.NameGenerator.Compiler; |
|||
using Avalonia.NameGenerator.Domain; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Parsers; |
|||
|
|||
namespace Avalonia.NameGenerator.Generator |
|||
{ |
|||
internal class XamlXClassResolver : IClassResolver, IXamlAstVisitor |
|||
{ |
|||
private readonly RoslynTypeSystem _typeSystem; |
|||
private readonly MiniCompiler _compiler; |
|||
private readonly bool _checkTypeValidity; |
|||
private readonly Action<string> _onTypeInvalid; |
|||
private ResolvedClass _resolvedClass; |
|||
|
|||
public XamlXClassResolver( |
|||
RoslynTypeSystem typeSystem, |
|||
MiniCompiler compiler, |
|||
bool checkTypeValidity = false, |
|||
Action<string> onTypeInvalid = null) |
|||
{ |
|||
_checkTypeValidity = checkTypeValidity; |
|||
_onTypeInvalid = onTypeInvalid; |
|||
_typeSystem = typeSystem; |
|||
_compiler = compiler; |
|||
} |
|||
|
|||
public ResolvedClass ResolveClass(string xaml) |
|||
{ |
|||
_resolvedClass = null; |
|||
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string> |
|||
{ |
|||
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} |
|||
}); |
|||
|
|||
_compiler.Transform(parsed); |
|||
parsed.Root.Visit(this); |
|||
parsed.Root.VisitChildren(this); |
|||
return _resolvedClass; |
|||
} |
|||
|
|||
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) |
|||
{ |
|||
if (node is not XamlAstObjectNode objectNode) |
|||
return node; |
|||
|
|||
var clrType = objectNode.Type.GetClrType(); |
|||
var isAvaloniaControl = clrType |
|||
.Interfaces |
|||
.Any(abstraction => abstraction.IsInterface && |
|||
abstraction.FullName == "Avalonia.Controls.IControl"); |
|||
|
|||
if (!isAvaloniaControl) |
|||
return node; |
|||
|
|||
foreach (var child in objectNode.Children) |
|||
{ |
|||
if (child is XamlAstXmlDirective directive && |
|||
directive.Name == "Class" && |
|||
directive.Namespace == XamlNamespaces.Xaml2006 && |
|||
directive.Values[0] is XamlAstTextNode text) |
|||
{ |
|||
if (_checkTypeValidity) |
|||
{ |
|||
var existingType = _typeSystem.FindType(text.Text); |
|||
if (existingType == null) |
|||
{ |
|||
_onTypeInvalid?.Invoke(text.Text); |
|||
return node; |
|||
} |
|||
} |
|||
|
|||
var split = text.Text.Split('.'); |
|||
var nameSpace = string.Join(".", split.Take(split.Length - 1)); |
|||
var className = split.Last(); |
|||
_resolvedClass = new ResolvedClass(className, nameSpace); |
|||
return node; |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
|
|||
void IXamlAstVisitor.Push(IXamlAstNode node) { } |
|||
|
|||
void IXamlAstVisitor.Pop() { } |
|||
} |
|||
} |
|||
@ -1,25 +1,19 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.NameGenerator.Compiler; |
|||
using Microsoft.CodeAnalysis.CSharp; |
|||
using Avalonia.NameGenerator.Domain; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Parsers; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
namespace Avalonia.NameGenerator.Generator |
|||
{ |
|||
internal class XamlXNameResolver : INameResolver, IXamlAstVisitor |
|||
{ |
|||
private const string AvaloniaXmlnsAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; |
|||
private readonly List<ResolvedName> _items = new(); |
|||
private readonly MiniCompiler _compiler; |
|||
|
|||
public XamlXNameResolver(CSharpCompilation compilation) => |
|||
_compiler = MiniCompiler |
|||
.CreateDefault( |
|||
new RoslynTypeSystem(compilation), |
|||
AvaloniaXmlnsAttribute); |
|||
public XamlXNameResolver(MiniCompiler compiler) => _compiler = compiler; |
|||
|
|||
public IReadOnlyList<ResolvedName> ResolveNames(string xaml) |
|||
{ |
|||
@ -1,17 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using Microsoft.CodeAnalysis; |
|||
using Microsoft.CodeAnalysis.CSharp.Syntax; |
|||
|
|||
namespace Avalonia.NameGenerator |
|||
{ |
|||
internal class NameReferenceSyntaxReceiver : ISyntaxReceiver |
|||
{ |
|||
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>(); |
|||
|
|||
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) |
|||
{ |
|||
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax) |
|||
CandidateClasses.Add(classDeclarationSyntax); |
|||
} |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
{ |
|||
internal interface INameGenerator |
|||
{ |
|||
string GenerateNames(string className, string nameSpace, IEnumerable<ResolvedName> names); |
|||
} |
|||
} |
|||
@ -1,43 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.NameGenerator.Resolver |
|||
{ |
|||
internal interface INameResolver |
|||
{ |
|||
IReadOnlyList<ResolvedName> ResolveNames(string xaml); |
|||
} |
|||
|
|||
internal class ResolvedName |
|||
{ |
|||
public string TypeName { get; } |
|||
public string Name { get; } |
|||
public string FieldModifier { get; } |
|||
|
|||
public ResolvedName(string typeName, string name, string fieldModifier) |
|||
{ |
|||
TypeName = typeName; |
|||
Name = name; |
|||
FieldModifier = fieldModifier; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (obj is not ResolvedName name) |
|||
return false; |
|||
return name.Name == Name && |
|||
name.TypeName == TypeName && |
|||
name.FieldModifier == FieldModifier; |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
var hashCode = TypeName != null ? TypeName.GetHashCode() : 0; |
|||
hashCode = (hashCode * 397) ^ (Name != null ? Name.GetHashCode() : 0); |
|||
hashCode = (hashCode * 397) ^ (FieldModifier != null ? FieldModifier.GetHashCode() : 0); |
|||
return hashCode; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,28 +0,0 @@ |
|||
using Microsoft.CodeAnalysis; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.NameGenerator |
|||
{ |
|||
internal static class SourceGeneratorContextExtensions |
|||
{ |
|||
private const string SourceItemGroupMetadata = "build_metadata.AdditionalFiles.SourceItemGroup"; |
|||
|
|||
public static string GetMSBuildProperty( |
|||
this GeneratorExecutionContext context, |
|||
string name, |
|||
string defaultValue = "") |
|||
{ |
|||
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.{name}", out var value); |
|||
return value ?? defaultValue; |
|||
} |
|||
|
|||
public static string[] GetMSBuildItems(this GeneratorExecutionContext context, string name) |
|||
=> context |
|||
.AdditionalFiles |
|||
.Where(f => context.AnalyzerConfigOptions |
|||
.GetOptions(f).TryGetValue(SourceItemGroupMetadata, out var sourceItemGroup) |
|||
&& sourceItemGroup == name) |
|||
.Select(f => f.Path) |
|||
.ToArray(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue