diff --git a/src/ArgumentParsing.Generators/ArgumentParserGenerator.CodeGen.cs b/src/ArgumentParsing.Generators/ArgumentParserGenerator.CodeGen.cs index 4e41c9c..77ee018 100644 --- a/src/ArgumentParsing.Generators/ArgumentParserGenerator.CodeGen.cs +++ b/src/ArgumentParsing.Generators/ArgumentParserGenerator.CodeGen.cs @@ -16,7 +16,7 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen var cancellationToken = context.CancellationToken; - var (hierarchy, method, optionsInfo, builtInCommandInfos, additionalCommandHandlers) = parserInfo; + var (hierarchy, method, optionsInfo, errorMessageFormatProvider, builtInCommandInfos, additionalCommandHandlers) = parserInfo; var (qualifiedName, hasAtLeastInternalAccessibility, optionInfos, parameterInfos, remainingParametersInfo, helpTextGeneratorInfo) = optionsInfo; var writer = new CodeWriter(); @@ -215,7 +215,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("if (state > 0 && startsOption)"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.OptionValueIsNotProvidedError(previousArgument));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.OptionValueIsNotProvidedError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.OptionValueIsNotProvidedError, "); + } + writer.WriteLine("previousArgument));"); writer.WriteLine("state = 0;"); writer.CloseBlock(); writer.WriteLine(); @@ -255,7 +260,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("else"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError(arg));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.UnrecognizedArgumentError, "); + } + writer.WriteLine("arg));"); writer.CloseBlock(); writer.WriteLine("continue;"); writer.Ident--; @@ -278,7 +288,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if ((seenOptions & 0b{usageCode.ToString()}) > 0)"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError(\"{info.LongName}\"));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.DuplicateOptionError, "); + } + writer.WriteLine($"\"{info.LongName}\"));"); writer.CloseBlock(); if (info.ParseStrategy == ParseStrategy.Flag) { @@ -297,7 +312,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("default:"); writer.Ident++; writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.UnknownOptionError(latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, arg));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.UnknownOptionError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.UnknownOptionError, "); + } + writer.WriteLine($"latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, arg));"); writer.WriteLine("if (written == 1)"); writer.OpenBlock(); writer.WriteLine("state = -1;"); @@ -359,7 +379,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if ((seenOptions & 0b{usageCode.ToString()}) > 0)"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError(\"{info.ShortName}\"));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.DuplicateOptionError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.DuplicateOptionError, "); + } + writer.WriteLine($"\"{info.ShortName}\"));"); writer.CloseBlock(); if (info.ParseStrategy == ParseStrategy.Flag) { @@ -381,16 +406,18 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen { writer.WriteLine($"if (state <= -10)"); writer.OpenBlock(); - if (hasAnyParameters || optionInfos.Any(static i => i.NullableUnderlyingType is not null || i.SequenceType != SequenceType.None)) - { - writer.WriteLine($"val = slice.{(canUseOptimalSpanBasedAlgorithm ? "Slice" : "Substring")}(i);"); - } + writer.WriteLine($"val = slice.{(canUseOptimalSpanBasedAlgorithm ? "Slice" : "Substring")}(i);"); writer.WriteLine($"latestOptionName = {(canUseOptimalSpanBasedAlgorithm ? "new global::System.ReadOnlySpan(in slice[i - 1])" : "slice[i - 1].ToString()")};"); writer.WriteLine("goto decodeValue;"); writer.CloseBlock(); } writer.WriteLine("errors ??= new();"); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.UnknownOptionError(shortOptionName.ToString(), arg));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.UnknownOptionError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.UnknownOptionError, "); + } + writer.WriteLine("shortOptionName.ToString(), arg));"); writer.WriteLine("state = -1;"); writer.WriteLine("goto continueMainLoop;"); writer.Ident--; @@ -413,12 +440,17 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("decodeValue:", identDelta: -1); writer.WriteLine("switch (state)"); writer.OpenBlock(); - if (!hasAnyParameters && optionInfos.Any(static i => i.ParseStrategy == ParseStrategy.Flag && i.NullableUnderlyingType is null)) + if (!hasAnyParameters && optionInfos.Any(static i => i is { ParseStrategy: ParseStrategy.Flag, NullableUnderlyingType: null })) { writer.WriteLine("case -10:"); writer.Ident++; writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.FlagOptionValueError(latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.FlagOptionValueError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.FlagOptionValueError, "); + } + writer.WriteLine($"latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.WriteLine("break;"); writer.Ident--; } @@ -464,7 +496,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!{nullableUnderlyingType ?? info.Type}.TryParse(val, {numberStyles}, global::System.Globalization.CultureInfo.InvariantCulture, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadOptionValueFormatError, "); + } + writer.WriteLine($"val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -476,7 +513,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!bool.TryParse(val, out {propertyName}_underlying))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadOptionValueFormatError, "); + } + writer.WriteLine($"val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); writer.WriteLine($"{propertyName}_val = {propertyName}_underlying;"); break; @@ -488,7 +530,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!global::System.Enum.TryParse<{nullableUnderlyingType ?? info.Type}>(val, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadOptionValueFormatError, "); + } + writer.WriteLine($"val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -503,7 +550,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("else"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadOptionValueFormatError, "); + } + writer.WriteLine($"val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); break; case ParseStrategy.DateTimeRelated: @@ -514,7 +566,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!{nullableUnderlyingType ?? info.Type}.TryParse(val, global::System.Globalization.CultureInfo.InvariantCulture, global::System.Globalization.DateTimeStyles.None, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadOptionValueFormatError, "); + } + writer.WriteLine($"val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -529,7 +586,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!global::System.TimeSpan.TryParse(val, global::System.Globalization.CultureInfo.InvariantCulture, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError(val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadOptionValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadOptionValueFormatError, "); + } + writer.WriteLine($"val{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}, latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -584,7 +646,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!{nullableUnderlyingType ?? info.Type}.TryParse(val, {numberStyles}, global::System.Globalization.CultureInfo.InvariantCulture, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError(arg, \"{info.Name}\", parameterIndex - 1));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadParameterValueFormatError, "); + } + writer.WriteLine($"arg, \"{info.Name}\", parameterIndex - 1));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -599,7 +666,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!bool.TryParse(arg, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError(arg, \"{info.Name}\", parameterIndex - 1));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadParameterValueFormatError, "); + } + writer.WriteLine($"arg, \"{info.Name}\", parameterIndex - 1));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -614,7 +686,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!global::System.Enum.TryParse<{nullableUnderlyingType ?? info.Type}>(arg, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError(arg, \"{info.Name}\", parameterIndex - 1));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadParameterValueFormatError, "); + } + writer.WriteLine($"arg, \"{info.Name}\", parameterIndex - 1));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -629,7 +706,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("else"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError(arg, \"{info.Name}\", parameterIndex - 1));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadParameterValueFormatError, "); + } + writer.WriteLine($"arg, \"{info.Name}\", parameterIndex - 1));"); writer.CloseBlock(); break; case ParseStrategy.DateTimeRelated: @@ -640,7 +722,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!{nullableUnderlyingType ?? info.Type}.TryParse(val, global::System.Globalization.CultureInfo.InvariantCulture, global::System.Globalization.DateTimeStyles.None, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError(arg, \"{info.Name}\", parameterIndex - 1));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadParameterValueFormatError, "); + } + writer.WriteLine($"arg, \"{info.Name}\", parameterIndex - 1));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -655,7 +742,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!global::System.TimeSpan.TryParse(val, global::System.Globalization.CultureInfo.InvariantCulture, out {propertyName}{(nullableUnderlyingType is not null ? "_underlying" : "_val")}))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError(arg, \"{info.Name}\", parameterIndex - 1));"); + writer.Write($"errors.Add(new global::ArgumentParsing.Results.Errors.BadParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadParameterValueFormatError, "); + } + writer.WriteLine($"arg, \"{info.Name}\", parameterIndex - 1));"); writer.CloseBlock(); if (nullableUnderlyingType is not null) { @@ -670,22 +762,37 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.Ident++; if (remainingParametersInfo is null) { - if (optionInfos.Any(static i => i.ParseStrategy == ParseStrategy.Flag && i.NullableUnderlyingType is null)) + writer.WriteLine("errors ??= new();"); + + if (optionInfos.Any(static i => i is { ParseStrategy: ParseStrategy.Flag, NullableUnderlyingType: null })) { - writer.WriteLine("errors ??= new();"); writer.WriteLine("if (state == -10)"); writer.OpenBlock(); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.FlagOptionValueError(latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.FlagOptionValueError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.FlagOptionValueError, "); + } + writer.WriteLine($"latestOptionName{(canUseOptimalSpanBasedAlgorithm ? ".ToString()" : string.Empty)}));"); writer.CloseBlock(); writer.WriteLine("else"); writer.OpenBlock(); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError(arg));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.UnrecognizedArgumentError, "); + } + writer.WriteLine("arg));"); writer.CloseBlock(); } else { - writer.WriteLine("errors ??= new();"); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError(arg));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.UnrecognizedArgumentError, "); + } + writer.WriteLine("arg));"); } } else @@ -706,7 +813,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!{type}.TryParse(val, {numberStyles}, global::System.Globalization.CultureInfo.InvariantCulture, out {propertyName}_val))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError(arg, parameterIndex - 1));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadRemainingParameterValueFormatError, "); + } + writer.WriteLine("arg, parameterIndex - 1));"); writer.CloseBlock(); writer.WriteLine($"remainingParametersBuilder.Add({propertyName}_val);"); break; @@ -715,7 +827,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!bool.TryParse(arg, out {propertyName}_val))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError(arg, parameterIndex - 1));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadRemainingParameterValueFormatError, "); + } + writer.WriteLine("arg, parameterIndex - 1));"); writer.CloseBlock(); writer.WriteLine($"remainingParametersBuilder.Add({propertyName}_val);"); break; @@ -724,7 +841,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!global::System.Enum.TryParse<{type}>(arg, out {propertyName}_val))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError(arg, parameterIndex - 1));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadRemainingParameterValueFormatError, "); + } + writer.WriteLine("arg, parameterIndex - 1));"); writer.CloseBlock(); writer.WriteLine($"remainingParametersBuilder.Add({propertyName}_val);"); break; @@ -736,7 +858,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("else"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError(arg, parameterIndex - 1));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadRemainingParameterValueFormatError, "); + } + writer.WriteLine("arg, parameterIndex - 1));"); writer.CloseBlock(); break; case ParseStrategy.DateTimeRelated: @@ -744,7 +871,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!{type}.TryParse(arg, global::System.Globalization.CultureInfo.InvariantCulture, global::System.Globalization.DateTimeStyles.None, out {propertyName}_val))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError(arg, parameterIndex - 1));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadRemainingParameterValueFormatError, "); + } + writer.WriteLine("arg, parameterIndex - 1));"); writer.CloseBlock(); writer.WriteLine($"remainingParametersBuilder.Add({propertyName}_val);"); break; @@ -753,7 +885,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (!global::System.TimeSpan.TryParse(arg, global::System.Globalization.CultureInfo.InvariantCulture, out {propertyName}_val))"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError(arg, parameterIndex - 1));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.BadRemainingParameterValueFormatError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.BadRemainingParameterValueFormatError, "); + } + writer.WriteLine("arg, parameterIndex - 1));"); writer.CloseBlock(); writer.WriteLine($"remainingParametersBuilder.Add({propertyName}_val);"); break; @@ -766,7 +903,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen else { writer.WriteLine("errors ??= new();"); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError(arg));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.UnrecognizedArgumentError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.UnrecognizedArgumentError, "); + } + writer.WriteLine("arg));"); } writer.WriteLine("break;"); writer.Ident--; @@ -794,7 +936,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine("if (state > 0)"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.OptionValueIsNotProvidedError(previousArgument));"); + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.OptionValueIsNotProvidedError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.OptionValueIsNotProvidedError, "); + } + writer.WriteLine("previousArgument));"); writer.CloseBlock(); } @@ -810,11 +957,15 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if ((seenOptions & 0b{usageCode.ToString()}) == 0)"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine((info.ShortName, info.LongName) switch + writer.Write("errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredOptionError("); + writer.WriteLine((info.ShortName, info.LongName, errorMessageFormatProvider is null) switch { - (not null, null) => $"errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredOptionError('{info.ShortName.Value}'));", - (null, not null) => $"errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredOptionError(\"{info.LongName}\"));", - (not null, not null) => $"errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredOptionError('{info.ShortName.Value}', \"{info.LongName}\"));", + (not null, null, true) => $"'{info.ShortName.Value}'));", + (null, not null, true) => $"\"{info.LongName}\"));", + (not null, not null, true) => $"'{info.ShortName.Value}', \"{info.LongName}\"));", + (not null, null, false) => $"{errorMessageFormatProvider}.MissingRequiredOptionError_OnlyShortOptionName, '{info.ShortName.Value}', null));", + (null, not null, false) => $"{errorMessageFormatProvider}.MissingRequiredOptionError_OnlyLongOptionName, null, \"{info.LongName}\"));", + (not null, not null, false) => $"{errorMessageFormatProvider}.MissingRequiredOptionError_BothOptionNames, '{info.ShortName.Value}', \"{info.LongName}\"));", _ => throw new InvalidOperationException("Unreachable"), }); writer.CloseBlock(); @@ -831,7 +982,12 @@ private static void EmitArgumentParser(SourceProductionContext context, (Argumen writer.WriteLine($"if (parameterIndex <= {i})"); writer.OpenBlock(); writer.WriteLine("errors ??= new();"); - writer.WriteLine($"errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredParameterError(\"{info.Name}\", {i}));"); + writer.WriteLine("errors.Add(new global::ArgumentParsing.Results.Errors.MissingRequiredParameterError("); + if (errorMessageFormatProvider is not null) + { + writer.Write($"{errorMessageFormatProvider}.MissingRequiredParameterError, "); + } + writer.WriteLine($"\"{info.Name}\", {i}));"); writer.CloseBlock(); } } diff --git a/src/ArgumentParsing.Generators/ArgumentParserGenerator.Extract.cs b/src/ArgumentParsing.Generators/ArgumentParserGenerator.Extract.cs index bae6dbb..2954702 100644 --- a/src/ArgumentParsing.Generators/ArgumentParserGenerator.Extract.cs +++ b/src/ArgumentParsing.Generators/ArgumentParserGenerator.Extract.cs @@ -28,6 +28,18 @@ public partial class ArgumentParserGenerator var namedArgs = genArgParserAttrData.NamedArguments; + string? errorMessageFormatProvider = null; + if (namedArgs.FirstOrDefault(static n => n.Key == "ErrorMessageFormatProvider") is { Key: not null, Value: { } errorMessageFormatProviderVal }) + { + if (errorMessageFormatProviderVal.Kind == TypedConstantKind.Error || + !errorMessageFormatProviderVal.IsNull && errorMessageFormatProviderVal.Value is not INamedTypeSymbol { TypeKind: not TypeKind.Error, SpecialType: SpecialType.None }) + { + return null; + } + + errorMessageFormatProvider = ((INamedTypeSymbol?)errorMessageFormatProviderVal.Value)?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + if (namedArgs.FirstOrDefault(static n => n.Key == "BuiltInCommandHandlers").Value is { Value: byte builtInHandlersByte }) { builtInCommandHandlers = (BuiltInCommandHandlers)builtInHandlersByte; @@ -202,6 +214,7 @@ public partial class ArgumentParserGenerator HierarchyInfo.From(argumentParserMethodSymbol.ContainingType), methodInfo, optionsInfo, + errorMessageFormatProvider, builtInCommandInfos.ToImmutable(), additionalCommandHandlerInfosBuilder.ToImmutable()); diff --git a/src/ArgumentParsing.Generators/Models/ArgumentParserInfo.cs b/src/ArgumentParsing.Generators/Models/ArgumentParserInfo.cs index cda01cd..2b055ba 100644 --- a/src/ArgumentParsing.Generators/Models/ArgumentParserInfo.cs +++ b/src/ArgumentParsing.Generators/Models/ArgumentParserInfo.cs @@ -6,5 +6,6 @@ internal sealed record ArgumentParserInfo( HierarchyInfo ContainingTypeHierarchy, ArgumentParserMethodInfo MethodInfo, OptionsInfo OptionsInfo, + string? ErrorMessageFormatProvider, ImmutableEquatableArray BuiltInCommandInfos, ImmutableEquatableArray AdditionalCommandHandlersInfos); diff --git a/tests/ArgumentParsing.Tests.Functional/ErrorMessageFormatProviderTests.cs b/tests/ArgumentParsing.Tests.Functional/ErrorMessageFormatProviderTests.cs new file mode 100644 index 0000000..543a334 --- /dev/null +++ b/tests/ArgumentParsing.Tests.Functional/ErrorMessageFormatProviderTests.cs @@ -0,0 +1,272 @@ +using System.Collections.Immutable; +using ArgumentParsing.Results; +using ArgumentParsing.Results.Errors; +using ArgumentParsing.Tests.Functional.Utils; + +namespace ArgumentParsing.Tests.Functional; + +public sealed partial class ErrorMessageFormatProviderTests +{ + #region OptionsAndParser + [OptionsType] + private sealed class Options1 + { + [Option] + public string? Opt1 { get; init; } + + [Option] + public int Opt2 { get; init; } + + [Option] + public bool Flag { get; init; } + } + + [GeneratedArgumentParser(ErrorMessageFormatProvider = typeof(MyErrorMessageFormatProvider))] + private static partial ParseResult ParseArguments1(ReadOnlySpan args); + + [OptionsType] + private sealed class Options2 + { + [Option('o', null)] + public required string Opt1 { get; init; } + + [Option] + public required string Opt2 { get; init; } + + [Option('r', "opt3")] + public required string Opt3 { get; init; } + } + + [GeneratedArgumentParser(ErrorMessageFormatProvider = typeof(MyErrorMessageFormatProvider))] + private static partial ParseResult ParseArguments2(Span args); + + [OptionsType] + private sealed class Options3 + { + [Parameter(0)] + public required int Param { get; init; } + + [RemainingParameters] + public ImmutableArray RemainingParams { get; init; } + } + + [GeneratedArgumentParser(ErrorMessageFormatProvider = typeof(MyErrorMessageFormatProvider))] + private static partial ParseResult ParseArguments3(string[] args); + #endregion + + [Fact] + public void UnknownOptionError() + { + var result = ParseArguments1(["-d", "a"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var unknownOptionError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.UnknownOptionError, unknownOptionError.OptionName, unknownOptionError.ContainingArgument), unknownOptionError.GetMessage()); + } + + [Fact] + public void UnrecognizedArgumentError() + { + var result = ParseArguments1(["a"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var unrecognizedArgumentError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.UnrecognizedArgumentError, unrecognizedArgumentError.Argument), unrecognizedArgumentError.GetMessage()); + } + + [Fact] + public void OptionValueIsNotProvidedError() + { + var result = ParseArguments1(["--opt1"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var optionValueIsNotProvidedError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.OptionValueIsNotProvidedError, optionValueIsNotProvidedError.PrecedingArgument), optionValueIsNotProvidedError.GetMessage()); + } + + [Fact] + public void DuplicateOptionError() + { + var result = ParseArguments1(["--opt1", "a", "--opt1", "b"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var duplicateOptionError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.DuplicateOptionError, duplicateOptionError.OptionName), duplicateOptionError.GetMessage()); + } + + [Fact] + public void MissingRequiredOptionError_OnlyShortOptionName() + { + var result = ParseArguments2(["--opt2", "a", "--opt3", "b"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var missingRequiredOptionError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.MissingRequiredOptionError_OnlyShortOptionName, missingRequiredOptionError.ShortOptionName), missingRequiredOptionError.GetMessage()); + } + + [Fact] + public void MissingRequiredOptionError_OnlyLongOptionName() + { + var result = ParseArguments2(["-o", "a", "--opt3", "b"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var missingRequiredOptionError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.MissingRequiredOptionError_OnlyLongOptionName, missingRequiredOptionError.LongOptionName), missingRequiredOptionError.GetMessage()); + } + + [Fact] + public void MissingRequiredOptionError_BothOptionNames() + { + var result = ParseArguments2(["-o", "a", "--opt2", "b"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var missingRequiredOptionError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.MissingRequiredOptionError_BothOptionNames, missingRequiredOptionError.ShortOptionName, missingRequiredOptionError.LongOptionName), missingRequiredOptionError.GetMessage()); + } + + [Fact] + public void BadOptionValueFormatError() + { + var result = ParseArguments1(["--opt2", "a"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var badOptionValueFormatError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.BadOptionValueFormatError, badOptionValueFormatError.Value, badOptionValueFormatError.OptionName), badOptionValueFormatError.GetMessage()); + } + + [Fact] + public void FlagOptionValueError() + { + var result = ParseArguments1(["--flag", "true"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var flagOptionValueError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.FlagOptionValueError, flagOptionValueError.OptionName), flagOptionValueError.GetMessage()); + } + + [Fact] + public void BadParameterValueFormatError() + { + var result = ParseArguments3(["a"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var badParameterValueFormatError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.BadParameterValueFormatError, badParameterValueFormatError.Value, badParameterValueFormatError.ParameterName, badParameterValueFormatError.ParameterIndex), badParameterValueFormatError.GetMessage()); + } + + [Fact] + public void MissingRequiredParameterError() + { + var result = ParseArguments3([]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var missingRequiredParameterError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.MissingRequiredParameterError, missingRequiredParameterError.ParameterName, missingRequiredParameterError.ParameterIndex), missingRequiredParameterError.GetMessage()); + } + + [Fact] + public void BadRemainingParameterValueFormatError() + { + var result = ParseArguments3(["2", "a"]); + + Assert.Equal(ParseResultState.ParsedWithErrors, result.State); + + Assert.Null(result.Options); + + var errors = result.Errors; + Assert.NotNull(errors); + + var error = Assert.Single(errors); + var badRemainingParameterValueFormatError = Assert.IsType(error); + + Assert.Equal(string.Format(MyErrorMessageFormatProvider.BadRemainingParameterValueFormatError, badRemainingParameterValueFormatError.Value, badRemainingParameterValueFormatError.ParameterIndex), badRemainingParameterValueFormatError.GetMessage()); + } +} diff --git a/tests/ArgumentParsing.Tests.Functional/Utils/MyErrorMessageFormatProvider.cs b/tests/ArgumentParsing.Tests.Functional/Utils/MyErrorMessageFormatProvider.cs new file mode 100644 index 0000000..74c41ea --- /dev/null +++ b/tests/ArgumentParsing.Tests.Functional/Utils/MyErrorMessageFormatProvider.cs @@ -0,0 +1,17 @@ +namespace ArgumentParsing.Tests.Functional.Utils; + +public static class MyErrorMessageFormatProvider +{ + public const string UnknownOptionError = "[Error]: Unknown option '{0}' in argument '{1}'"; + public const string UnrecognizedArgumentError = "[Error]: Unrecognized argument '{0}'"; + public const string OptionValueIsNotProvidedError = "[Error]: No option value is provided after argument '{0}'"; + public const string DuplicateOptionError = "[Error]: Duplicate option '{0}'"; + public const string MissingRequiredOptionError_OnlyShortOptionName = "[Error]: Missing required option with short name '{0}'"; + public const string MissingRequiredOptionError_OnlyLongOptionName = "[Error]: Missing required option with long name '{0}'"; + public const string MissingRequiredOptionError_BothOptionNames = "[Error]: Missing required option '{0}' ('{1}')"; + public const string BadOptionValueFormatError = "[Error]: Value '{0}' is in incorrect format for option '{1}'"; + public const string FlagOptionValueError = "[Error]: Flag option '{0}' does not accept a value"; + public const string BadParameterValueFormatError = "[Error]: Value '{0}' is in incorrect format for parameter '{1}' (parameter index {2})"; + public const string MissingRequiredParameterError = "[Error]: Missing required parameter '{0}' (parameter index {1})"; + public const string BadRemainingParameterValueFormatError = "[Error]: Value '{0}' is in incorrect format for parameter at index {1}"; +} diff --git a/tests/ArgumentParsing.Tests.Unit/ArgumentParserGeneratorTests.cs b/tests/ArgumentParsing.Tests.Unit/ArgumentParserGeneratorTests.cs index ea80547..9097306 100644 --- a/tests/ArgumentParsing.Tests.Unit/ArgumentParserGeneratorTests.cs +++ b/tests/ArgumentParsing.Tests.Unit/ArgumentParserGeneratorTests.cs @@ -2223,6 +2223,79 @@ class MyOptions await VerifyGeneratorAsync(source); } + [Theory] + [InlineData("null")] + [InlineData("default")] + [InlineData("default(System.Type)")] + public async Task ErrorMessageFormatProvider_TypeSpecifier_Default(string defaultSyntax) + { + var source = $$""" + partial class C + { + [GeneratedArgumentParser(ErrorMessageFormatProvider = {{defaultSyntax}})] + public static partial ParseResult ParseArguments(string[] args); + } + """; + + await VerifyGeneratorAsync(source, ("EmptyOptions.g.cs", GetMainCodeGenForEmptyOptions("string[]", "args")), ("HelpCommandHandler.EmptyOptions.g.cs", HelpCodeGenForEmptyOptions), ("VersionCommandHandler.TestProject.g.cs", VersionCommandHander)); + } + + [Fact] + public async Task ErrorMessageFormatProvider_TypeSpecifier_InvalidValue() + { + var source = """ + partial class C + { + [GeneratedArgumentParser(ErrorMessageFormatProvider = {|CS0029:5|})] + public static partial ParseResult {|CS8795:ParseArguments|}(string[] args); + } + """; + + await VerifyGeneratorAsync(source); + } + + [Fact] + public async Task ErrorMessageFormatProvider_TypeSpecifier_ErrorType() + { + var source = """ + partial class C + { + [GeneratedArgumentParser(ErrorMessageFormatProvider = typeof({|CS0246:ErrorType|}))] + public static partial ParseResult {|CS8795:ParseArguments|}(string[] args); + } + """; + + await VerifyGeneratorAsync(source); + } + + [Fact] + public async Task ErrorMessageFormatProvider_TypeSpecifier_Invalid_NotNamedType() + { + var source = """ + partial class C + { + [GeneratedArgumentParser(ErrorMessageFormatProvider = typeof({|#0:C[]|}))] + public static partial ParseResult {|CS8795:ParseArguments|}(string[] args); + } + """; + + await VerifyGeneratorAsync(source); + } + + [Fact] + public async Task ErrorMessageFormatProvider_TypeSpecifier_Invalid_SpecialType() + { + var source = """ + partial class C + { + [GeneratedArgumentParser(ErrorMessageFormatProvider = typeof({|#0:int|}))] + public static partial ParseResult {|CS8795:ParseArguments|}(string[] args); + } + """; + + await VerifyGeneratorAsync(source); + } + private static async Task VerifyGeneratorAsync(string source, params (string Hint, string Content)[] generatedDocuments) { var test = new CSharpSourceGeneratorTest()