using AIStudio.Tests.Models.Generation; using Microsoft.CodeAnalysis; using SourceCodeRules.UsageAnalyzers; namespace AIStudio.Tests.Analyzers; /// /// Checks that the app takes its icons only from the catalog AppIcons. /// /// /// AppIcons names every icon after its meaning and decides there which Lucide icon stands for it. MWAIS0017 refuses /// an icon of LucideIcons or of MudBlazor anywhere else while compiling, so no component picks an icon on its own. /// [TestFixture] public sealed class IconCatalogAnalyzerTests { [TestCase("public string Icon => LucideIcons.HOUSE;", "AIStudio.Tools.LucideIcons.HOUSE", TestName = "A Lucide icon")] [TestCase("public string Icon => MudBlazor.Icons.Material.Filled.Home;", "MudBlazor.Icons.Material.Filled.Home", TestName = "A Material icon of MudBlazor")] [TestCase("public string Icon => Icons.Material.Outlined.Home;", "MudBlazor.Icons.Material.Outlined.Home", TestName = "A Material icon of MudBlazor through its using")] [TestCase("public string Icon => Icons.Custom.Brands.GitHub;", "MudBlazor.Icons.Custom.Brands.GitHub", TestName = "A brand logo of MudBlazor")] [TestCase("public int Rank(string icon) => icon switch { LucideIcons.HOUSE => 1, _ => 0 };", "AIStudio.Tools.LucideIcons.HOUSE", TestName = "A Lucide icon in a pattern")] [TestCase("private const string ICON = LucideIcons.HOUSE;", "AIStudio.Tools.LucideIcons.HOUSE", TestName = "A Lucide icon in a constant of another class")] public async Task AnIconOutsideTheCatalogIsRefused(string member, string icon) { var reported = await AnalyzeAsync(member); using (Assert.EnterMultipleScope()) { Assert.That(reported, Has.Count.EqualTo(1)); Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0017")); Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"'{icon}'")); } } [TestCase("public string Icon => AppIcons.DELETE;", TestName = "A constant of the catalog")] [TestCase("public bool Find(out string icon) => LucideIconRegistry.TryGet(\"house\", out icon);", TestName = "The registry plugins name their icons with")] [TestCase("public string Icon => \"lucide/x\";", TestName = "A plain string")] public async Task EverythingElseIsAllowed(string member) { var reported = await AnalyzeAsync(member); Assert.That(reported, Is.Empty); } [Test] public async Task TheCatalogMayUseBothIconSets() { // // The snippet declares its own AppIcons where the analyzer looks for it. It takes the place of the one // of the app, which the compilation references as well: // const string SOURCE = """ namespace AIStudio.Tools; public static class AppIcons { public const string HOME = LucideIcons.HOUSE; public const string GITHUB = MudBlazor.Icons.Custom.Brands.GitHub; public static class Steps { public const string FIRST = MudBlazor.Icons.Material.Filled.LooksOne; } } """; var reported = await CompilationHarness.AnalyzeAsync(SOURCE, new IconCatalogAnalyzer()); Assert.That(reported, Is.Empty); } [Test] public async Task GeneratedCodeIsCheckedAsWell() { // // The Razor compiler turns the markup of a component into generated code, where most icons are set: // const string SOURCE = """ // namespace Sample; public sealed class Component { public string Icon => AIStudio.Tools.LucideIcons.HOUSE; } """; var reported = await CompilationHarness.AnalyzeAsync(SOURCE, new IconCatalogAnalyzer()); Assert.That(reported, Has.Count.EqualTo(1)); } private static async Task> AnalyzeAsync(string member) { var source = $$""" using AIStudio.Tools; using MudBlazor; namespace Sample; public sealed class Component { {{member}} } """; return await CompilationHarness.AnalyzeAsync(source, new IconCatalogAnalyzer()); } }