Skip to content

Commit 709a5f8

Browse files
authored
Add source-generated Program entry point when no Main() is defined (#183)
1 parent 8439c3d commit 709a5f8

7 files changed

Lines changed: 105 additions & 19 deletions

File tree

CliFx.Generators/Generator.CommandDescriptorEmitter.cs renamed to CliFx.Generators/Binding/CommandDescriptor.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using CliFx.Generators.Binding;
54
using CliFx.Generators.Utils;
65
using CliFx.Generators.Utils.Extensions;
76
using Microsoft.CodeAnalysis;
87

9-
namespace CliFx.Generators;
8+
namespace CliFx.Generators.Binding;
109

11-
public partial class Generator
10+
internal static class CommandDescriptor
1211
{
1312
private static string EmitValidators(
1413
IReadOnlyList<INamedTypeSymbol> validatorTypes,
@@ -394,7 +393,7 @@ DiagnosticReporter diagnostics
394393
false,
395394
{{CSharp.Encode("Shows help text.")}},
396395
new global::CliFx.Activation.BoolScalarInputConverter(),
397-
global::System.Array.Empty<global::CliFx.Activation.InputValidator<bool>>()
396+
global::System.Array.Empty<global::CliFx.Activation.IInputValidator<bool>>()
398397
)
399398
""";
400399
}
@@ -465,7 +464,7 @@ DiagnosticReporter diagnostics
465464
false,
466465
{{CSharp.Encode("Shows version information.")}},
467466
new global::CliFx.Activation.BoolScalarInputConverter(),
468-
global::System.Array.Empty<global::CliFx.Activation.InputValidator<bool>>()
467+
global::System.Array.Empty<global::CliFx.Activation.IInputValidator<bool>>()
469468
)
470469
""";
471470
}
@@ -492,10 +491,7 @@ DiagnosticReporter diagnostics
492491
),
493492
};
494493

