Browse Source
* Add diagnostics support to the Avalonia.Build.Tasks * HostApp and generators build fix * Diagnostics support in Avalonia XAML * Support multiple style selector errors at once * Improve avalonia intrinsics error handling + add tests * Add CompiledBindings multiple errors tests * Fix name generator * Make AvaloniaXamlIlDuplicateSettersChecker a warning * Fix Style_Parser_Throws_For_Duplicate_Setter test * Make XamlLoaderUnreachable respect warnings settings * Add AvaloniaXamlIlStyleValidatorTransformer * Throw more specific exceptions instead of XamlParseException * Get rid of XamlXDiagnosticCode to simplify diagnostics code * Simplify XAML exceptions by avoiding DiagnosticCode in them * Simplify XamlCompilerDiagnosticsFilter * Don't use AvaloniaXamlDiagnosticCodes in Avalonia.Generators * Fix some error handlings in compiler task * Update editor config for in-solution analysis * Update XamlX * Fix missing document path * Avoid Description field usage * Add AvaloniaXamlVerboseExceptions property and make exception formatting customizable * Make Avalonia.NameGenerator not crash if there are XAML errors, members should still be generated * Update tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs --------- Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>pull/13644/head
committed by
GitHub
48 changed files with 923 additions and 342 deletions
@ -1,14 +0,0 @@ |
|||
namespace Avalonia.Build.Tasks |
|||
{ |
|||
public enum BuildEngineErrorCode |
|||
{ |
|||
InvalidXAML = 1, |
|||
DuplicateXClass = 2, |
|||
LegacyResmScheme = 3, |
|||
TransformError = 4, |
|||
EmitError = 4, |
|||
Loader = 5, |
|||
|
|||
Unknown = 9999 |
|||
} |
|||
} |
|||
@ -1,41 +1,110 @@ |
|||
using System; |
|||
using System.Text; |
|||
using System.Xml; |
|||
using Microsoft.Build.Framework; |
|||
using XamlX; |
|||
|
|||
namespace Avalonia.Build.Tasks |
|||
namespace Avalonia.Build.Tasks; |
|||
|
|||
internal static class Extensions |
|||
{ |
|||
static class Extensions |
|||
public static void LogError(this IBuildEngine engine, string code, string file, Exception ex, |
|||
int? lineNumber = null, int? linePosition = null) |
|||
{ |
|||
static string FormatErrorCode(BuildEngineErrorCode code) => FormattableString.Invariant($"AVLN:{(int)code:0000}"); |
|||
|
|||
public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, Exception ex, |
|||
int lineNumber = 0, int linePosition = 0) |
|||
if (lineNumber is null && linePosition is null |
|||
&& ex is XmlException xe) |
|||
{ |
|||
lineNumber = xe.LineNumber; |
|||
linePosition = xe.LinePosition; |
|||
} |
|||
|
|||
#if DEBUG
|
|||
LogError(engine, code, file, ex.ToString(), lineNumber, linePosition); |
|||
LogError(engine, code, file, ex.ToString(), lineNumber, linePosition); |
|||
#else
|
|||
LogError(engine, code, file, ex.Message, lineNumber, linePosition); |
|||
LogError(engine, code, file, ex.Message, lineNumber, linePosition); |
|||
#endif
|
|||
} |
|||
|
|||
public static void LogDiagnostic(this IBuildEngine engine, XamlDiagnostic diagnostic) |
|||
{ |
|||
var message = diagnostic.Title; |
|||
|
|||
if (diagnostic.Severity == XamlDiagnosticSeverity.None) |
|||
{ |
|||
// Skip.
|
|||
} |
|||
|
|||
public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message, |
|||
int lineNumber = 0, int linePosition = 0) |
|||
else if (diagnostic.Severity == XamlDiagnosticSeverity.Warning) |
|||
{ |
|||
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", |
|||
lineNumber, linePosition, lineNumber, linePosition, message, |
|||
engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", diagnostic.Code, diagnostic.Document ?? "", |
|||
diagnostic.LineNumber ?? 0, diagnostic.LinePosition ?? 0, |
|||
diagnostic.LineNumber ?? 0, diagnostic.LinePosition ?? 0, |
|||
message, |
|||
"", "Avalonia")); |
|||
} |
|||
|
|||
public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message, |
|||
int lineNumber = 0, int linePosition = 0) |
|||
else |
|||
{ |
|||
engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", |
|||
lineNumber, linePosition, lineNumber, linePosition, message, |
|||
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", diagnostic.Code, diagnostic.Document ?? "", |
|||
diagnostic.LineNumber ?? 0, diagnostic.LinePosition ?? 0, |
|||
diagnostic.LineNumber ?? 0, diagnostic.LinePosition ?? 0, |
|||
message, |
|||
"", "Avalonia")); |
|||
} |
|||
} |
|||
|
|||
public static void LogError(this IBuildEngine engine, string code, string file, string message, |
|||
int? lineNumber = null, int? linePosition = null) |
|||
{ |
|||
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", code, file ?? "", |
|||
lineNumber ?? 0, linePosition ?? 0, lineNumber ?? 0, linePosition ?? 0, |
|||
message, "", "Avalonia")); |
|||
} |
|||
|
|||
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) |
|||
public static void LogWarning(this IBuildEngine engine, string code, string file, string message, |
|||
int lineNumber = 0, int linePosition = 0) |
|||
{ |
|||
engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", code, file ?? "", |
|||
lineNumber, linePosition, lineNumber, linePosition, message, |
|||
"", "Avalonia")); |
|||
} |
|||
|
|||
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp) |
|||
{ |
|||
engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); |
|||
} |
|||
|
|||
public static string FormatException(this Exception exception, bool verbose) |
|||
{ |
|||
if (!verbose) |
|||
{ |
|||
engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp)); |
|||
return exception.Message; |
|||
} |
|||
|
|||
var builder = new StringBuilder(); |
|||
Process(exception); |
|||
return builder.ToString(); |
|||
|
|||
// Inspired by https://github.com/dotnet/msbuild/blob/e6409007d3a09255431eb28af01835ce1cd316b5/src/Shared/TaskLoggingHelper.cs#L909
|
|||
void Process(Exception exception) |
|||
{ |
|||
if (exception is AggregateException aggregateException) |
|||
{ |
|||
foreach (Exception innerException in aggregateException.Flatten().InnerExceptions) |
|||
{ |
|||
Process(innerException); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
do |
|||
{ |
|||
builder.Append(exception.GetType().Name); |
|||
builder.Append(": "); |
|||
builder.AppendLine(exception.Message); |
|||
builder.AppendLine(exception.StackTrace); |
|||
|
|||
exception = exception.InnerException; |
|||
} while (exception != null); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,70 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Text.RegularExpressions; |
|||
using Microsoft.Build.Framework; |
|||
using XamlX; |
|||
|
|||
namespace Avalonia.Build.Tasks; |
|||
|
|||
// With MSBuild, we don't need to read for TreatWarningsAsErrors/WarningsAsErrors/WarningsNotAsErrors/NoWarn properties.
|
|||
// Just by reporting them with LogWarning MSBuild will do the rest for us.
|
|||
// But we still need to read EditorConfig manually.
|
|||
public class XamlCompilerDiagnosticsFilter |
|||
{ |
|||
private static readonly Regex s_editorConfigRegex = |
|||
new("""avalonia_xaml_diagnostic\.([\w\d]+)\.severity\s*=\s*(\w*)"""); |
|||
|
|||
private readonly Lazy<Dictionary<string, string>> _lazyEditorConfig; |
|||
|
|||
public XamlCompilerDiagnosticsFilter( |
|||
ITaskItem[]? analyzerConfigFiles) |
|||
{ |
|||
_lazyEditorConfig = new Lazy<Dictionary<string, string>>(() => ParseEditorConfigFiles(analyzerConfigFiles)); |
|||
} |
|||
|
|||
internal XamlDiagnosticSeverity Handle(XamlDiagnostic diagnostic) |
|||
{ |
|||
return Handle(diagnostic.Severity, diagnostic.Code); |
|||
} |
|||
|
|||
internal XamlDiagnosticSeverity Handle(XamlDiagnosticSeverity currentSeverity, string diagnosticCode) |
|||
{ |
|||
if (_lazyEditorConfig.Value.TryGetValue(diagnosticCode, out var severity)) |
|||
{ |
|||
return severity.ToLowerInvariant() switch |
|||
{ |
|||
"default" => currentSeverity, |
|||
"error" => XamlDiagnosticSeverity.Error, |
|||
"warning" => XamlDiagnosticSeverity.Warning, |
|||
_ => XamlDiagnosticSeverity.None // "suggestion", "silent", "none"
|
|||
}; |
|||
} |
|||
|
|||
return currentSeverity; |
|||
} |
|||
|
|||
private Dictionary<string, string> ParseEditorConfigFiles(ITaskItem[]? analyzerConfigFiles) |
|||
{ |
|||
// Very naive EditorConfig parser, supporting minimal properties set via regex:
|
|||
var severities = new Dictionary<string, string>(); |
|||
if (analyzerConfigFiles is not null) |
|||
{ |
|||
foreach (var fileItem in analyzerConfigFiles) |
|||
{ |
|||
if (File.Exists(fileItem.ItemSpec)) |
|||
{ |
|||
var fileContent = File.ReadAllText(fileItem.ItemSpec); |
|||
var matches = s_editorConfigRegex.Matches(fileContent); |
|||
foreach (Match match in matches) |
|||
{ |
|||
severities[match.Groups[1].Value] = match.Groups[2].Value; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return severities; |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; |
|||
|
|||
internal static class AvaloniaXamlDiagnosticCodes |
|||
{ |
|||
public const string Unknown = "AVLN9999"; |
|||
|
|||
// XML/XAML parsing errors 1000-1999.
|
|||
public const string ParseError = "AVLN1000"; |
|||
public const string InvalidXAML = "AVLN1001"; |
|||
|
|||
// XAML transform errors 2000-2999.
|
|||
public const string TransformError = "AVLN2000"; |
|||
public const string DuplicateXClass = "AVLN2002"; |
|||
public const string TypeSystemError = "AVLN2003"; |
|||
public const string AvaloniaIntrinsicsError = "AVLN2005"; |
|||
public const string BindingsError = "AVLN2100"; |
|||
public const string DataContextResolvingError = "AVLN2101"; |
|||
public const string StyleTransformError = "AVLN2200"; |
|||
public const string SelectorsTransformError = "AVLN2201"; |
|||
public const string PropertyPathError = "AVLN2202"; |
|||
public const string DuplicateSetterError = "AVLN2203"; |
|||
public const string StyleInMergedDictionaries = "AVLN2204"; |
|||
|
|||
// XAML emit errors 3000-3999.
|
|||
public const string EmitError = "AVLN3000"; |
|||
public const string XamlLoaderUnreachable = "AVLN3001"; |
|||
|
|||
// Generator specific errors 4000-4999.
|
|||
public const string NameGeneratorError = "AVLN4001"; |
|||
|
|||
// Reserved 5000-9998
|
|||
public const string Obsolete = "AVLN5001"; |
|||
|
|||
internal static string XamlXDiagnosticCodeToAvalonia(object xamlException) |
|||
{ |
|||
return xamlException switch |
|||
{ |
|||
XamlXWellKnownDiagnosticCodes wellKnownDiagnosticCodes => wellKnownDiagnosticCodes switch |
|||
{ |
|||
XamlXWellKnownDiagnosticCodes.Obsolete => Obsolete, |
|||
_ => throw new ArgumentOutOfRangeException() |
|||
}, |
|||
|
|||
XamlDataContextException => DataContextResolvingError, |
|||
XamlBindingsTransformException => BindingsError, |
|||
XamlPropertyPathException => PropertyPathError, |
|||
XamlStyleTransformException => StyleTransformError, |
|||
XamlSelectorsTransformException => SelectorsTransformError, |
|||
|
|||
XamlTransformException => TransformError, |
|||
XamlTypeSystemException => TypeSystemError, |
|||
XamlLoadException => EmitError, |
|||
XamlParseException => ParseError, |
|||
|
|||
_ => Unknown |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System.Linq; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Transform; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
|||
|
|||
internal class AvaloniaXamlIlStyleValidatorTransformer : IXamlAstTransformer |
|||
{ |
|||
// See https://github.com/AvaloniaUI/Avalonia/issues/7461
|
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (!(node is XamlAstObjectNode on |
|||
&& context.GetAvaloniaTypes().IStyle.IsAssignableFrom(on.Type.GetClrType()))) |
|||
return node; |
|||
|
|||
if (context.ParentNodes().FirstOrDefault() is XamlAstXamlPropertyValueNode propertyValueNode |
|||
&& propertyValueNode.Property.GetClrProperty() is { } clrProperty |
|||
&& clrProperty.Name == "MergedDictionaries" |
|||
&& clrProperty.DeclaringType == context.GetAvaloniaTypes().ResourceDictionary) |
|||
{ |
|||
var nodeName = on.Type.GetClrType().Name; |
|||
context.ReportDiagnostic(new XamlDiagnostic( |
|||
AvaloniaXamlDiagnosticCodes.StyleInMergedDictionaries, |
|||
XamlDiagnosticSeverity.Warning, |
|||
// Keep it single line, as MSBuild splits multiline warnings into two warnings.
|
|||
$"Including {nodeName} as part of MergedDictionaries will ignore any nested styles." + |
|||
$"Instead, you can add {nodeName} to the Styles collection on the same control or application.", |
|||
node)); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; |
|||
|
|||
internal class XamlDocumentParseException : XamlParseException |
|||
{ |
|||
public string FilePath { get; } |
|||
|
|||
public XamlDocumentParseException(string path, XamlParseException parseException) |
|||
: base(parseException.Message, parseException.LineNumber, parseException.LinePosition) |
|||
{ |
|||
FilePath = path; |
|||
} |
|||
|
|||
public XamlDocumentParseException(string path, string message, IXamlLineInfo lineInfo) |
|||
: base(message, lineInfo.Line, lineInfo.Position) |
|||
{ |
|||
FilePath = path; |
|||
} |
|||
|
|||
public XamlDocumentParseException(IXamlDocumentResource document, string message, IXamlLineInfo lineInfo) |
|||
: this(document.FileSource?.FilePath, message, lineInfo) |
|||
{ |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit aa2223dec1e7c70679fdb73f9d364363a0285adb |
|||
Subproject commit b7ed273273949a5dd9f01e682ab97f61b43697ad |
|||
@ -0,0 +1,134 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Immutable; |
|||
using Avalonia.Styling; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.Xaml.UnitTests.Xaml; |
|||
|
|||
public class AvaloniaIntrinsicsTests : XamlTestBase |
|||
{ |
|||
[Fact] |
|||
public void All_Intrinsics_Are_Parsed_And_Set() |
|||
{ |
|||
var xaml = @"<local:TestIntrinsicsControl
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
TimeSpanProperty='00:10:10' |
|||
ThicknessProperty='1 1 1 1' |
|||
PointProperty='15, 15' |
|||
VectorProperty='16.6, 16.6' |
|||
SizeProperty='20, 20' |
|||
MatrixProperty='1 0 0 1 0 0' |
|||
CornerRadiusProperty='4' |
|||
ColorProperty='#44ff11' |
|||
RelativePointProperty='50%, 50%' |
|||
GridLengthProperty='10*' |
|||
IBrushProperty='#44ff11' |
|||
TextTrimmingProperty='CharacterEllipsis' |
|||
TextDecorationCollectionProperty='Strikethrough' |
|||
WindowTransparencyLevelProperty='AcrylicBlur' |
|||
UriProperty='https://avaloniaui.net/'
|
|||
ThemeVariantProperty='Dark' |
|||
PointsProperty='1, 1, 2, 2' />";
|
|||
|
|||
var target = AvaloniaRuntimeXamlLoader.Parse<TestIntrinsicsControl>(xaml); |
|||
|
|||
Assert.NotNull(target); |
|||
Assert.Equal(new TimeSpan(0, 10, 10), target.TimeSpanProperty); |
|||
Assert.Equal(new Thickness(1), target.ThicknessProperty); |
|||
Assert.Equal(new Thickness(1), target.ThicknessProperty); |
|||
Assert.Equal(new Point(15, 15), target.PointProperty); |
|||
Assert.Equal(new Vector(16.6, 16.6), target.VectorProperty); |
|||
Assert.Equal(new Size(20, 20), target.SizeProperty); |
|||
Assert.Equal(new Matrix(1, 0, 0, 1, 0, 0), target.MatrixProperty); |
|||
Assert.Equal(new CornerRadius(4), target.CornerRadiusProperty); |
|||
Assert.Equal(Color.Parse("#44ff11"), target.ColorProperty); |
|||
Assert.Equal(new RelativePoint(0.5, 0.5, RelativeUnit.Relative), target.RelativePointProperty); |
|||
Assert.Equal(new GridLength(10, GridUnitType.Star), target.GridLengthProperty); |
|||
Assert.Equal(new ImmutableSolidColorBrush(Color.Parse("#44ff11")), target.IBrushProperty); |
|||
Assert.Equal(TextTrimming.CharacterEllipsis, target.TextTrimmingProperty); |
|||
Assert.Equal(TextDecorations.Strikethrough, target.TextDecorationCollectionProperty); |
|||
Assert.Equal(WindowTransparencyLevel.AcrylicBlur, target.WindowTransparencyLevelProperty); |
|||
Assert.Equal(new Uri("https://avaloniaui.net/"), target.UriProperty); |
|||
Assert.Equal(ThemeVariant.Dark, target.ThemeVariantProperty); |
|||
Assert.Equal(new[] { new Point(1, 1), new Point(2, 2) }, target.PointsProperty); |
|||
} |
|||
|
|||
[Fact] |
|||
public void All_Intrinsics_Report_Errors_If_Failed() |
|||
{ |
|||
var xaml = @"<local:TestIntrinsicsControl
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
TimeSpanProperty='00:00:10,1' |
|||
ThicknessProperty='1 1 1' |
|||
PointProperty='15% 15%' |
|||
VectorProperty='16.6. 16.6' |
|||
SizeProperty='20%, 20%' |
|||
MatrixProperty='1 0 1 0 0' |
|||
CornerRadiusProperty='4 1 4' |
|||
ColorProperty='#44ff1' |
|||
RelativePointProperty='50, 50%' |
|||
GridLengthProperty='10%' |
|||
PointsProperty='1, 1, 2' />";
|
|||
// TODO: double check why we don't throw error on other supported types. Should it be warnings?
|
|||
|
|||
var diagnostics = new List<RuntimeXamlDiagnostic>(); |
|||
Assert.Throws<AggregateException>(() => AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml), |
|||
new RuntimeXamlLoaderConfiguration |
|||
{ |
|||
DiagnosticHandler = diagnostic => |
|||
{ |
|||
diagnostics.Add(diagnostic); |
|||
return diagnostic.Severity; |
|||
} |
|||
})); |
|||
|
|||
Assert.Collection( |
|||
diagnostics, |
|||
d => AssertDiagnostic(d, "time span"), |
|||
d => AssertDiagnostic(d, "thickness"), |
|||
d => AssertDiagnostic(d, "point"), |
|||
d => AssertDiagnostic(d, "vector"), |
|||
d => AssertDiagnostic(d, "size"), |
|||
d => AssertDiagnostic(d, "matrix"), |
|||
d => AssertDiagnostic(d, "corner radius"), |
|||
d => AssertDiagnostic(d, "color"), |
|||
d => AssertDiagnostic(d, "relative point"), |
|||
d => AssertDiagnostic(d, "grid length"), |
|||
d => AssertDiagnostic(d, "points list"), |
|||
// Compiler attempts to parse PointsList twice - as a list and as a point.
|
|||
d => AssertDiagnostic(d, "point")); |
|||
|
|||
void AssertDiagnostic(RuntimeXamlDiagnostic runtimeXamlDiagnostic, string contains) |
|||
{ |
|||
Assert.Equal(RuntimeXamlDiagnosticSeverity.Error, runtimeXamlDiagnostic.Severity); |
|||
Assert.Contains(contains, runtimeXamlDiagnostic.Title, StringComparison.OrdinalIgnoreCase); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public class TestIntrinsicsControl : Control |
|||
{ |
|||
public TimeSpan TimeSpanProperty { get; set; } |
|||
|
|||
// public FontFamily FontFamilyProperty { get; set; }
|
|||
public Thickness ThicknessProperty { get; set; } |
|||
public Point PointProperty { get; set; } |
|||
public Vector VectorProperty { get; set; } |
|||
public Size SizeProperty { get; set; } |
|||
public Matrix MatrixProperty { get; set; } |
|||
public CornerRadius CornerRadiusProperty { get; set; } |
|||
public Color ColorProperty { get; set; } |
|||
public RelativePoint RelativePointProperty { get; set; } |
|||
public GridLength GridLengthProperty { get; set; } |
|||
public IBrush IBrushProperty { get; set; } |
|||
public TextTrimming TextTrimmingProperty { get; set; } |
|||
public TextDecorationCollection TextDecorationCollectionProperty { get; set; } |
|||
public WindowTransparencyLevel WindowTransparencyLevelProperty { get; set; } |
|||
public Uri UriProperty { get; set; } |
|||
public ThemeVariant ThemeVariantProperty { get; set; } |
|||
public Points PointsProperty { get; set; } |
|||
} |
|||
Loading…
Reference in new issue