diff --git a/src/tools/Avalonia.Generators/Common/EquatableList.cs b/src/tools/Avalonia.Generators/Common/EquatableList.cs index 2b4c8a184d..fd65c668f0 100644 --- a/src/tools/Avalonia.Generators/Common/EquatableList.cs +++ b/src/tools/Avalonia.Generators/Common/EquatableList.cs @@ -9,6 +9,8 @@ namespace Avalonia.Generators.Common; internal class EquatableList(IList collection) : ReadOnlyCollection(collection), IEquatable> { + public static readonly EquatableList Empty = new([]); + public bool Equals(EquatableList? other) { // If the other list is null or a different size, they're not equal diff --git a/src/tools/Avalonia.Generators/GeneratorExtensions.cs b/src/tools/Avalonia.Generators/GeneratorExtensions.cs index 9553dddc46..8911ca2b20 100644 --- a/src/tools/Avalonia.Generators/GeneratorExtensions.cs +++ b/src/tools/Avalonia.Generators/GeneratorExtensions.cs @@ -1,14 +1,9 @@ -using System; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace Avalonia.Generators; internal static class GeneratorExtensions { - private const string UnhandledErrorDescriptorId = "AXN0002"; - private const string InvalidTypeDescriptorId = "AXN0001"; - public static string GetMsBuildProperty( this AnalyzerConfigOptions options, string name, @@ -17,27 +12,4 @@ internal static class GeneratorExtensions options.TryGetValue($"build_property.{name}", out var value); return value ?? defaultValue; } - - public static DiagnosticDescriptor NameGeneratorUnhandledError(Exception error) => new( - UnhandledErrorDescriptorId, - title: "Unhandled exception occurred while generating typed Name references. " + - "Please file an issue: https://github.com/avaloniaui/Avalonia", - messageFormat: error.Message, - description: error.ToString(), - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static DiagnosticDescriptor NameGeneratorInvalidType(string typeName) => new( - InvalidTypeDescriptorId, - title: $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + - $"The type '{typeName}' does not exist in the assembly.", - messageFormat: $"Avalonia x:Name generator was unable to generate names for type '{typeName}'. " + - $"The type '{typeName}' does not exist in the assembly.", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static void Report(this SourceProductionContext context, DiagnosticDescriptor diagnostics) => - context.ReportDiagnostic(Diagnostic.Create(diagnostics, Location.None)); } diff --git a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs index ba0d0d7579..a1cec53ed9 100644 --- a/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs +++ b/src/tools/Avalonia.Generators/NameGenerator/AvaloniaNameIncrementalGenerator.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; +using System.Xml; +using System.Xml.Linq; using Avalonia.Generators.Common; using Avalonia.Generators.Common.Domain; using Avalonia.Generators.Compiler; using Microsoft.CodeAnalysis; -using XamlX.Transform; +using Microsoft.CodeAnalysis.Text; +using XamlX; namespace Avalonia.Generators.NameGenerator; @@ -62,39 +64,52 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator .Select(static (file, cancellationToken) => { cancellationToken.ThrowIfCancellationRequested(); - var text = file.GetText(cancellationToken); - var diagnostics = new List(); - if (text is not null) + var xaml = file.GetText(cancellationToken)?.ToString(); + if (xaml is null) { - try - { - var xaml = text.ToString(); - var viewResolver = new XamlXViewResolver(s_noopCompiler); - var view = viewResolver.ResolveView(xaml, cancellationToken); - if (view is null) - { - return null; - } - - var nameResolver = new XamlXNameResolver(); - var xmlNames = nameResolver.ResolveXmlNames(view.Xaml, cancellationToken); + return null; + } - return new XmlClassInfo( - new ResolvedXmlView(view, xmlNames), - new EquatableList(diagnostics)); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) + ResolvedXmlView? resolvedXmlView; + DiagnosticFactory? diagnosticFactory = null; + var location = new FileLinePositionSpan(file.Path, default); + try + { + var viewResolver = new XamlXViewResolver(s_noopCompiler); + var view = viewResolver.ResolveView(xaml, cancellationToken); + if (view is null) { - diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); - return new XmlClassInfo(null, new EquatableList(diagnostics)); + return null; } + + var xmlNames = EquatableList.Empty; + var nameResolver = new XamlXNameResolver(); + xmlNames = nameResolver.ResolveXmlNames(view.Xaml, cancellationToken); + + resolvedXmlView = new ResolvedXmlView(view, xmlNames); + } + catch (OperationCanceledException) + { + throw; + } + catch (XmlException ex) + { + diagnosticFactory = new(NameGeneratorDiagnostics.ParseFailed, new(file.Path, GetLinePositionSpan(ex)), new([ex.Message])); + + resolvedXmlView = ex is XamlParseException ? TryExtractTypeFromXml(xaml) : null; + } + catch (XamlTypeSystemException ex) + { + diagnosticFactory = new(NameGeneratorDiagnostics.ParseFailed, location, new([ex.Message])); + resolvedXmlView = TryExtractTypeFromXml(xaml); + } + catch (Exception ex) + { + diagnosticFactory = GetInternalErrorDiagnostic(location, ex); + resolvedXmlView = null; } - return null; + return new XmlClassInfo(file.Path, resolvedXmlView, diagnosticFactory); }) .Where(request => request is not null) .WithTrackingName(TrackingNames.ParsedXamlClasses); @@ -119,15 +134,20 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator var hasDevToolsReference = compiler.TypeSystem.FindAssembly("Avalonia.Diagnostics") is not null; var nameResolver = new XamlXNameResolver(); - var diagnostics = new List(classInfo!.Diagnostics); + var diagnostics = new List(2); + if (classInfo?.Diagnostic != null) + { + diagnostics.Add(classInfo.Diagnostic); + } + ResolvedView? view = null; - if (classInfo.XmlView is { } xmlView) + if (classInfo?.XmlView is { } xmlView) { var type = compiler.TypeSystem.FindType(xmlView.FullName); if (type is null) { - diagnostics.Add(GeneratorExtensions.NameGeneratorInvalidType(xmlView.FullName)); + diagnostics.Add(new(NameGeneratorDiagnostics.InvalidType, new(classInfo.FilePath, default), new([xmlView.FullName]))); } else if (type.IsAvaloniaStyledElement()) { @@ -147,17 +167,22 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator resolvedNames.Add(nameResolver .ResolveName(clrType, xmlName.Name, xmlName.FieldModifier)); } + catch (XmlException ex) + { + diagnostics.Add(new(NameGeneratorDiagnostics.NamedElementFailed, + new(classInfo.FilePath, GetLinePositionSpan(ex)), new([xmlName.Name, ex.Message]))); + } catch (Exception ex) { - diagnostics.Add(GeneratorExtensions.NameGeneratorUnhandledError(ex)); + diagnostics.Add(GetInternalErrorDiagnostic(new(classInfo.FilePath, default), ex)); } } - view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), new EquatableList(resolvedNames)); + view = new ResolvedView(xmlView, type.IsAvaloniaWindow(), new(resolvedNames)); } } - return new ResolvedClassInfo(view, hasDevToolsReference, new EquatableList(diagnostics)); + return new ResolvedClassInfo(view, hasDevToolsReference, new(diagnostics)); }) .WithTrackingName(TrackingNames.ResolvedNamesProvider); @@ -165,9 +190,9 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator { var (info, options) = pair; - foreach (var diagnostic in info!.Diagnostics) + foreach (var diagnostic in info.Diagnostics) { - context.Report(diagnostic); + context.ReportDiagnostic(diagnostic.Create()); } if (info.View is { } view && options.AvaloniaNameGeneratorFilterByNamespace.Matches(view.Namespace)) @@ -200,12 +225,53 @@ public class AvaloniaNameIncrementalGenerator : IIncrementalGenerator }); } + private static DiagnosticFactory GetInternalErrorDiagnostic(FileLinePositionSpan location, Exception ex) => + new(NameGeneratorDiagnostics.InternalError, location, new([ex.ToString().Replace('\n', '*').Replace('\r', '*')])); + + /// + /// Fallback in case XAML parsing fails. Extracts just the class name and namespace of the root element. + /// + private static ResolvedXmlView? TryExtractTypeFromXml(string xaml) + { + try + { + var document = XDocument.Parse(xaml); + var classValue = document.Root.Attribute(XName.Get("Class", XamlNamespaces.Xaml2006))?.Value; + if (classValue?.LastIndexOf('.') is { } lastDotIndex && lastDotIndex != -1) + { + return new(classValue.Substring(lastDotIndex + 1), classValue.Substring(0, lastDotIndex), EquatableList.Empty); + } + } + catch + { + // ignore + } + return null; + } + + private static LinePositionSpan GetLinePositionSpan(XmlException ex) + { + var position = new LinePosition(Math.Max(0, ex.LineNumber - 1), Math.Max(0, ex.LinePosition - 1)); + return new(position, position); + } + internal record XmlClassInfo( + string FilePath, ResolvedXmlView? XmlView, - EquatableList Diagnostics); + DiagnosticFactory? Diagnostic); internal record ResolvedClassInfo( ResolvedView? View, bool CanAttachDevTools, - EquatableList Diagnostics); + EquatableList Diagnostics); + + /// + /// Avoid holding references to because it can hold references to , , etc. + /// + internal record DiagnosticFactory(DiagnosticDescriptor Descriptor, FileLinePositionSpan LinePosition, EquatableList FormatArguments) + { + public Diagnostic Create() => Diagnostic.Create(Descriptor, + Location.Create(LinePosition.Path, default, new(LinePosition.StartLinePosition, LinePosition.EndLinePosition)), + messageArgs: [.. FormatArguments]); + } } diff --git a/src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs b/src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs new file mode 100644 index 0000000000..53c1a43855 --- /dev/null +++ b/src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace Avalonia.Generators.NameGenerator; + +internal static class NameGeneratorDiagnostics +{ + private const string Category = "Avalonia.NameGenerator"; + private const string BugReportLink = "https://github.com/AvaloniaUI/Avalonia/issues/new/choose"; + + // Name generation errors should typicially be warnings, because that allows the compile to proceed and + // reach the point at which code errors are reported. These can give the user actionable information + // about what they need to fix, which the name generator doesn't have. + + public static readonly DiagnosticDescriptor InvalidType = new( + "AXN0001", $"Invalid type", + "Avalonia could not generate code-behind properties or the InitializeContext method because the x:Class type '{0}' was not found in the project", + Category, + defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + + [SuppressMessage("MicrosoftCodeAnalysisDesign", "RS1032:Define diagnostic message correctly", Justification = "Printing internal exception")] + public static readonly DiagnosticDescriptor InternalError = new( + "AXN0002", "Internal error", + messageFormat: $"Avalonia encountered an internal error while generating code-behind properties and/or the InitializeContext method. " + + $"Please file a bug report at {BugReportLink}. The exception is {{0}}", + Category, + DiagnosticSeverity.Error, true, + helpLinkUri: BugReportLink); + + public static readonly DiagnosticDescriptor ParseFailed = new( + "AXN0003", $"XAML error", + "Avalonia could not generate code-behind properties for named elements due to a XAML error: {0}", + Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor NamedElementFailed = new( + "AXN0004", $"XAML error", + "Avalonia could not generate code-behind property for '{0}' due to a XAML error: {1}", + Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); +}