dotmake-build / command-line

Declarative syntax for System.CommandLine via attributes for easy, fast, strongly-typed (no reflection) usage. Includes a source generator which automagically converts your classes to CLI commands and properties to CLI options or CLI arguments.
https://dotmake.build
MIT License
79 stars 6 forks source link

Improve exceptions for incorrect usage of "CliCommand" #11

Closed je-sendra closed 8 months ago

je-sendra commented 8 months ago

When trying to invoke a command that is inside of a class that is not marked as "CliCommand", like this:

using DotMake.CommandLine;

namespace cli_test_josep;

public class Program
{
    [CliCommand]
    public class RootCommand
    {
        public void Run()
        {
            Console.WriteLine("Hello Cli!");
        }
    }

    static void Main(string[] args)
    {
        Cli.Run<RootCommand>(args);
    }
}

you get the following error:

Unhandled exception. System.Exception: A registered command builder is not found for 'cli_test_josep.Program+RootCommand'. Please ensure the source generator is running and generating a command builder for your definition class.
   at DotMake.CommandLine.CliCommandBuilder.Get(Type definitionType) in D:\Development\Projects\DotMake-Build\command-line\src\DotMake.CommandLine\CliCommandBuilder.cs:line 149
   at DotMake.CommandLine.Cli.GetConfiguration(Type definitionType, CliSettings settings) in D:\Development\Projects\DotMake-Build\command-line\src\DotMake.CommandLine\Cli.cs:line 78
   at DotMake.CommandLine.Cli.GetConfiguration[TDefinition](CliSettings settings) in D:\Development\Projects\DotMake-Build\command-line\src\DotMake.CommandLine\Cli.cs:line 68
   at DotMake.CommandLine.Cli.Run[TDefinition](String[] args, CliSettings settings) in D:\Development\Projects\DotMake-Build\command-line\src\DotMake.CommandLine\Cli.cs:line 157
   at cli_test_josep.Program.Main(String[] args) in /home/tecnico52/src/cli-test-josep/Program.cs:line 18

which didn't make it immediately obvious for me what the problem was. This happened while I was first testing the project.

After further investigation, I discovered that the same simple System.Exception and message is thrown in case the class is not marked as CliCommand at all. Like this:

using DotMake.CommandLine;

namespace cli_test_josep;

public class Program
{
    static void Main(string[] args)
    {
        Cli.Run<RootCommand>(args);
    }
}

public class RootCommand
{
    public void Run()
    {
        Console.WriteLine("Hello Cli!");
    }
}

And if you forget to mark the parent command as a CliCommand:

using DotMake.CommandLine;

namespace cli_test_josep;

public class Program
{
    static void Main(string[] args)
    {
        Cli.Run<RootCommand.InstallCommand>(args);
    }
}

public class RootCommand
{
    [CliCommand]
    public class InstallCommand
    {
        public void Run()
        {

        }
    }
}

I guess we should change the System.Exception thrown to some custom exceptions like DotMake.CommandLine.ParentClassInvalidException and improve the message to something more explanative.

calacayir commented 8 months ago

FYI, this is fixed with v1.8.2:

I don't think we need custom exception classes for this as in your PR but error messages is now more clear:

public static CliCommandBuilder Get(Type definitionType)
{
    if (!RegisteredDefinitionTypes.TryGetValue(definitionType, out var commandBuilder))
    {
        if (definitionType.GetCustomAttribute<CliCommandAttribute>() == null)
            throw new Exception($"The class '{definitionType.Name}' should have [CliCommand] attribute.");

        var parentWithoutAttribute = definitionType.RecurseWhileNotNull(t => t.DeclaringType)
            .FirstOrDefault(t => t.GetCustomAttribute<CliCommandAttribute>() == null);
        if (parentWithoutAttribute != null) //nested
            throw new Exception($"The parent class '{parentWithoutAttribute.Name}' of nested class '{definitionType.Name}' should have [CliCommand] attribute.");

        throw new Exception($"A registered command builder is not found for '{definitionType.Name}'. " +
                            $"Please ensure the source generator is running and generating a command builder for your definition class.");
    }

    return commandBuilder;
}