XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.AutoConfig

【DLL】自动实现配置文件的存储

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using XFEExtension.NetCore.AutoConfig.Analyzer.Generator;
using XFEExtension.NetCore.AutoConfig.CodeFix;
using XFEExtension.NetCore.AutoConfig.Diagnostics;
using Xunit;

namespace XFEExtension.NetCore.AutoConfig.Tests;

public sealed class GeneratorAndCodeFixTests
{
    [Fact]
    public void GeneratorUsesSemanticDiscoveryAndUniqueHintNames()
    {
        const string source = """
            namespace Shared
            {
                public abstract partial class ProfileBase : global::XFEExtension.NetCore.AutoConfig.XFEProfile { }
            }
            namespace First
            {
                public partial class Settings : Shared.ProfileBase
                {
                    [global::XFEExtension.NetCore.AutoConfig.ProfileProperty]
                    private int value;
                }
            }
            namespace Second
            {
                public partial class Settings : Shared.ProfileBase
                {
                    [global::XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute]
                    private int value;
                }
            }
            """;

        var (result, outputCompilation) = RunGenerator(source);

        Assert.Equal(2, result.Results.Single().GeneratedSources.Length);
        Assert.Equal(2, result.Results.Single().GeneratedSources.Select(sourceResult => sourceResult.HintName).Distinct().Count());
        Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
    }

