Skip to content

Commit

Permalink
feat: Add analyzer for missing packages when using ProgressRing or MPE
Browse files Browse the repository at this point in the history
  • Loading branch information
Youssef1313 committed Jun 10, 2024
1 parent 2e81c9f commit 722b2e8
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 0 deletions.
9 changes: 9 additions & 0 deletions doc/articles/uno-build-error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ Invocations to `Dispose` can cause the application to crash in `__NSObject_Dispo

The method `InitializeComponent` should always be called in class constructor. A missing call will lead to hard-to-diagnose bugs. This analyzer reports when it's missing to make issues more apparent.

### UNO0007

**An assembly required for a component is missing**

Some components like `ProgressRing` and `MediaPlayerElement` requires you to reference a specific NuGet package for them to work correctly.

- For `ProgressRing`, it requires Lottie dependency. For more information about adding Lottie to your project, see [Lottie for Uno](xref:Uno.Features.Lottie).
- For `MediaPlayerElement` on WebAssembly or Gtk, it requires `Uno.WinUI.MediaPlayer.WebAssembly` or `Uno.WinUI.MediaPlayer.Skia.Gtk` NuGet package. For more information, see [MediaPlayerElement](xref:Uno.Controls.MediaPlayerElement).

## XAML Errors

### UNOX0001
Expand Down
192 changes: 192 additions & 0 deletions src/Uno.Analyzers.Tests/UnoMissingAssemblyAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.CodeAnalysis.Testing;
using Uno.Analyzers.Tests.Verifiers;
using System.Collections.Immutable;

namespace Uno.Analyzers.Tests;

using Verify = CSharpCodeFixVerifier<UnoMissingAssemblyAnalyzer, EmptyCodeFixProvider>;

[TestClass]
public class UnoMissingAssemblyAnalyzerTests
{
#if HAS_UNO_WINUI
private static readonly ImmutableArray<PackageIdentity> _unoPackage = [new PackageIdentity("Uno.WinUI", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithLottie = [new PackageIdentity("Uno.WinUI", "5.2.161"), new PackageIdentity("Uno.WinUI.Lottie", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithMPE = [new PackageIdentity("Uno.WinUI", "5.2.161"), new PackageIdentity("Uno.WinUI.MediaPlayer.WebAssembly", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithGtk = [new PackageIdentity("Uno.WinUI", "5.2.161"), new PackageIdentity("Uno.WinUI.Runtime.Skia.Gtk", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithGtkAndMPE = [new PackageIdentity("Uno.WinUI", "5.2.161"), new PackageIdentity("Uno.WinUI.Runtime.Skia.Gtk", "5.2.161"), new PackageIdentity("Uno.WinUI.MediaPlayer.Skia.Gtk", "5.2.161")];
#else
private static readonly ImmutableArray<PackageIdentity> _unoPackage = [new PackageIdentity("Uno.UI", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithLottie = [new PackageIdentity("Uno.UI", "5.2.161"), new PackageIdentity("Uno.UI.Lottie", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithMPE = [new PackageIdentity("Uno.UI", "5.2.161"), new PackageIdentity("Uno.UI.MediaPlayer.WebAssembly", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithGtk = [new PackageIdentity("Uno.UI", "5.2.161"), new PackageIdentity("Uno.UI.Runtime.Skia.Gtk", "5.2.161")];
private static readonly ImmutableArray<PackageIdentity> _unoPackageWithGtkAndMPE = [new PackageIdentity("Uno.UI", "5.2.161"), new PackageIdentity("Uno.UI.Runtime.Skia.Gtk", "5.2.161"), new PackageIdentity("Uno.UI.MediaPlayer.Skia.Gtk", "5.2.161")];
#endif

private static readonly ReferenceAssemblies _net80WithUno = ReferenceAssemblies.Net.Net80.AddPackages(_unoPackage);
private static readonly ReferenceAssemblies _net80WithUnoAndLottie = ReferenceAssemblies.Net.Net80.AddPackages(_unoPackageWithLottie);
private static readonly ReferenceAssemblies _net80WithUnoAndMPE = ReferenceAssemblies.Net.Net80.AddPackages(_unoPackageWithMPE);
private static readonly ReferenceAssemblies _net80WithUnoAndGtk = ReferenceAssemblies.Net.Net80.AddPackages(_unoPackageWithGtk);
private static readonly ReferenceAssemblies _net80WithUnoAndGtkAndMPE = ReferenceAssemblies.Net.Net80.AddPackages(_unoPackageWithGtkAndMPE);

private const string WasmGlobalConfig = """
is_global = true
build_property.UnoRuntimeIdentifier = WebAssembly
build_property.IsUnoHead = true
""";

private const string SkiaGlobalConfig = """
is_global = true
build_property.UnoRuntimeIdentifier = Skia
build_property.IsUnoHead = true
""";

private const string UnoHeadGlobalConfig = """
is_global = true
build_property.IsUnoHead = true
""";

[TestMethod]
public async Task UseProgressRingWithLottiePackage_NoDiagnostic()
{
var code = """
using Microsoft/* UWP don't rename */.UI.Xaml.Controls;
public class C
{
public ProgressRing M() => new ProgressRing();
}
""";

var test = new Verify.Test()
{
TestCode = code,
FixedCode = code,
ReferenceAssemblies = _net80WithUnoAndLottie,
};

test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", WasmGlobalConfig));

await test.RunAsync();
}

[TestMethod]
public async Task UseProgressRingWithoutLottiePackage_Diagnostic()
{
var code = """
// <auto-generated />
using Microsoft/* UWP don't rename */.UI.Xaml.Controls;
public class C
{
public ProgressRing M() => [|new ProgressRing()|];
}
""";

var test = new Verify.Test()
{
TestCode = code,
FixedCode = code,
ReferenceAssemblies = _net80WithUno,
};

test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", UnoHeadGlobalConfig));

await test.RunAsync();
}

[TestMethod]
public async Task UseMPEWithoutPackageWasm_Diagnostic()
{
var code = """
// <auto-generated />
using Microsoft.UI.Xaml.Controls;
public class C
{
public MediaPlayerElement M() => [|new MediaPlayerElement()|];
}
""";

var test = new Verify.Test()
{
TestCode = code,
FixedCode = code,
ReferenceAssemblies = _net80WithUno,
};

test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", WasmGlobalConfig));

await test.RunAsync();
}

[TestMethod]
public async Task UseMPEWithPackageWasm_NoDiagnostic()
{
var code = """
using Microsoft.UI.Xaml.Controls;
public class C
{
public MediaPlayerElement M() => new MediaPlayerElement();
}
""";

var test = new Verify.Test()
{
TestCode = code,
FixedCode = code,
ReferenceAssemblies = _net80WithUnoAndMPE,
};

test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", WasmGlobalConfig));

await test.RunAsync();
}

[TestMethod]
public async Task UseMPEWithoutPackageGtk_Diagnostic()
{
var code = """
// <auto-generated />
using Microsoft.UI.Xaml.Controls;
public class C
{
public MediaPlayerElement M() => [|new MediaPlayerElement()|];
}
""";

var test = new Verify.Test()
{
TestCode = code,
FixedCode = code,
ReferenceAssemblies = _net80WithUnoAndGtk,
};

test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", UnoHeadGlobalConfig));

await test.RunAsync();
}

[TestMethod]
public async Task UseMPEWithPackageGtk_NoDiagnostic()
{
var code = """
using Microsoft.UI.Xaml.Controls;
public class C
{
public MediaPlayerElement M() => new MediaPlayerElement();
}
""";

var test = new Verify.Test()
{
TestCode = code,
FixedCode = code,
ReferenceAssemblies = _net80WithUnoAndGtkAndMPE,
};

test.TestState.AnalyzerConfigFiles.Add(("/.globalconfig", UnoHeadGlobalConfig));

await test.RunAsync();
}
}
26 changes: 26 additions & 0 deletions src/Uno.Analyzers/SymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis;

namespace Uno.Analyzers;

internal static class SymbolExtensions
{
public static bool DerivesFrom(this INamedTypeSymbol? symbol, INamedTypeSymbol? expectedBaseClass)
{
while (symbol is not null)
{
if (symbol.Equals(expectedBaseClass, SymbolEqualityComparer.Default))
{
return true;
}

symbol = symbol.BaseType;
}

return false;
}
}
101 changes: 101 additions & 0 deletions src/Uno.Analyzers/UnoMissingAssemblyAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#nullable enable

using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Uno.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UnoMissingAssemblyAnalyzer : DiagnosticAnalyzer
{
internal const string Title = "An assembly required for a component is missing";
internal const string MessageFormat = "Using '{0}' requires '{1}' NuGet package to be referenced";
internal const string Category = "Correctness";

internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
#pragma warning disable RS2008 // Enable analyzer release tracking
"Uno0007",
#pragma warning restore RS2008 // Enable analyzer release tracking
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.platform.uno/UNO0007"
);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterCompilationStartAction(context =>
{
var assemblies = context.Compilation.ReferencedAssemblyNames.Select(a => a.Name).ToImmutableHashSet();
var progressRing = context.Compilation.GetTypeByMetadataName("Microsoft" /* UWP don't rename */ + ".UI.Xaml.Controls.ProgressRing");
var mpe = context.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Controls.MediaPlayerElement");
_ = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.UnoRuntimeIdentifier", out var unoRuntimeIdentifier);
_ = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.IsUnoHead", out var isUnoHead);
if (isUnoHead is null || !isUnoHead.Equals("true", StringComparison.OrdinalIgnoreCase))
{
return;
}
context.RegisterOperationAction(context =>
{
var objectCreation = (IObjectCreationOperation)context.Operation;
if (objectCreation.Type is not INamedTypeSymbol type)
{
return;
}
if (type.DerivesFrom(progressRing) && !assemblies.Contains("Uno.UI.Lottie"))
{
const string lottieNuGetPackageName =
#if HAS_UNO_WINUI
"Uno.WinUI.Lottie";
#else
"Uno.UI.Lottie";
#endif
context.ReportDiagnostic(Diagnostic.Create(Rule, objectCreation.Syntax.GetLocation(), "ProgressRing", lottieNuGetPackageName));
}
else if (type.DerivesFrom(mpe))
{
if (unoRuntimeIdentifier?.Equals("WebAssembly", StringComparison.OrdinalIgnoreCase) == true)
{
if (!assemblies.Contains("Uno.UI.MediaPlayer.WebAssembly"))
{
const string wasmMPENuGetPackageName =
#if HAS_UNO_WINUI
"Uno.WinUI.MediaPlayer.WebAssembly";
#else
"Uno.UI.MediaPlayer.WebAssembly";
#endif
context.ReportDiagnostic(Diagnostic.Create(Rule, objectCreation.Syntax.GetLocation(), "MediaPlayer", wasmMPENuGetPackageName));
}
}
else if (assemblies.Contains("Uno.UI.Runtime.Skia.Gtk"))
{
if (!assemblies.Contains("Uno.UI.MediaPlayer.Skia.Gtk"))
{
const string wasmMPENuGetPackageName =
#if HAS_UNO_WINUI
"Uno.WinUI.MediaPlayer.Skia.Gtk";
#else
"Uno.UI.MediaPlayer.Skia.Gtk";
#endif
context.ReportDiagnostic(Diagnostic.Create(Rule, objectCreation.Syntax.GetLocation(), "MediaPlayer", wasmMPENuGetPackageName));
}
}
}
}, OperationKind.ObjectCreation);
});
}
}

0 comments on commit 722b2e8

Please sign in to comment.