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 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 : 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(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(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(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(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync()) .Single(item => item.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId); var actions = new List(); 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().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(new AutoConfigDiagnostics())).GetAnalyzerDiagnosticsAsync()).Single(item => item.Id == diagnosticId); var actions = new List(); 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().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 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))]; } }