diff --git a/README.md b/README.md index 2c190c80cb..923dbb43bc 100644 --- a/README.md +++ b/README.md @@ -30,63 +30,32 @@ Or, if you are using [submodules](https://git-scm.com/docs/git-submodule), you c ``` -### Usage (Default) +### Usage -After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for all classes that inherit from the `Avalonia.INamed` interface (including those classes that inherit from `Window`, `UserControl`, `ReactiveWindow`, `ReactiveUserControl`). For example, for the following XAML markup: +After installing the NuGet package, declare your view class as `partial`. Typed C# references to Avalonia controls declared in XAML files will be generated for classes referenced by the `x:Class` directive in XAML files. For example, for the following XAML markup: ```xml + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.SignUpView"> ``` -A new C# public property named `UserNameTextBox` of type `TextBox` will be generated: +A new C# partial class named `SignUpView` with a single `public` property named `UserNameTextBox` of type `TextBox` will be generated in the `Sample.App` namespace. We won't see the generated file, but we'll be able to access the generated property as shown below: ```cs using Avalonia.Controls; -public partial class SignUpView : Window +namespace Sample.App { - public SignUpView() - { - AvaloniaXamlLoader.Load(this); - UserNameTextBox.Text = "Joseph"; // Cool stuff! - } -} -``` - -By default, the generator tries to generate `x:Name` references for every class implementing `INamed`, and this can result in a lot of warnings. In order to disable those warnings, either switch to opt-in attribute-based approach (see the documentation section below), or add the following to your `.csproj` file: - -```xml - - AXN0001 - AXN0003 - -``` - -### Usage (Opt-in) - -If you don't want to generate typed `x:Name` references for every window or user control in your assembly, you can always turn off this default behavior by setting the `AvaloniaNameGenerator` MsBuild property to `false` in your C# project file (`.csproj`). Just add the following property group to your `` tag: - -```xml - - false - -``` - -From now on, the source generator will process only those files that are decorated with the `[GenerateTypedNameReferences]` attribute. Other window or user control classes will be left unchanged, and you won't have to mark them as `partial`. - -```cs -using Avalonia.Controls; - -[GenerateTypedNameReferences] -public partial class SignUpView : Window -{ - public SignUpView() + public partial class SignUpView : Window { - AvaloniaXamlLoader.Load(this); - UserNameTextBox.Text = "Joseph"; // Cool stuff! + public SignUpView() + { + AvaloniaXamlLoader.Load(this); + UserNameTextBox.Text = "Joseph"; // Cool stuff! + } } } ``` @@ -102,7 +71,7 @@ For the [`SignUpView` view class](https://github.com/avaloniaui/Avalonia.NameGen using Avalonia.Controls; -namespace Your.View.Namespace +namespace Avalonia.NameGenerator.Sandbox.Views { partial class SignUpView { diff --git a/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs b/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs index c5798628ec..41217005c5 100644 --- a/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs +++ b/src/Avalonia.NameGenerator.Sandbox/Views/SignUpView.xaml.cs @@ -10,13 +10,11 @@ namespace Avalonia.NameGenerator.Sandbox.Views /// references are living in a separate partial class file. See also: /// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/ /// - [GenerateTypedNameReferences] public partial class SignUpView : Window { public SignUpView() { AvaloniaXamlLoader.Load(this); - UserNameTextBox.Text = "Joseph!"; UserNameValidation.Text = "User name is valid."; PasswordTextBox.Text = "qwerty"; @@ -25,8 +23,7 @@ namespace Avalonia.NameGenerator.Sandbox.Views ConfirmPasswordValidation.Text = "Password confirmation is valid."; SignUpButton.Content = "Sign up please!"; CompoundValidation.Text = "Everything is okay."; - - var listView = AwesomeListView; + AwesomeListView.VirtualizationMode = ItemVirtualizationMode.None; } } } \ No newline at end of file diff --git a/src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs b/src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs index 460c597240..f77abb663f 100644 --- a/src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs +++ b/src/Avalonia.NameGenerator.Tests/FindControlNameGeneratorTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; -using Avalonia.NameGenerator.Resolver; +using Avalonia.NameGenerator.Compiler; +using Avalonia.NameGenerator.Generator; using Avalonia.NameGenerator.Tests.GeneratedCode; using Avalonia.NameGenerator.Tests.Views; using Microsoft.CodeAnalysis.CSharp; @@ -27,10 +28,14 @@ namespace Avalonia.NameGenerator.Tests View.CreateAvaloniaCompilation() .WithCustomTextBox(); - var resolver = new XamlXNameResolver(compilation); + var resolver = new XamlXNameResolver( + MiniCompiler.CreateDefault( + new RoslynTypeSystem(compilation), + MiniCompiler.AvaloniaXmlnsDefinitionAttribute)); + var generator = new FindControlNameGenerator(); var code = generator - .GenerateNames("SampleView", "Sample.App", resolver.ResolveNames(xaml)) + .GenerateCode("SampleView", "Sample.App", resolver.ResolveNames(xaml)) .Replace("\r", string.Empty); var expected = await Code.Load(expectation); diff --git a/src/Avalonia.NameGenerator.Tests/Views/AttachedProps.xml b/src/Avalonia.NameGenerator.Tests/Views/AttachedProps.xml index 1993b8c8a7..896da6d1cd 100644 --- a/src/Avalonia.NameGenerator.Tests/Views/AttachedProps.xml +++ b/src/Avalonia.NameGenerator.Tests/Views/AttachedProps.xml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:custom="clr-namespace:Avalonia.ReactiveUI;assembly=Avalonia.ReactiveUI" xmlns:rxui="http://reactiveui.net" + x:Class="Sample.App.AttachedProps" Design.Width="300"> diff --git a/src/Avalonia.NameGenerator.Tests/Views/DataTemplates.xml b/src/Avalonia.NameGenerator.Tests/Views/DataTemplates.xml index 3cd6a8577a..f7e15644aa 100644 --- a/src/Avalonia.NameGenerator.Tests/Views/DataTemplates.xml +++ b/src/Avalonia.NameGenerator.Tests/Views/DataTemplates.xml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.DataTemplates"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.FieldModifier"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.NamedControl"> diff --git a/src/Avalonia.NameGenerator.Tests/Views/NamedControls.xml b/src/Avalonia.NameGenerator.Tests/Views/NamedControls.xml index d55917be18..6083ec794c 100644 --- a/src/Avalonia.NameGenerator.Tests/Views/NamedControls.xml +++ b/src/Avalonia.NameGenerator.Tests/Views/NamedControls.xml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.NamedControls"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.NoNamedControls"> \ No newline at end of file diff --git a/src/Avalonia.NameGenerator.Tests/Views/SignUpView.xml b/src/Avalonia.NameGenerator.Tests/Views/SignUpView.xml index 00ad893353..dd6593536d 100644 --- a/src/Avalonia.NameGenerator.Tests/Views/SignUpView.xml +++ b/src/Avalonia.NameGenerator.Tests/Views/SignUpView.xml @@ -1,6 +1,7 @@  + xmlns:controls="clr-namespace:Controls" + x:Class="Sample.App.SignUpView"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.xNamedControl"> diff --git a/src/Avalonia.NameGenerator.Tests/Views/xNamedControls.xml b/src/Avalonia.NameGenerator.Tests/Views/xNamedControls.xml index 9d402c276c..5e9d083be3 100644 --- a/src/Avalonia.NameGenerator.Tests/Views/xNamedControls.xml +++ b/src/Avalonia.NameGenerator.Tests/Views/xNamedControls.xml @@ -1,5 +1,6 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + x:Class="Sample.App.xNamedControls"> { + public const string AvaloniaXmlnsDefinitionAttribute = "Avalonia.Metadata.XmlnsDefinitionAttribute"; + public static MiniCompiler CreateDefault(RoslynTypeSystem typeSystem, params string[] additionalTypes) { var mappings = new XamlLanguageTypeMappings(typeSystem); diff --git a/src/Avalonia.NameGenerator/Domain/IClassResolver.cs b/src/Avalonia.NameGenerator/Domain/IClassResolver.cs new file mode 100644 index 0000000000..5f83e2cca3 --- /dev/null +++ b/src/Avalonia.NameGenerator/Domain/IClassResolver.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Domain/ICodeGenerator.cs b/src/Avalonia.NameGenerator/Domain/ICodeGenerator.cs new file mode 100644 index 0000000000..885182fd91 --- /dev/null +++ b/src/Avalonia.NameGenerator/Domain/ICodeGenerator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Avalonia.NameGenerator.Domain +{ + internal interface ICodeGenerator + { + string GenerateCode(string className, string nameSpace, IEnumerable names); + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Domain/INameGenerator.cs b/src/Avalonia.NameGenerator/Domain/INameGenerator.cs new file mode 100644 index 0000000000..9b12fdfe39 --- /dev/null +++ b/src/Avalonia.NameGenerator/Domain/INameGenerator.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Avalonia.NameGenerator.Domain +{ + internal interface INameGenerator + { + IReadOnlyList GenerateNameReferences(IEnumerable additionalFiles); + } + + internal record GeneratedPartialClass + { + public string FileName { get; } + public string Content { get; } + + public GeneratedPartialClass(string fileName, string content) + { + FileName = fileName; + Content = content; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Domain/INameResolver.cs b/src/Avalonia.NameGenerator/Domain/INameResolver.cs new file mode 100644 index 0000000000..08623c20a1 --- /dev/null +++ b/src/Avalonia.NameGenerator/Domain/INameResolver.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Avalonia.NameGenerator.Domain +{ + internal interface INameResolver + { + IReadOnlyList 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; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Generator.props b/src/Avalonia.NameGenerator/Generator.props index f41d0800bd..a0f3171ddb 100644 --- a/src/Avalonia.NameGenerator/Generator.props +++ b/src/Avalonia.NameGenerator/Generator.props @@ -1,10 +1,5 @@  - - true - - - @@ -12,5 +7,5 @@ - + \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Generator/AvaloniaNameGenerator.cs b/src/Avalonia.NameGenerator/Generator/AvaloniaNameGenerator.cs new file mode 100644 index 0000000000..f35e256db4 --- /dev/null +++ b/src/Avalonia.NameGenerator/Generator/AvaloniaNameGenerator.cs @@ -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 GenerateNameReferences(IEnumerable 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; + } + } + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Resolver/FindControlNameGenerator.cs b/src/Avalonia.NameGenerator/Generator/FindControlNameGenerator.cs similarity index 71% rename from src/Avalonia.NameGenerator/Resolver/FindControlNameGenerator.cs rename to src/Avalonia.NameGenerator/Generator/FindControlNameGenerator.cs index 857f9d1ac2..971ce84407 100644 --- a/src/Avalonia.NameGenerator/Resolver/FindControlNameGenerator.cs +++ b/src/Avalonia.NameGenerator/Generator/FindControlNameGenerator.cs @@ -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 names) + public string GenerateCode(string className, string nameSpace, IEnumerable names) { var namedControls = names .Select(info => " " + diff --git a/src/Avalonia.NameGenerator/Generator/XamlXClassResolver.cs b/src/Avalonia.NameGenerator/Generator/XamlXClassResolver.cs new file mode 100644 index 0000000000..319f097382 --- /dev/null +++ b/src/Avalonia.NameGenerator/Generator/XamlXClassResolver.cs @@ -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 _onTypeInvalid; + private ResolvedClass _resolvedClass; + + public XamlXClassResolver( + RoslynTypeSystem typeSystem, + MiniCompiler compiler, + bool checkTypeValidity = false, + Action onTypeInvalid = null) + { + _checkTypeValidity = checkTypeValidity; + _onTypeInvalid = onTypeInvalid; + _typeSystem = typeSystem; + _compiler = compiler; + } + + public ResolvedClass ResolveClass(string xaml) + { + _resolvedClass = null; + var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary + { + {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() { } + } +} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Resolver/XamlXNameResolver.cs b/src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs similarity index 87% rename from src/Avalonia.NameGenerator/Resolver/XamlXNameResolver.cs rename to src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs index 2ad9e3d4c4..532ac78543 100644 --- a/src/Avalonia.NameGenerator/Resolver/XamlXNameResolver.cs +++ b/src/Avalonia.NameGenerator/Generator/XamlXNameResolver.cs @@ -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 _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 ResolveNames(string xaml) { diff --git a/src/Avalonia.NameGenerator/NameReferenceGenerator.cs b/src/Avalonia.NameGenerator/NameReferenceGenerator.cs index 61955ee9f5..710d907d9b 100644 --- a/src/Avalonia.NameGenerator/NameReferenceGenerator.cs +++ b/src/Avalonia.NameGenerator/NameReferenceGenerator.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.NameGenerator.Compiler; +using Avalonia.NameGenerator.Domain; +using Avalonia.NameGenerator.Generator; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using System.Text; -using Avalonia.NameGenerator.Resolver; using Microsoft.CodeAnalysis.CSharp; [assembly: InternalsVisibleTo("Avalonia.NameGenerator.Tests")] @@ -15,190 +13,61 @@ namespace Avalonia.NameGenerator [Generator] public class NameReferenceGenerator : ISourceGenerator { - private const string INamedType = "Avalonia.INamed"; - private const string AttributeName = "GenerateTypedNameReferencesAttribute"; - private const string AttributeFile = "GenerateTypedNameReferencesAttribute"; - private const string AttributeCode = @"// - -using System; - -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -internal sealed class GenerateTypedNameReferencesAttribute : Attribute { } -"; - private static readonly SymbolDisplayFormat SymbolDisplayFormat = new SymbolDisplayFormat( - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | - SymbolDisplayGenericsOptions.IncludeTypeConstraints | - SymbolDisplayGenericsOptions.IncludeVariance); - - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver()); - } + public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) { - context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8)); - if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver)) - { - return; - } - var compilation = (CSharpCompilation)context.Compilation; - var nameResolver = new XamlXNameResolver(compilation); - var nameGenerator = new FindControlNameGenerator(); - var symbols = UnpackAnnotatedTypes(context, compilation, receiver); - if (symbols == null) + var types = new RoslynTypeSystem(compilation); + var compiler = MiniCompiler.CreateDefault(types, MiniCompiler.AvaloniaXmlnsDefinitionAttribute); + + INameGenerator avaloniaNameGenerator = + new AvaloniaNameGenerator( + new XamlXClassResolver(types, compiler, true, type => ReportInvalidType(context, type)), + new XamlXNameResolver(compiler), + new FindControlNameGenerator()); + + try { - return; + var partials = avaloniaNameGenerator.GenerateNameReferences(context.AdditionalFiles); + foreach (var partial in partials) context.AddSource(partial.FileName, partial.Content); } - - foreach (var typeSymbol in symbols) + catch (Exception exception) { - var xamlFileName = $"{typeSymbol.Name}.xaml"; - var aXamlFileName = $"{typeSymbol.Name}.axaml"; - var relevantXamlFile = context - .AdditionalFiles - .FirstOrDefault(text => - text.Path.EndsWith(xamlFileName) || - text.Path.EndsWith(aXamlFileName)); - - if (relevantXamlFile is null) - { - context.ReportDiagnostic( - Diagnostic.Create( - new DiagnosticDescriptor( - "AXN0001", - $"Unable to discover the relevant Avalonia XAML file for {typeSymbol.Name}.", - "Unable to discover the relevant Avalonia XAML file " + - $"neither at {xamlFileName} nor at {aXamlFileName}", - "Usage", - DiagnosticSeverity.Warning, - true), - Location.None)); - continue; - } - - try - { - var nameSpace = typeSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat); - var xaml = relevantXamlFile.GetText()!.ToString(); - var names = nameResolver.ResolveNames(xaml); - var sourceCode = nameGenerator.GenerateNames(typeSymbol.Name, nameSpace, names); - context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); - } - catch (Exception exception) - { - context.ReportDiagnostic( - Diagnostic.Create( - new DiagnosticDescriptor( - "AXN0002", - $"Unhandled exception occured while generating typed Name references for {typeSymbol.Name}.", - $"Unhandled exception occured while generating typed Name references: {exception}", - "Usage", - DiagnosticSeverity.Warning, - true), - Location.None)); - } + ReportUnhandledError(context, exception); } } - private static IReadOnlyList UnpackAnnotatedTypes( - GeneratorExecutionContext context, - CSharpCompilation existingCompilation, - NameReferenceSyntaxReceiver nameReferenceSyntaxReceiver) + private static void ReportUnhandledError(GeneratorExecutionContext context, Exception error) { - var allowedNameGenerator = context - .GetMSBuildProperty("AvaloniaNameGenerator", "false") - .Equals("true", StringComparison.OrdinalIgnoreCase); - - var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options; - var compilation = existingCompilation.AddSyntaxTrees( - CSharpSyntaxTree.ParseText( - SourceText.From(AttributeCode, Encoding.UTF8), - options)); - - var symbols = new List(); - var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName); - foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses) - { - var model = compilation.GetSemanticModel(candidateClass.SyntaxTree); - var typeSymbol = (INamedTypeSymbol)model.GetDeclaredSymbol(candidateClass); - if (InheritsFrom(typeSymbol, INamedType) == false) - { - continue; - } - if (allowedNameGenerator == false) - { - var relevantAttribute = typeSymbol! - .GetAttributes() - .FirstOrDefault(attr => attr.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default)); - - if (relevantAttribute == null) - { - continue; - } - } - - var isPartial = candidateClass - .Modifiers - .Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword)); - - if (isPartial) - { - symbols.Add(typeSymbol); - } - else - { - var missingPartialKeywordMessage = - $"The type {typeSymbol?.Name} should be declared with the 'partial' keyword " + - "as it is either annotated with the [GenerateTypedNameReferences] attribute, " + - "or the property is set to 'true' in the C# project file (it is set " + - "to 'true' by default). In order to skip the processing of irrelevant files, put " + - "false into your .csproj file as " + - " descendant and decorate only relevant view classes with the " + - "[GenerateTypedNameReferences] attribute."; - - context.ReportDiagnostic( - Diagnostic.Create( - new DiagnosticDescriptor( - "AXN0003", - missingPartialKeywordMessage, - missingPartialKeywordMessage, - "Usage", - DiagnosticSeverity.Warning, - true), - Location.None)); - } - } - - return symbols; + const string message = "Unhandled exception occured while generating typed Name references. " + + "Please file an issue: https://github.com/avaloniaui/avalonia.namegenerator"; + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "AXN0002", + message, + error.ToString(), + "Usage", + DiagnosticSeverity.Warning, + true), + Location.None)); } - static bool InheritsFrom(INamedTypeSymbol symbol, string typeName) - { - while (true) - { - if (symbol.ToString() == typeName) - { - return true; - } - if (symbol.BaseType != null) - { - var intefaces = symbol.AllInterfaces; - foreach (var @interface in intefaces) - { - if (@interface.ToString() == typeName) - { - return true; - } - } - symbol = symbol.BaseType; - continue; - } - break; - } - return false; + private static void ReportInvalidType(GeneratorExecutionContext context, string typeName) + { + var message = $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + + $"The type '{typeName}' does not exist in the assembly."; + context.ReportDiagnostic( + Diagnostic.Create( + new DiagnosticDescriptor( + "AXN0001", + message, + message, + "Usage", + DiagnosticSeverity.Error, + true), + Location.None)); } } } diff --git a/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs b/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs deleted file mode 100644 index f74a54c467..0000000000 --- a/src/Avalonia.NameGenerator/NameReferenceSyntaxReceiver.cs +++ /dev/null @@ -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 CandidateClasses { get; } = new List(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax) - CandidateClasses.Add(classDeclarationSyntax); - } - } -} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Resolver/INameGenerator.cs b/src/Avalonia.NameGenerator/Resolver/INameGenerator.cs deleted file mode 100644 index b531ebe098..0000000000 --- a/src/Avalonia.NameGenerator/Resolver/INameGenerator.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Avalonia.NameGenerator.Resolver -{ - internal interface INameGenerator - { - string GenerateNames(string className, string nameSpace, IEnumerable names); - } -} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/Resolver/INameResolver.cs b/src/Avalonia.NameGenerator/Resolver/INameResolver.cs deleted file mode 100644 index fde121aa1c..0000000000 --- a/src/Avalonia.NameGenerator/Resolver/INameResolver.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; - -namespace Avalonia.NameGenerator.Resolver -{ - internal interface INameResolver - { - IReadOnlyList 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; - } - } - } -} \ No newline at end of file diff --git a/src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs b/src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs deleted file mode 100644 index 6c30ac512f..0000000000 --- a/src/Avalonia.NameGenerator/SourceGeneratorContextExtensions.cs +++ /dev/null @@ -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(); - } -} diff --git a/version.json b/version.json index 7aa55b7351..9d636ccb2a 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.2.0-preview", + "version": "0.2.1-preview", "assemblyVersion": { "precision": "revision" },