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))];
}
}
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))];
}
}