From 1a938de66d476bf6fd2bbdee9892bfaf5f3af1a6 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:03:07 +0100 Subject: [PATCH] Add file locations to diagnostics reported by AvaloniaNameIncrementalGenerator (#19934) Add a pure XML parsing fallback which allows InitializeContext to be generated even if XAML parsing failed Improve diagnostic messages and make user errors warnings, so that the compile can continue and fail "properly" later --- .../Common/EquatableList.cs | 2 + .../GeneratorExtensions.cs | 28 ---- .../AvaloniaNameIncrementalGenerator.cs | 144 +++++++++++++----- .../NameGenerator/NameGeneratorDiagnostics.cs | 39 +++++ 4 files changed, 146 insertions(+), 67 deletions(-) create mode 100644 src/tools/Avalonia.Generators/NameGenerator/NameGeneratorDiagnostics.cs 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); +}