dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.02k stars 4.03k forks source link

Allow Multi-Parsing of same file with different defines. #75537

Open JackTheSpades opened 5 days ago

JackTheSpades commented 5 days ago

Defines can be added for projects or individual files. I propose the ability to have the same source file included multiple times with different defines being present/absent for each parsing.

The main reason why this would be desirable is to have the ability to provide async support without having to write the common parts of the method/class twice. Using partial classes one could simple use the preprocessor directives to provide a different method signature/head and call the corresponding await/async methods when necessary.

Here a quick example of what this would look like.

// class needs to be partial
partial class StreamHelper
{
#if Async
    public static async IAsyncEnumerable<T> ParseLinesAsync<T>(Stream stream, IStringParser<T> parser, [EnumeratorCancellation] CancellationToken cancellationToken)
#else
    public static IEnumerable<T> ParseLines<T>(Stream stream, IStringParser<T> parser)
#endif
    {
        using StreamReader sr = new StreamReader(stream, leaveOpen: true);
        string? line;

        while (true)
        {
#if Async
            line = await sr.ReadLineAsync(cancellationToken).ConfigureAwait(false);
#else
            line = sr.ReadLine();
#endif

            // break at end-of-stream
            if (line is null)
                break;
            // line comment starts with #
            if (line.SkipWhile(c => char.IsWhiteSpace(c)).FirstOrDefault() is '#')
                continue;

            yield return parser.Parse(line);
        }
    }
}

While async is the most common example one would think of, it could also be used in methods that require 99% the same code but only differ somewhere in the middle, with no convenient way to just repurpose the code. I believe, at the moment, this is usually done by adding a private _Internal method with a delegate to handle the task specific situations.

public void DoThingA(object param1, object param2) => DoThingCommonInternal(param1, param2, DoThingAInternal);
public void DoThingB(object param1, object param2) => DoThingCommonInternal(param1, param2, DoThingBInternal);

private void DoThingCommonInternal(object param1, object param2, Action handler)
{
    // common part for A and B

    handler();

    // common part for A and B
}
private void DoThingAInternal() { /* Handler for A specific things */ }
private void DoThingBInternal() { /* Handler for B specific things */ }

When it could be shrunken down to this:

#if THING_A
public void DoThingA(object param1, object param2)
#else
public void DoThingB(object param1, object param2)
#endif
{
    // common part for A and B

#if THING_A
    { /* Handler for A specific things */ }
#else
    { /* Handler for B specific things */ }
#endif

    // common part for A and B
}

Admittedly, whether this is better or worse depends a lot on how different ThingA and ThingB here really are.

A feature like this could be provided either on the necessary file itself, similar to how defines are provided now. Like #multipass Async. Or through the csproj file. Ideally though, the IDE would have to show all different renditions of the file with the different defines being active:

For example: Having #multipass Async;Custom on the file StreamHelper.cs could show up as:

Image

lsoft commented 5 days ago

@JackTheSpades side note: fyi if you need to have async and sync versions of the same method, this can be easily achieved with this source generator. No need to wait new Roslyn stuff.