Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reference Microsoft.CodeAnalysis.CSharp version 4.9.2 is completely out of mind #44

Open
NotAsea opened this issue May 3, 2024 · 4 comments

Comments

@NotAsea
Copy link

NotAsea commented May 3, 2024

Sorry if title feel harsh but how can i use source gen version if it reference Microsoft.CodeAnalysis.CSharp 4.9.2 while every tooling in newest Asp.Net core reference 4.5, is this lib suppose to use with preview version of .NET or we suppose to force reference 4.9.2 ??

@NotAsea
Copy link
Author

NotAsea commented May 3, 2024

i forked source gen version and build it myself using standard Microsoft.CodeAnalysis.CSharp 4.5.0 and it work fine, maybe there's some reason i dont know

@NotAsea
Copy link
Author

NotAsea commented May 3, 2024

this loc

if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp11))
{
      return default;
}

i dont see any point to force use 4.9.2 as 4.5.0 already contain definition for CSharp 11 and compatible with good amount of library outhere that target .Net 7+

@NotAsea
Copy link
Author

NotAsea commented May 3, 2024

we can event add branch to detect .Net 6 LanguageVersion.CSharp10 and .Net 7+ LanguageVersion.CSharp11 , with .Net 6 we generate register as

public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints)
    {
       new global::TestRepo.Api.Routes.AccountRoute().MapEndpoints(endpoints);
        return endpoints;
    }

and for .Net 7+

public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints)
    {
        global::TestRepo.Api.Routes.AccountRoute.MapEndpoints(endpoints);
        return endpoints;
    }

@NotAsea
Copy link
Author

NotAsea commented May 3, 2024

i propose my very simple implementation that can use in Net 6 +

// EndpointHandlerGenerator.cs
using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace MinimalHelpers.Routing.Analyzers;

[Generator]
public sealed class EndpointHandlerGenerator : IIncrementalGenerator
{
    private static bool isLang11OrUp;

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var provider = context
            .SyntaxProvider.CreateSyntaxProvider(
                static (node, _) =>
                    node is ClassDeclarationSyntax classDeclaration
                    && classDeclaration.HasOrPotentiallyHasBaseTypes(),
                static (context, token) =>
                {
                    var compilation = context.SemanticModel.Compilation;
                    if (!compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp10))
                    {
                        return default;
                    }

                    token.ThrowIfCancellationRequested();
                    isLang11OrUp = compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp11);
                    return (ClassDeclarationSyntax)context.Node;
                }
            )
            .Where(static c => c is not null);

        var compilation = context.CompilationProvider.Combine(provider.Collect());
        context.RegisterSourceOutput(compilation, Execute!);
    }

    private static void Execute(
        SourceProductionContext context,
        (Compilation Compilation, ImmutableArray<ClassDeclarationSyntax> Classes) tuple
    )
    {
        //#if DEBUG
        //        if (!Debugger.IsAttached)
        //        {
        //            Debugger.Launch();
        //        }
        //#endif

        var (_, classes) = tuple;

        var @interface = GetIEndpointRouteHandlerBuilderInterface();
        context.AddSource("IEndpointRouteHandlerBuilder.g.cs", @interface);

        const string prefixCode = """
            // <auto-generated />
            namespace Microsoft.AspNetCore.Routing;

            #nullable enable annotations
            #nullable disable warnings

            /// <summary>
            /// Provides extension methods for <see cref="IEndpointRouteBuilder" /> to add route handlers.
            /// </summary>
            public static class EndpointRouteBuilderExtensions
            {
                /// <summary>
                /// Automatically registers all the route endpoints defined in classes that implement the <see cref="IEndpointRouteHandlerBuilder "/> interface.
                /// </summary>
                /// <param name="endpoints">The <see cref="IEndpointRouteBuilder" /> to add routes to.</param>
                public static IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints)
                {
            """;

        const string suffixCode = """

                    return endpoints;
                }
            }
            """;

        var codeBuilder = new StringBuilder();
        codeBuilder.AppendLine(prefixCode);

        foreach (
            var @class in classes.Where(c =>
                c.BaseList?.Types.Any(t => t.Type.ToString() == "IEndpointRouteHandlerBuilder")
                    is true
            )
        )
        {
            var @namespace = GetNamespace(@class);
            var fullClassName = $"{@namespace}.{@class.Identifier.Text}".TrimStart('.');

            codeBuilder.AppendLine(
                $"              {(!isLang11OrUp ? $"new global::{fullClassName}()" : $"global::{fullClassName}")}.MapEndpoints(endpoints);"
            );
        }

        codeBuilder.AppendLine(suffixCode);

        context.AddSource("EndpointRouteBuilderExtensions.g.cs", codeBuilder.ToString());
    }

    private static string GetIEndpointRouteHandlerBuilderInterface() =>
        $$"""
            // <auto-generated />
            namespace Microsoft.AspNetCore.Routing;

            #nullable enable annotations
            #nullable disable warnings

            /// <summary>
            /// Defines a contract for a class that holds one or more route handlers that must be registered by the application.
            /// </summary>
            public interface IEndpointRouteHandlerBuilder
            {
                /// <summary>
                /// Maps route endpoints to the corresponding handlers.
                /// </summary>
                {{(
                isLang11OrUp ? "static" : ""
            )}} abstract void MapEndpoints(IEndpointRouteBuilder endpoints);
            }
            """;

    // determine the namespace the class/enum/struct is declared in, if any
    // https://andrewlock.net/creating-a-source-generator-part-5-finding-a-type-declarations-namespace-and-type-hierarchy/
    private static string GetNamespace(BaseTypeDeclarationSyntax syntax)
    {
        var @namespace = string.Empty;
        var potentialNamespaceParent = syntax.Parent;

        while (
            potentialNamespaceParent
                is not null
                    and not NamespaceDeclarationSyntax
                    and not FileScopedNamespaceDeclarationSyntax
        )
        {
            potentialNamespaceParent = potentialNamespaceParent.Parent;
        }

        if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
        {
            @namespace = namespaceParent.Name.ToString();

            while (namespaceParent.Parent is NamespaceDeclarationSyntax parent)
            {
                @namespace = $"{namespaceParent.Name}.{@namespace}";
                namespaceParent = parent;
            }
        }

        return @namespace;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant