Browse Source

fix: Xaml Compiler error when code-behind class contains a `DllImport` method (#12882)

* fix: Avalonia.Generators not found

if you put SourceGenerators.props in a project with path are like this:"C:\GitHub\Avalonia\sample\mobile\android\ANoteSample\ANoteSample.csproj", the Compiler is not able to found `Avalonia.Generators`

* test: Add test #10046 Xaml Compiler error when code-behind class contains a DllImport method

* fix: Xaml Compiler error when code-behind class contains a DllImport method

* fix: Address Review

* fix: ValidateApiDiff has thrown an exception
workgroupengineering 2 năm trước cách đây
mục cha
commit
1cb271fb37

+ 21 - 1
Avalonia.sln

@@ -273,7 +273,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit.Uni
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Browser", "samples\MobileSandbox.Browser\MobileSandbox.Browser.csproj", "{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFiles", "TestFiles", "{9D6AEF22-221F-4F4B-B335-A4BA510F002C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks", "{5BF0C3B8-E595-4940-AB30-2DA206C2F085}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -666,6 +674,14 @@ Global
 		{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}.Release|Any CPU.Build.0 = Release|Any CPU
+		{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -748,6 +764,10 @@ Global
 		{2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+		{B0FD6A48-FBAB-4676-B36A-DE76B0922B12} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}
+		{0A948D71-99C5-43E9-BACB-B0BA59EA25B4} = {5BF0C3B8-E595-4940-AB30-2DA206C2F085}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 1 - 1
build/SourceGenerators.props

@@ -15,7 +15,7 @@
 
   <ItemGroup Condition="'$(IncludeAvaloniaGenerators)' == 'true'">
     <ProjectReference
-      Include="../../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
+      Include="$(MSBuildThisFileDirectory)/../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
       OutputItemType="Analyzer"
       ReferenceOutputAssembly="false"
       PrivateAssets="all" />

+ 1 - 1
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@@ -484,7 +484,7 @@ namespace Avalonia.Build.Tasks
 
                             var foundXamlLoader = false;
                             // Find AvaloniaXamlLoader.Load(this) or AvaloniaXamlLoader.Load(sp, this) and replace it with !XamlIlPopulateTrampoline(this)
-                            foreach (var method in classTypeDefinition.Methods.ToArray())
+                            foreach (var method in classTypeDefinition.Methods.Where(m => m.Body is not null).ToArray())
                             {
                                 var i = method.Body.Instructions;
                                 for (var c = 1; c < i.Count; c++)

+ 39 - 0
tests/Avalonia.Build.Tasks.UnitTest/Avalonia.Build.Tasks.UnitTest.csproj

@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net472</TargetFramework>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <OutputType>Library</OutputType>
+    <IsPackable>false</IsPackable>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <Configuration Condition="'$(Configuration)'==''">Debug</Configuration>
+  </PropertyGroup>
+
+  <Import Project="..\..\build\Moq.props" />
+  <Import Project="..\..\build\Rx.props" />
+  <Import Project="..\..\build\HarfBuzzSharp.props" />
+  <Import Project="..\..\build\XUnit.props" />
+  <Import Project="..\..\build\SharedVersion.props" />
+  <ItemGroup>
+    <Content Include="..\TestFiles\BuildTasks\PInvoke\bin\$(Configuration)\netstandard2.0\PInvoke.dll" Link="Assets\PInvoke.dll">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\TestFiles\BuildTasks\PInvoke\bin\$(Configuration)\netstandard2.0\PInvoke.dll.refs" Link="Assets\PInvoke.dll.refs">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
+  </ItemGroup>
+  
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" />
+    <!-- Ensure PInvoke.csproj is build before Avalonia.Build.Tasks.UnitTest -->
+    <ProjectReference Include="..\TestFiles\BuildTasks\PInvoke\PInvoke.csproj" 
+                      SetConfiguration="Configuration=$(Configuration)"
+                      SetTargetFramework="TargetFramework=netstandard2.0"
+                      ReferenceOutputAssembly="false" 
+                      PrivateAssets="all" />
+  </ItemGroup>
+</Project>

+ 38 - 0
tests/Avalonia.Build.Tasks.UnitTest/CompileAvaloniaXamlTaskTest.cs

@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+using System.Reflection;
+using Xunit;
+
+namespace Avalonia.Build.Tasks.UnitTest;
+
+public class CompileAvaloniaXamlTaskTest
+{
+
+    [Fact]
+    public void Does_Not_Fail_When_Codebehind_Contains_DllImport()
+    {
+        using var engine = UnitTestBuildEngine.Start();
+        var basePath = Path.Combine(Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath), "Assets");
+        var originalAssemblyPath = Path.Combine(basePath,
+            "PInvoke.dll");
+        var referencesPath = Path.Combine(basePath,
+                "PInvoke.dll.refs");
+        var compiledAssemblyPath = "PInvoke.dll";
+
+        Assert.True(File.Exists(originalAssemblyPath), $"The original {originalAssemblyPath} don't exists.");
+
+        new CompileAvaloniaXamlTask()
+        {
+            AssemblyFile = originalAssemblyPath,
+            ReferencesFilePath = referencesPath,
+            OutputPath = compiledAssemblyPath,
+            RefAssemblyFile = null,
+            BuildEngine = engine,
+            ProjectDirectory = Directory.GetCurrentDirectory(),
+            VerifyIl = true
+        }.Execute();
+        Assert.Equal(0, engine.Errors.Count);
+    }
+
+
+}

+ 96 - 0
tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngine.cs

@@ -0,0 +1,96 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Build.Framework;
+using Xunit;
+
+namespace Avalonia.Build.Tasks.UnitTest;
+
+/// <summary>
+/// This is fake BuildEngine using for testing build task
+/// at moment it manage only <see cref="BuildErrorEventArgs"/> and <see cref="BuildWarningEventArgs"/>
+/// other messages are ignored/>
+/// </summary>
+internal class UnitTestBuildEngine : IBuildEngine, IDisposable
+{
+    private readonly bool _treatWarningAsError;
+    private readonly bool _assertOnDispose;
+    private readonly List<UnitTestBuildEngineMessage> _errors = new();
+
+    /// <summary>
+    /// Start new instance of <see cref="UnitTestBuildEngine"/>
+    /// </summary>
+    /// <param name="continueOnError">if it is <c>false</c> immediately assert error</param>
+    /// <param name="treatWarningAsError">if it is <c>true</c> treat warning as error</param>
+    /// <param name="assertOnDispose">if it is <c>true</c> assert on dispose if there are any errors.</param>
+    /// <returns></returns>
+    public static UnitTestBuildEngine Start(bool continueOnError = false,
+        bool treatWarningAsError = false,
+        bool assertOnDispose = false) =>
+        new UnitTestBuildEngine(continueOnError, treatWarningAsError, assertOnDispose);
+
+    private UnitTestBuildEngine(bool continueOnError,
+        bool treatWarningAsError,
+        bool assertOnDispose)
+    {
+        ContinueOnError = continueOnError;
+        _treatWarningAsError = treatWarningAsError;
+        _assertOnDispose = assertOnDispose;
+    }
+
+    public bool ContinueOnError { get; }
+
+    public int LineNumberOfTaskNode { get; }
+
+    public int ColumnNumberOfTaskNode { get; }
+
+    public string ProjectFileOfTaskNode { get; }
+
+    public IReadOnlyList<UnitTestBuildEngineMessage> Errors => _errors;
+
+    public bool BuildProjectFile(string projectFileName,
+        string[] targetNames,
+        IDictionary globalProperties,
+        IDictionary targetOutputs)
+        => throw new NotImplementedException();
+
+    public void Dispose()
+    {
+        if (_assertOnDispose && _errors.Count > 0)
+        {
+            Assert.Fail("There is one o more errors.");
+        }
+    }
+
+
+    public void LogCustomEvent(CustomBuildEventArgs e)
+    {
+    }
+
+    public void LogMessageEvent(BuildMessageEventArgs e)
+    {
+    }
+
+    public void LogErrorEvent(BuildErrorEventArgs e)
+    {
+        var message = UnitTestBuildEngineMessage.From(e);
+        _errors.Add(message);
+        if (!ContinueOnError)
+        {
+            Assert.Fail(message.Message);
+        }
+    }
+
+    public void LogWarningEvent(BuildWarningEventArgs e)
+    {
+        if (_treatWarningAsError)
+        {
+            var message = UnitTestBuildEngineMessage.From(e);
+            _errors.Add(message);
+            if (!ContinueOnError)
+            {
+                Assert.Fail(message.Message);
+            }
+        }
+    }
+}

+ 39 - 0
tests/Avalonia.Build.Tasks.UnitTest/UnitTestBuildEngineMessage.cs

@@ -0,0 +1,39 @@
+using Microsoft.Build.Framework;
+
+namespace Avalonia.Build.Tasks.UnitTest;
+
+enum MessageSource
+{
+    Unknown,
+    ErrorEvent,
+    MessageEvent,
+    CustomEvent,
+    WarningEvent
+}
+
+record class UnitTestBuildEngineMessage
+{
+    private UnitTestBuildEngineMessage(MessageSource Type, LazyFormattedBuildEventArgs Source)
+    {
+        this.Type = Type;
+        this.Source = Source;
+        Message = Source.Message;
+    }
+
+    public MessageSource Type { get; }
+    public LazyFormattedBuildEventArgs Source { get; }
+    public string Message { get; }
+
+    public static UnitTestBuildEngineMessage From(BuildWarningEventArgs buildWarning) =>
+        new UnitTestBuildEngineMessage(MessageSource.WarningEvent, buildWarning);
+
+    public static UnitTestBuildEngineMessage From(BuildMessageEventArgs buildMessage) =>
+        new UnitTestBuildEngineMessage(MessageSource.MessageEvent, buildMessage);
+
+    public static UnitTestBuildEngineMessage From(BuildErrorEventArgs buildError) =>
+        new UnitTestBuildEngineMessage(MessageSource.ErrorEvent, buildError);
+
+    public static UnitTestBuildEngineMessage From(CustomBuildEventArgs customBuild) =>
+        new UnitTestBuildEngineMessage(MessageSource.CustomEvent, customBuild);
+
+}

+ 8 - 0
tests/TestFiles/BuildTasks/PInvoke/App.axaml

@@ -0,0 +1,8 @@
+<Application
+    xmlns="https://github.com/avaloniaui" 
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    x:Class="PInvoke.App">
+    <Application.Styles>
+        <FluentTheme />
+    </Application.Styles>
+</Application>

+ 21 - 0
tests/TestFiles/BuildTasks/PInvoke/App.axaml.cs

@@ -0,0 +1,21 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+
+namespace PInvoke;
+
+public class App : Application
+{
+    public override void Initialize()
+    {
+        AvaloniaXamlLoader.Load(this);
+    }
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+        {
+            desktopLifetime.MainWindow = new MainWindow();
+        }
+    }
+}

+ 4 - 0
tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml

@@ -0,0 +1,4 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        x:Class="PInvoke.MainWindow">
+</Window>

+ 22 - 0
tests/TestFiles/BuildTasks/PInvoke/MainWindow.axaml.cs

@@ -0,0 +1,22 @@
+using System.Runtime.InteropServices;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace PInvoke;
+
+public partial class MainWindow : Window
+{
+    [DllImport(@"libhello")]
+    extern static int add(int a, int b);
+
+    public MainWindow()
+    {
+        InitializeComponent();
+    }
+
+    protected override void OnLoaded(RoutedEventArgs e)
+    {
+        base.OnLoaded(e);
+        var x = add(1, 2);
+    }
+}

+ 26 - 0
tests/TestFiles/BuildTasks/PInvoke/PInvoke.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks>
+    <TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
+    <!--<AvaloniaXamlIlDebuggerLaunch>true</AvaloniaXamlIlDebuggerLaunch>-->
+    <EnableAvaloniaXamlCompilation>false</EnableAvaloniaXamlCompilation>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\..\..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
+    <ProjectReference Include="..\..\..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
+    <ProjectReference Include="..\..\..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
+  </ItemGroup>
+
+  <Import Project="..\..\..\..\build\SampleApp.props" />
+  <Import Project="..\..\..\..\build\ReferenceCoreLibraries.props" />
+  <Import Project="..\..\..\..\build\BuildTargets.targets" />
+  <Import Project="..\..\..\..\build\SourceGenerators.props" />
+</Project>

+ 14 - 0
tests/TestFiles/BuildTasks/PInvoke/Program.cs

@@ -0,0 +1,14 @@
+using Avalonia;
+
+namespace PInvoke;
+
+public class Program
+{
+    static void Main(string[] args) => BuildAvaloniaApp()
+        .StartWithClassicDesktopLifetime(args);
+
+    public static AppBuilder BuildAvaloniaApp() =>
+        AppBuilder.Configure<App>()
+            .UsePlatformDetect()
+            .LogToTrace();
+}