Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using FluentAssertions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace CSharpFunctionalExtensions.HttpResults.Generators.Tests;

/// <summary>
/// Tests that verify the source generator correctly handles error types defined in external assemblies.
/// This addresses the issue where short type names were used instead of fully qualified names,
/// causing compilation errors when error types came from referenced assemblies.
/// </summary>
public class ExternalAssemblyErrorTypeTests
{
[Fact]
public void GeneratesFullyQualifiedTypeNamesForExternalErrorType_WithToOkHttpResult()
{
// Create a fake external assembly with an error type
var externalErrorTypeCode = """
namespace MyApp.Infrastructure.Errors;

public sealed record NotFoundError(string Message);
""";

var externalAssemblySyntaxTree = CSharpSyntaxTree.ParseText(externalErrorTypeCode);
var externalCompilation = CSharpCompilation.Create(
"ExternalAssembly",
[externalAssemblySyntaxTree],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

// Emit the external assembly to get a reference
using var ms = new MemoryStream();
var emitResult = externalCompilation.Emit(ms);
emitResult.Success.Should().BeTrue("External assembly should compile");

var externalAssemblyReference = MetadataReference.CreateFromStream(new MemoryStream(ms.ToArray()));

// Create mapper in the main assembly that references the external error type
var mapperCode = """
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using CSharpFunctionalExtensions.HttpResults;
using MyApp.Infrastructure.Errors;

namespace MyApp.Api.ErrorMappers;

public class NotFoundErrorMapper : IResultErrorMapper<NotFoundError, ProblemHttpResult>
{
public ProblemHttpResult Map(NotFoundError error) =>
TypedResults.Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Not Found",
detail: error.Message
);
}
""";

var (_, generatedSource) = GeneratorTestHelper.RunGenerator(mapperCode, [externalAssemblyReference]);

generatedSource.Should().Contain("this Result<T,global::MyApp.Infrastructure.Errors.NotFoundError> result");
}

[Fact]
public void GeneratesFullyQualifiedTypeNamesForExternalErrorType_MultipleMappers()
{
// Create fake external assemblies with error types
var externalErrorTypesCode = """
namespace MyApp.Infrastructure.Errors;

public sealed record NotFoundError(string Message);
public sealed record ValidationError(string Message);
""";

var externalAssemblySyntaxTree = CSharpSyntaxTree.ParseText(externalErrorTypesCode);
var externalCompilation = CSharpCompilation.Create(
"ExternalAssembly",
[externalAssemblySyntaxTree],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

using var ms = new MemoryStream();
var emitResult = externalCompilation.Emit(ms);
emitResult.Success.Should().BeTrue();

var externalAssemblyReference = MetadataReference.CreateFromStream(new MemoryStream(ms.ToArray()));

// Create multiple mappers
var mappersCode = """
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using CSharpFunctionalExtensions.HttpResults;
using MyApp.Infrastructure.Errors;

namespace MyApp.Api.ErrorMappers;

public class NotFoundErrorMapper : IResultErrorMapper<NotFoundError, ProblemHttpResult>
{
public ProblemHttpResult Map(NotFoundError error) =>
TypedResults.Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Not Found",
detail: error.Message
);
}

public class ValidationErrorMapper : IResultErrorMapper<ValidationError, BadRequest<ProblemHttpResult>>
{
public BadRequest<ProblemHttpResult> Map(ValidationError error) =>
TypedResults.BadRequest(
TypedResults.Problem(
statusCode: StatusCodes.Status400BadRequest,
title: "Validation Error",
detail: error.Message
)
);
}
""";

var (_, generatedSource) = GeneratorTestHelper.RunGenerator(mappersCode, [externalAssemblyReference]);

generatedSource.Should().Contain("this Result<T,global::MyApp.Infrastructure.Errors.ValidationError");
generatedSource.Should().Contain("this Result<T,global::MyApp.Infrastructure.Errors.NotFoundError");
}

[Fact]
public void NoCompilationErrorsWithExternalErrorType()
{
// Create external assembly
var externalErrorTypeCode = """
namespace MyApp.Infrastructure.Errors;

public sealed record NotFoundError(string Message);
""";

var externalAssemblySyntaxTree = CSharpSyntaxTree.ParseText(externalErrorTypeCode);
var externalCompilation = CSharpCompilation.Create(
"ExternalAssembly",
[externalAssemblySyntaxTree],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

using var ms = new MemoryStream();
var emitResult = externalCompilation.Emit(ms);
emitResult.Success.Should().BeTrue();

var externalAssemblyReference = MetadataReference.CreateFromStream(new MemoryStream(ms.ToArray()));

var mapperCode = """
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using CSharpFunctionalExtensions.HttpResults;
using MyApp.Infrastructure.Errors;

namespace MyApp.Api.ErrorMappers;

public class NotFoundErrorMapper : IResultErrorMapper<NotFoundError, ProblemHttpResult>
{
public ProblemHttpResult Map(NotFoundError error) =>
TypedResults.Problem(
statusCode: StatusCodes.Status404NotFound,
title: "Not Found",
detail: error.Message
);
}
""";

var (diagnostics, generatedSource) = GeneratorTestHelper.RunGenerator(mapperCode, [externalAssemblyReference]);

diagnostics.Should().BeEmpty("Should not report diagnostics about NotFoundError being unresolved");
generatedSource.Should().NotBeNullOrEmpty("Should generate extension methods");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace CSharpFunctionalExtensions.HttpResults.Generators.Tests;

public static class GeneratorTestHelper
{
public static (IEnumerable<Diagnostic> Diagnostics, string GeneratedSource) RunGenerator(
string sourceCode,
IEnumerable<MetadataReference>? additionalReferences = null
)
{
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);

var references = new List<MetadataReference>
{
MetadataReference.CreateFromFile(typeof(ResultExtensionsGenerator).Assembly.Location),
MetadataReference.CreateFromFile(typeof(IResultErrorMapper<,>).Assembly.Location),
};

if (additionalReferences != null)
references.AddRange(additionalReferences);

var compilation = CSharpCompilation.Create(
"TestAssembly",
[syntaxTree],
references.OfType<PortableExecutableReference>(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

var generator = new ResultExtensionsGenerator();
var driver = CSharpGeneratorDriver.Create(generator);

driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);

var sourceFiles = outputCompilation
.SyntaxTrees.Where(tree => tree.FilePath.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase))
.Select(tree => tree.GetText().ToString());

var generatedSource = string.Join("\n\n", sourceFiles);

return (diagnostics, generatedSource);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ public class DocumentCreationErrorMapper2 : IResultErrorMapper<DocumentCreationE
}
""";

var diagnostics = ResultExtensionsGeneratorTestHelper.RunGenerator(sourceCode).ToList();
var (diagnostics, _) = GeneratorTestHelper.RunGenerator(sourceCode);

diagnostics.Count.Should().Be(1);
var diagnosticsList = diagnostics.ToList();

var diagnostic = diagnostics[0];
diagnosticsList.Should().HaveCount(1);

var diagnostic = diagnosticsList[0];

diagnostic.Id.Should().Be("CFEHTTPR002");
diagnostic.Severity.Should().Be(DiagnosticSeverity.Error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ public DocumentCreationErrorMapper3(string foo) { }
}
""";

var diagnostics = ResultExtensionsGeneratorTestHelper.RunGenerator(sourceCode).ToList();
var (diagnostics, _) = GeneratorTestHelper.RunGenerator(sourceCode);

diagnostics.Count.Should().Be(1);
var diagnosticsList = diagnostics.ToList();

var diagnostic = diagnostics[0];
diagnosticsList.Should().HaveCount(1);

var diagnostic = diagnosticsList[0];

diagnostic.Id.Should().Be("CFEHTTPR004");
diagnostic.Severity.Should().Be(DiagnosticSeverity.Error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
using System.Text;
using CSharpFunctionalExtensions.HttpResults.Generators.Utils;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CSharpFunctionalExtensions.HttpResults.Generators.Builders;

public abstract class ClassBuilder
{
private const string MapMethodName = "Map";
private readonly Compilation? _compilation;
private readonly List<ClassDeclarationSyntax> _mapperClasses;
private readonly HashSet<string> _requiredNamespaces;

protected ClassBuilder(HashSet<string> requiredNamespaces, List<ClassDeclarationSyntax> mapperClasses)
protected ClassBuilder(List<ClassDeclarationSyntax> mapperClasses, Compilation? compilation = null)
{
_requiredNamespaces = requiredNamespaces;
_mapperClasses = mapperClasses;
_compilation = compilation;
}

private static string DefaultUsings =>
"""
using CSharpFunctionalExtensions;
using IResult = Microsoft.AspNetCore.Http.IResult;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using System.Text;
using IResult = Microsoft.AspNetCore.Http.IResult;
""";

public string SourceFileName => $"{ClassName}.g.cs";
Expand All @@ -42,13 +44,6 @@ public string Build()
sourceBuilder.AppendLine();
sourceBuilder.AppendLine(DefaultUsings);

_requiredNamespaces
.Where(@namespace => !@namespace.StartsWith("global"))
.Distinct()
.Select(@namespace => $"using {@namespace};")
.ToList()
.ForEach(@using => sourceBuilder.AppendLine(@using));

sourceBuilder.AppendLine();
sourceBuilder.AppendLine(ClassSummary);

Expand All @@ -68,8 +63,8 @@ public string Build()
if (mappingMethod.ParameterList.Parameters.Count != 1)
throw new ArgumentException($"Mapping method in class {mapperClassName} must have exactly one parameter.");

var resultErrorType = mappingMethod.ParameterList.Parameters[0].Type!.ToString();
var httpResultType = mappingMethod.ReturnType.ToString();
var resultErrorType = GetFullyQualifiedTypeName(mapperClass, mappingMethod.ParameterList.Parameters[0].Type!);
var httpResultType = mappingMethod.ReturnType!.ToString();

foreach (var methodGenerator in MethodGenerators)
{
Expand All @@ -83,4 +78,18 @@ public string Build()

return sourceBuilder.ToString();
}

private string GetFullyQualifiedTypeName(ClassDeclarationSyntax mapperClass, TypeSyntax typeSyntax)
{
if (_compilation == null)
return typeSyntax.ToString();

var semanticModel = _compilation.GetSemanticModel(mapperClass.SyntaxTree);
var typeInfo = semanticModel.GetTypeInfo(typeSyntax);

if (typeInfo.Type == null)
return typeSyntax.ToString();

return TypeNameResolver.GetFullyQualifiedTypeName(typeInfo.Type);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using CSharpFunctionalExtensions.HttpResults.Generators.ResultExtensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace CSharpFunctionalExtensions.HttpResults.Generators.Builders;

public class ResultExtensionsClassBuilder(
HashSet<string> requiredNamespaces,
List<ClassDeclarationSyntax> mapperClasses
) : ClassBuilder(requiredNamespaces, mapperClasses)
public class ResultExtensionsClassBuilder(List<ClassDeclarationSyntax> mapperClasses, Compilation? compilation = null)
: ClassBuilder(mapperClasses, compilation)
{
protected override string ClassName => "ResultExtensions";

Expand Down
Loading
Loading