495-
private static string EmitCommandDescriptor(
496-
CommandSymbol command,
497-
DiagnosticReporter diagnostics
498-
) =>
494+
internal static string Emit(CommandSymbol command, DiagnosticReporter diagnostics) =>
499495
// lang=csharp
500496
$$"""
501497
// <auto-generated />

CliFx.Generators/Generator.cs renamed to CliFx.Generators/CommandDescriptorGenerator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace CliFx.Generators;
1313

1414
[Generator]
15-
public partial class Generator : IIncrementalGenerator
15+
public class CommandDescriptorGenerator : IIncrementalGenerator
1616
{
1717
public void Initialize(IncrementalGeneratorInitializationContext context)
1818
{
@@ -63,7 +63,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6363

6464
var emitterDiagnostics = new List<Diagnostic>();
6565

66-
var source = EmitCommandDescriptor(
66+
var source = CommandDescriptor.Emit(
6767
item.Command,
6868
new DiagnosticReporter(emitterDiagnostics)
6969
);
@@ -107,7 +107,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
107107
{
108108
ctx.AddSource(
109109
"CommandRegistrations.g.cs",
110-
SourceText.From(EmitCommandRegistrations(commands), Encoding.UTF8)
110+
SourceText.From(CommandRegistration.Emit(commands), Encoding.UTF8)
111111
);
112112
}
113113
);

CliFx.Generators/Generator.CommandRegistrationEmitter.cs renamed to CliFx.Generators/CommandRegistration.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
namespace CliFx.Generators;
1010

11-
public partial class Generator
11+
internal static class CommandRegistration
1212
{
13-
private static string EmitCommandRegistrations(IReadOnlyList<CommandSymbol> commands)
13+
internal static string Emit(IReadOnlyList<CommandSymbol> commands)
1414
{
1515
var orderedCommands = commands.OrderBy(c => c.Name, StringComparer.Ordinal).ToArray();
1616

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace CliFx.Generators;
2+
3+
internal static class ProgramEntryPoint
4+
{
5+
internal static string Emit()
6+
{
7+
// lang=csharp
8+
return """
9+
// <auto-generated />
10+
#nullable enable
11+
12+
namespace CliFx;
13+
14+
/// <summary>
15+
/// Auto-generated entry point for the CliFx-based command-line application.
16+
/// This class is emitted by the source generator when the project does not define
17+
/// a <c>Main</c> method or top-level statements.
18+
/// </summary>
19+
public static class AutoGeneratedCommandLineProgram
20+
{
21+
/// <summary>
22+
/// Builds and runs the command-line application using all commands detected in the current assembly.
23+
/// </summary>
24+
public static async global::System.Threading.Tasks.Task<int> Main() =>
25+
await new global::CliFx.CommandLineApplicationBuilder()
26+
.AddCommandsFromThisAssembly()
27+
.Build()
28+
.RunAsync();
29+
}
30+
""";
31+
}
32+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Text;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.Text;
4+
5+
namespace CliFx.Generators;
6+
7+
[Generator]
8+
public class ProgramEntryPointGenerator : IIncrementalGenerator
9+
{
10+
public void Initialize(IncrementalGeneratorInitializationContext context)
11+
{
12+
var needsEntryPoint = context.CompilationProvider.Select(
13+
static (compilation, cancellationToken) =>
14+
(
15+
compilation.Options.OutputKind == OutputKind.ConsoleApplication
16+
|| compilation.Options.OutputKind == OutputKind.WindowsApplication
17+
) && compilation.GetEntryPoint(cancellationToken) is null
18+
);
19+
20+
context.RegisterSourceOutput(
21+
needsEntryPoint,
22+
static (ctx, needsEntryPoint) =>
23+
{
24+
if (!needsEntryPoint)
25+
return;
26+
27+
ctx.AddSource(
28+
"AutoGeneratedCommandLineProgram.g.cs",
29+
SourceText.From(ProgramEntryPoint.Emit(), Encoding.UTF8)
30+
);
31+
}
32+
);
33+
}
34+
}

CliFx.Tests/ApplicationSpecs.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using CliFx.Tests.Utils.Extensions;
88
using CliWrap;
99
using FluentAssertions;
10+
using Microsoft.CodeAnalysis;
1011
using Xunit;
1112
using Xunit.Abstractions;
1213

@@ -49,6 +50,26 @@ public async Task I_can_create_an_application_with_a_custom_configuration()
4950
exitCode.Should().Be(0);
5051
}
5152

53+
[Fact]
54+
public void I_can_create_an_application_without_a_configuration()
55+
{
56+
// Act
57+
var commands = CommandCompiler.Compile(
58+
// lang=csharp
59+
"""
60+
[Command]
61+
public partial class DefaultCommand : ICommand
62+
{
63+
public ValueTask ExecuteAsync(IConsole console) => default;
64+
}
65+
""",
66+
OutputKind.ConsoleApplication
67+
);
68+
69+
// Assert
70+
commands[0].Type.Assembly.EntryPoint.Should().NotBeNull();
71+
}
72+
5273
[Fact(Timeout = 15000)]
5374
public async Task I_can_use_an_environment_variable_to_make_the_application_wait_for_the_debugger_to_attach()
5475
{

CliFx.Tests/Utils/CommandCompiler.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal static class CommandCompiler
1717
{
1818
private static Compilation CreateCompilation(
1919
string sourceCode,
20+
OutputKind outputKind,
2021
out IReadOnlyList<Diagnostic> diagnostics
2122
)
2223
{
@@ -63,14 +64,15 @@ out IReadOnlyList<Diagnostic> diagnostics
6364
.Append(
6465
MetadataReference.CreateFromFile(typeof(CommandCompiler).Assembly.Location)
6566
),
66-
// DLL to avoid having to define the Main() method
67-
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
67+
new CSharpCompilationOptions(outputKind)
6868
);
6969

70-
// Run the source generator
7170
CSharpGeneratorDriver
7271
.Create(
73-
[new Generator().AsSourceGenerator()],
72+
[
73+
new CommandDescriptorGenerator().AsSourceGenerator(),
74+
new ProgramEntryPointGenerator().AsSourceGenerator(),
75+
],
7476
parseOptions: CSharpParseOptions.Default.WithLanguageVersion(
7577
LanguageVersion.Preview
7678
)
@@ -88,10 +90,11 @@ out var generatorDiagnostics
8890

8991
public static IReadOnlyList<CommandDescriptor> Compile(
9092
string sourceCode,
93+
OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary,
9194
bool treatWarningsAsErrors = false
9295
)
9396
{
94-
var compilation = CreateCompilation(sourceCode, out var diagnostics);
97+
var compilation = CreateCompilation(sourceCode, outputKind, out var diagnostics);
9598

9699
var compilationErrors = diagnostics
97100
.Where(d =>

0 commit comments

Comments
 (0)