    [Fact]
    public void GeneratorOutputMatchesApprovedSnapshot()
    {
        const string source = """
            namespace XFEExtension.NetCore.AutoConfig.Tests;
            [global::XFEExtension.NetCore.AutoConfig.AutoLoadProfile(false)]
            [global::XFEExtension.NetCore.AutoConfig.ProfilePath("custom/settings")]
            public partial class AttributePathProfile : global::XFEExtension.NetCore.AutoConfig.XFEProfile
            {
                [global::XFEExtension.NetCore.AutoConfig.ProfileProperty]
                private string value = "default";
            }
            """;

        var (result, _) = RunGenerator(source);
        var generatedSource = result.Results.Single().GeneratedSources.Single().SourceText.ToString().Replace("\r\n", "\n");
        var hash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(generatedSource)));

        Assert.Contains("[global::System.Xml.Serialization.XmlElementAttribute(\"Value\")]", generatedSource);
        Assert.Contains("public static byte[] ExportProfileBytes()", generatedSource);
        Assert.Contains("public static void ImportProfileBytes(global::System.ReadOnlyMemory<byte> profileContent)", generatedSource);
        Assert.Equal("7B0B7B07FE1271B592876778731820ACB41520A9F836C6D3F7501735F9A839A8", hash);
    }

    [Fact]
    public void GeneratorImplementsNet10PartialProfileProperty()
    {
        const string source = """
            using XFEExtension.NetCore.AutoConfig;
            [AutoLoadProfile(false)]
            public partial class Settings : XFEProfile
            {
                [ProfileProperty]
                public static partial string Name { get; set; } = "default";
            }
            """;

        var (result, outputCompilation) = RunGenerator(source);
        var generatedSource = result.Results.Single().GeneratedSources.Single().SourceText.ToString();

        Assert.Contains("public static partial string Name", generatedSource);
        Assert.Contains("public string InstanceName", generatedSource);
        Assert.Contains("return field;", generatedSource);
        Assert.Contains("__current.InstanceRequestSaveProfile();", generatedSource);
        Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
    }

    [Fact]
    public void GeneratorSupportsCustomPersistedNameOnNaturalPartialProperty()
    {
        const string source = """
            using XFEExtension.NetCore.AutoConfig;
            [AutoLoadProfile(false)]
            public partial class Settings : XFEProfile
            {
                [ProfileProperty("StoredName")]
                public static partial string Name { get; set; } = "default";
            }
            """;

        var (result, outputCompilation) = RunGenerator(source);
        var generatedSource = result.Results.Single().GeneratedSources.Single().SourceText.ToString();

        Assert.Contains("XmlElementAttribute(\"StoredName\")", generatedSource);
        Assert.Contains("JsonPropertyNameAttribute(\"StoredName\")", generatedSource);
        Assert.Contains("PropertyInfoDictionary[\"StoredName\"]", generatedSource);
        Assert.Contains("public static partial string Name", generatedSource);
        Assert.Contains("public string InstanceStoredName", generatedSource);
        Assert.DoesNotContain(outputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
    }

    [Theory]
    [InlineData("public class Settings : XFEProfile { [ProfileProperty] private int value; }", "XFE1001")]
    [InlineData("public partial class Settings : XFEProfile { [ProfileProperty] private int first, second; }", "XFE1003")]
    [InlineData("public partial class Settings<T> : XFEProfile { [ProfileProperty] private int value; }", "XFE1004")]
    [InlineData("public partial class Settings : XFEProfile { public Settings(int value) { } [ProfileProperty] private int value; }", "XFE1005")]
    [InlineData("public partial class Settings : XFEProfile { public static Settings Current { get; set; } = null!; [ProfileProperty] private int value; }", "XFE1006")]
    [InlineData("public partial class Settings { [ProfileProperty] private int value; }", "XFE1008")]
    [InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public static partial int Value { get; init; } }", "XFE1009")]
    [InlineData("public partial class Settings : XFEProfile { [ProfileProperty] public partial int Value { get; set; } }", "XFE1009")]
    public void GeneratorReportsActionableUsageDiagnostics(string declaration, string diagnosticId)
    {
        var source = "using XFEExtension.NetCore.AutoConfig;\n" + declaration;

        var (result, _) = RunGenerator(source);

        Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId);
    }

    [Fact]
    public void GeneratorReportsGeneratedPropertyNameConflicts()
    {
        const string source = """
            using XFEExtension.NetCore.AutoConfig;
            public partial class Settings : XFEProfile
            {
                [ProfileProperty("Value")] private int first;
                [ProfileProperty("Value")] private int second;
            }
            """;

        var (result, _) = RunGenerator(source);

        Assert.Contains(result.Diagnostics, diagnostic => diagnostic.Id == "XFE1002");
    }

    [Fact]
    public async Task AnalyzerReportsEachInvalidHookOnce()
    {
        const string source = """
            using XFEExtension.NetCore.AutoConfig;
            public partial class Settings : XFEProfile
            {
                [ProfileProperty]
                [ProfilePropertyAddGet("System.Console.WriteLine(1)")]
                [ProfilePropertyAddSet("System.Console.WriteLine(value)")]
                private int value;
            }
            """;
        var compilation = CreateCompilation(source);

        var diagnostics = await compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync();

        Assert.Single(diagnostics, diagnostic => diagnostic.Id == AutoConfigDiagnostics.AddGetNoResultErrorId);
        Assert.Single(diagnostics, diagnostic => diagnostic.Id == AutoConfigDiagnostics.AddSetNoSetResultWarningId);
    }

    [Theory]
    [InlineData(".NETCoreApp,Version=v10.0", true)]
    [InlineData(".NETCoreApp,Version=v11.0", true)]
    [InlineData(".NETCoreApp,Version=v9.0", false)]
    public async Task FieldUpgradeSuggestionRequiresNet10OrGreater(string targetFramework, bool expected)
    {
        var source = $$"""
            using XFEExtension.NetCore.AutoConfig;
            [assembly: global::System.Runtime.Versioning.TargetFramework("{{targetFramework}}")]
            public partial class Settings : XFEProfile
            {
                [ProfileProperty]
                private string value = "default";
            }
            """;
        var compilation = CreateCompilation(source);

        var diagnostics = await compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync();

        Assert.Equal(expected, diagnostics.Any(diagnostic => diagnostic.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId));
    }

    [Fact]
    public async Task FieldUpgradeSuggestionRequiresCSharp14()
    {
        const string source = """
            using XFEExtension.NetCore.AutoConfig;
            [assembly: global::System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0")]
            public partial class Settings : XFEProfile
            {
                [ProfileProperty]
                private string value = "default";
            }
            """;
        var compilation = CreateCompilation(source, LanguageVersion.CSharp13);

        var diagnostics = await compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync();

        Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId);
    }

    [Fact]
    public async Task CodeFixUpgradesFieldToPartialPropertyAndRenamesReferences()
    {
        const string source = """
            using XFEExtension.NetCore.AutoConfig;
            using Profile = XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute;
            using SetHook = XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute;
            [assembly: global::System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v10.0")]
            public partial class Settings : XFEProfile
            {
                [Profile]
                [SetHook("Current.value = value")]
                [System.NonSerialized]
                private string value = "default";

                public string ReadValue() => value;
            }
            """;
        using var workspace = new AdhocWorkspace();
        var projectId = ProjectId.CreateNewId();
        var documentId = DocumentId.CreateNewId(projectId);
        var generatedDocumentId = DocumentId.CreateNewId(projectId);
        var consumerDocumentId = DocumentId.CreateNewId(projectId);
        var solution = workspace.CurrentSolution
            .AddProject(projectId, "PartialPropertyCodeFixTest", "PartialPropertyCodeFixTest", LanguageNames.CSharp)
            .WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Preview))
            .WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable))
            .AddMetadataReferences(projectId, GetMetadataReferences())
            .AddDocument(documentId, "Settings.cs", SourceText.From(source))
            .AddDocument(generatedDocumentId, "Settings.Generated.cs", SourceText.From("""
                public partial class Settings
                {
                    [XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerate]
                    public static string Value { get; set; } = string.Empty;
                }
                """))
            .AddDocument(consumerDocumentId, "Consumer.cs", SourceText.From("""
                public static class Consumer
                {
                    public static string Read() => Settings.Value;
                }
                """));
        Assert.True(workspace.TryApplyChanges(solution));
        var document = workspace.CurrentSolution.GetDocument(documentId)!;
        var compilation = (CSharpCompilation)(await document.Project.GetCompilationAsync())!;
        var diagnostic = (await compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync())
            .Single(item => item.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId);
        var actions = new List<CodeAction>();
        var context = new CodeFixContext(document, diagnostic, (action, _) => actions.Add(action), CancellationToken.None);
        var provider = new AutoConfigCodeFixProvider();

        await provider.RegisterCodeFixesAsync(context);
        var operations = await actions.Single().GetOperationsAsync(CancellationToken.None);
        var changedSolution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
        var changedDocument = changedSolution.GetDocument(documentId)!;
        var changedText = (await changedDocument.GetTextAsync()).ToString();

        Assert.Contains("public static partial string Value", changedText);
        Assert.Contains("field:", changedText);
        Assert.Contains("System.NonSerialized", changedText);
        Assert.Contains("[Profile]", changedText);
        Assert.Contains("field = value", changedText);
        Assert.Contains("= \"default\";", changedText);
        Assert.Contains("ReadValue() => Value", changedText);
        var consumerText = (await changedSolution.GetDocument(consumerDocumentId)!.GetTextAsync()).ToString();
        Assert.Contains("Settings.Value", consumerText);
        Assert.DoesNotContain("Settings.Current.Value", consumerText);
        changedSolution = changedSolution.RemoveDocument(generatedDocumentId);
        changedDocument = changedSolution.GetDocument(documentId)!;
        var changedCompilation = (CSharpCompilation)(await changedDocument.Project.GetCompilationAsync())!;
        GeneratorDriver driver = CSharpGeneratorDriver.Create(
            generators: [new ProfilePropertyAutoGenerator().AsSourceGenerator()],
            parseOptions: new CSharpParseOptions(LanguageVersion.Preview));
        driver.RunGeneratorsAndUpdateCompilation(changedCompilation, out var outputCompilation, out _);
        Assert.DoesNotContain(outputCompilation.GetDiagnostics(), item => item.Severity == DiagnosticSeverity.Error);
    }

    [Theory]
    [InlineData("ProfilePropertyAddGet", "System.Console.WriteLine(1)", AutoConfigDiagnostics.AddGetNoResultErrorId, "return Current.value", false)]
    [InlineData("ProfilePropertyAddSet", "System.Console.WriteLine(value)", AutoConfigDiagnostics.AddSetNoSetResultWarningId, "Current.value = value", false)]
    [InlineData("ProfilePropertyAddGet", "System.Console.WriteLine(1)", AutoConfigDiagnostics.AddGetNoResultErrorId, "return field", true)]
    [InlineData("ProfilePropertyAddSet", "System.Console.WriteLine(value)", AutoConfigDiagnostics.AddSetNoSetResultWarningId, "field = value", true)]
    public async Task CodeFixAddsRequiredHookStatement(string attributeName, string existingCode, string diagnosticId, string expectedCode, bool usePartialProperty)
    {
        var member = usePartialProperty
            ? $$"""
                [ProfileProperty]
                [{{attributeName}}("{{existingCode}}")]
                public static partial int Value { get; set; }
                """
            : $$"""
                [ProfileProperty]
                [{{attributeName}}("{{existingCode}}")]
                private int value;
                """;
        var source = $$"""
            using XFEExtension.NetCore.AutoConfig;
            public partial class Settings : XFEProfile
            {
            {{member}}
            }
            """;
        using var workspace = new AdhocWorkspace();
        var projectId = ProjectId.CreateNewId();
        var documentId = DocumentId.CreateNewId(projectId);
        var solution = workspace.CurrentSolution
            .AddProject(projectId, "CodeFixTest", "CodeFixTest", LanguageNames.CSharp)
            .WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Preview))
            .WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))
            .AddMetadataReferences(projectId, GetMetadataReferences())
            .AddDocument(documentId, "CodeFixTest.cs", SourceText.From(source));
        Assert.True(workspace.TryApplyChanges(solution));
        var document = workspace.CurrentSolution.GetDocument(documentId)!;
        var compilation = (CSharpCompilation)(await document.Project.GetCompilationAsync())!;
        var diagnostic = (await compilation.WithAnalyzers(ImmutableArray.Create<DiagnosticAnalyzer>(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync()).Single(item => item.Id == diagnosticId);
        var actions = new List<CodeAction>();
        var context = new CodeFixContext(document, diagnostic, (action, _) => actions.Add(action), CancellationToken.None);
        var provider = new AutoConfigCodeFixProvider();

        await provider.RegisterCodeFixesAsync(context);
        var operations = await actions.Single().GetOperationsAsync(CancellationToken.None);
        var changedSolution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
        var changedText = await changedSolution.GetDocument(documentId)!.GetTextAsync();

        Assert.Contains(expectedCode, changedText.ToString());
    }

    private static (GeneratorDriverRunResult Result, Compilation OutputCompilation) RunGenerator(string source)
    {
        var compilation = CreateCompilation(source);
        GeneratorDriver driver = CSharpGeneratorDriver.Create(
            generators: [new ProfilePropertyAutoGenerator().AsSourceGenerator()],
            parseOptions: new CSharpParseOptions(LanguageVersion.Preview));
        driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _);
        return (driver.GetRunResult(), outputCompilation);
    }

    private static CSharpCompilation CreateCompilation(string source, LanguageVersion languageVersion = LanguageVersion.Preview) => CSharpCompilation.Create(
        "GeneratorTest_" + Guid.NewGuid().ToString("N"),
        [CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(languageVersion))],
        GetMetadataReferences(),
        new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable));

    private static ImmutableArray<MetadataReference> GetMetadataReferences()
    {
        var trustedAssemblies = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))?.Split(Path.PathSeparator) ?? [];
        var paths = trustedAssemblies.Append(typeof(XFEProfile).Assembly.Location).Distinct(StringComparer.OrdinalIgnoreCase);
        return [.. paths.Select(path => MetadataReference.CreateFromFile(path))];
    }
}