Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ namespace ModularPipelines.OptionsGenerator.Tests.Generators;
public class OptionEnumFactoryTests
{
[Test]
public async Task Syntax_Delimiters_Are_Rejected_Only_For_Inferred_Choices()
public async Task Empty_List_Default_Does_Not_Add_Type_Metadata()
{
string[] values = ["[json", "yaml]"];
await Assert.That(OptionEnumFactory.PreserveValueHint(null, "Set labels.", "[]")).IsEqualTo("Set labels.");
}

[Test]
[Arguments("[json", "yaml]")]
[Arguments("(json", "yaml)")]
public async Task Syntax_Delimiters_Are_Rejected_Only_For_Inferred_Choices(string first, string second)
{
string[] values = [first, second];
await Assert.That(OptionEnumFactory.TryCreateFromHint("TestOptions", "Mode", "--mode", values)).IsNull();
var structured = OptionEnumFactory.TryCreate("TestOptions", "Mode", "--mode", values);
await Assert.That(structured!.Values.Select(value => value.CliValue)).IsEquivalentTo(values);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class CliDocumentationScraperBaseTests
[Arguments("json|yaml]")]
[Arguments("[json|yaml>")]
[Arguments("[<json|yaml>]")]
[Arguments("(json|yaml")]
[Arguments("json|yaml)")]
[Arguments("(json|yaml]")]
public async Task DetectEnumValues_Rejects_Annotated_Or_Incomplete_Pipe_Hints(string hint)
{
await Assert.That(TestDocumentationScraper.DetectEnumValues(hint)).IsNull();
Expand All @@ -36,6 +39,8 @@ public async Task DetectEnumValues_Does_Not_Infer_Enums_From_Numeric_Hints(strin
[Arguments("<json|yaml>")]
[Arguments("{json|yaml}")]
[Arguments("[json|yaml]")]
[Arguments("(json|yaml)")]
[Arguments(" ( json|yaml ) ")]
[Arguments(" [ json|yaml ] ")]
[Arguments(" < json|yaml > ")]
[Arguments(" { json|yaml } ")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public async Task Type_Hint_Preserves_Long_Choices()
[Arguments("<json|yaml>")]
[Arguments("{json|yaml}")]
[Arguments("[json|yaml]")]
[Arguments("(json|yaml)")]
public async Task Type_Hint_Strips_Choice_Wrappers(string hint)
{
var helpText = $"Usage: fake run [OPTIONS]\n\nOptions:\n --mode {hint} Select mode\n";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ namespace ModularPipelines.OptionsGenerator.Tests.Scrapers;

public class DockerDocumentationScraperTests
{
[Test]
public async Task Empty_List_Default_Does_Not_Become_Value_Type_Metadata()
{
const string html = """
<html><body><h1>docker build</h1>
<table><thead><tr><th>Option</th><th>Description</th><th>Default</th></tr></thead>
<tbody><tr><td>--label</td><td>Set labels.</td><td>[]</td></tr></tbody></table>
</body></html>
""";
using var handler = new DocumentationHandler(html);
using var client = new HttpClient(handler);
var scraper = new DockerDocumentationScraper(client, NullLogger<DockerDocumentationScraper>.Instance);
var tool = await scraper.ScrapeAsync();
await Assert.That(tool.Commands.Single().Options.Single().Description).IsEqualTo("Set labels.");
}

[Test]
[Arguments(2, "", "")]
[Arguments(21, "", "")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,85 @@ namespace ModularPipelines.OptionsGenerator.Tests.Scrapers;

public class SnykCliScraperTests
{
[Test]
[Arguments("[json|yaml]")]
[Arguments("(json|yaml)")]
[Arguments("{json|yaml}")]
[Arguments("<json|yaml>")]
public async Task Optional_Heading_Brackets_Do_Not_Become_Part_Of_The_Value_Hint(string hint)
{
var command = (await new TestSnykCliScraper().Parse(["snyk", "test"],
$"Options\n [--mode={hint}]\n Select mode."))!;
await Assert.That(command.Options.Single().EnumDefinition?.Values.Select(value => value.CliValue))
.IsEquivalentTo(["json", "yaml"]);
}

[Test]
[Arguments("[]")]
[Arguments("()")]
[Arguments("{}")]
[Arguments("<>")]
public async Task Optional_Headings_Preserve_Empty_Value_Placeholders(string hint)
{
var command = (await new TestSnykCliScraper().Parse(["snyk", "test"],
$"Options\n [--mode={hint}]\n Select mode."))!;
var option = command.Options.Single();
await Assert.That(option.CSharpType).IsEqualTo("string?");
await Assert.That(option.IsFlag).IsFalse();
await Assert.That(option.Description).IsEqualTo($"Select mode. [value type: {hint}]");
}

[Test]
[Arguments("(json|yaml)")]
[Arguments("{json|yaml}")]
[Arguments("<json|yaml>")]
public async Task Choice_Hints_Use_Shared_Wrapper_Parsing(string hint)
{
var command = (await new TestSnykCliScraper().Parse(["snyk", "test"],
$"Options\n --mode={hint}\n Select mode."))!;
await Assert.That(command.Options.Single().EnumDefinition!.Values.Select(value => value.CliValue))
.IsEquivalentTo(["json", "yaml"]);
}

[Test]
[Arguments("(true|false)")]
[Arguments("{true|false}")]
[Arguments("<true|false>")]
public async Task Wrapped_Boolean_Choices_Remain_Value_Options(string hint)
{
var command = (await new TestSnykCliScraper().Parse(["snyk", "test"],
$"Options\n --reachable={hint}\n Select reachability."))!;
var option = command.Options.Single();
await Assert.That(option.CSharpType).IsEqualTo("bool?");
await Assert.That(option.IsFlag).IsFalse();
await Assert.That(option.EnumDefinition).IsNull();
}

[Test]
[Arguments("(json|yaml")]
[Arguments("json|yaml)")]
[Arguments("json||yaml")]
[Arguments("true||false")]
[Arguments("(json||yaml)")]
[Arguments("()")]
[Arguments("[]")]
[Arguments("{}")]
[Arguments("<>")]
[Arguments("<json|yaml")]
[Arguments("json|yaml>")]
[Arguments("<json|yaml)")]
public async Task Malformed_Choice_Hints_Remain_In_String_Descriptions(string hint)
{
var command = (await new TestSnykCliScraper().Parse(["snyk", "test"],
$"Options\n --mode={hint}\n Select mode."))!;
var option = command.Options.Single();
await Assert.That(option.CSharpType).IsEqualTo("string?");
await Assert.That(option.IsFlag).IsFalse();
await Assert.That(option.ValueSeparator).IsEqualTo("=");
await Assert.That(option.EnumDefinition).IsNull();
await Assert.That(option.Description).Contains(hint);
}

[Test]
[Arguments("Required. Specify the identifier.", true)]
[Arguments("Required: specify the identifier.", true)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace ModularPipelines.OptionsGenerator.Generators;

internal static class OptionEnumFactory
{
private static readonly SearchValues<char> HintSyntaxDelimiters = SearchValues.Create("<>{}[]");
private static readonly SearchValues<char> HintSyntaxDelimiters = SearchValues.Create("<>{}[]()");

// Inferred hints need a lexical guard; authoritative structured choices may
// contain numeric or symbolic literals and must retain their original values.
Expand All @@ -22,7 +22,7 @@ public static string UnwrapChoiceHint(string hint)
{
var trimmed = hint.Trim();
return trimmed.Length >= 2
&& (trimmed[0], trimmed[^1]) is ('<', '>') or ('{', '}') or ('[', ']')
&& (trimmed[0], trimmed[^1]) is ('<', '>') or ('{', '}') or ('[', ']') or ('(', ')')
? trimmed[1..^1].Trim()
: trimmed;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,12 @@ private void ParseOptionLines(
HashSet<string> seenOptions)
{
var longForm = match.Groups["long"].Value.Trim();
var valueHint = match.Groups["value"].Value.Trim().Trim('<', '>', '[', ']');
var rawValueHint = match.Groups["value"].Value;
if (match.Groups["optional"].Success && rawValueHint.EndsWith(']'))
{
rawValueHint = rawValueHint[..^1];
}
var valueHint = OptionEnumFactory.UnwrapChoiceHint(rawValueHint);
Comment thread
thomhurst marked this conversation as resolved.
if (!seenOptions.Add(longForm))
{
return null;
Expand All @@ -412,7 +417,7 @@ private void ParseOptionLines(
}

var isNumeric = NumericOptions.Contains(longForm);
var isFlag = IsFlagOption(longForm, valueHint, isNumeric);
var isFlag = IsFlagOption(longForm, rawValueHint, isNumeric);
var isBoolean = IsBooleanValueHint(valueHint);
var acceptsMultipleValues = AcceptsMultipleValues(
commandParts,
Expand All @@ -422,14 +427,19 @@ private void ParseOptionLines(
isBoolean);
var enumDefinition = CreateEnumDefinition(propertyName, longForm, valueHint, isBoolean);
var scalarType = GetScalarType(enumDefinition, isFlag, isBoolean, isNumeric);
if (rawValueHint.Length > 0 && valueHint.Length == 0)
{
// Here the token is an explicit value placeholder, not a table's default cell.
description = $"{description} [value type: {rawValueHint}]".Trim();
}

return new CliOptionDefinition
{
SwitchName = longForm,
ShortForm = null,
PropertyName = propertyName,
CSharpType = AsCSharpType(scalarType, acceptsMultipleValues),
Description = description,
Description = isBoolean ? description : OptionEnumFactory.PreserveValueHint(enumDefinition, description, rawValueHint),
IsFlag = isFlag,
IsRequired = description is not null && DescriptionDeclaresRequiredOption(description),
AcceptsMultipleValues = acceptsMultipleValues,
Expand Down Expand Up @@ -481,19 +491,8 @@ enumDefinition is not null
return null;
}

var values = valueHint.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return values.Length < 2
? null
: new CliEnumDefinition
{
EnumName = $"Snyk{propertyName}",
Values = values.Select(value => new CliEnumValue
{
MemberName = GeneratorUtils.ToEnumMemberName(value),
CliValue = value,
}).ToList(),
Description = $"Allowed values for --{longForm.TrimStart('-')}",
};
var values = valueHint.Split('|', StringSplitOptions.TrimEntries);
return OptionEnumFactory.TryCreateFromHint("Snyk", propertyName, longForm, values);
Comment thread
thomhurst marked this conversation as resolved.
}

private static bool IsKnownScalarValueOption(
Expand Down Expand Up @@ -582,7 +581,7 @@ private static void AddOptionIfMissing(

private static bool IsBooleanValueHint(string valueHint)
{
var values = valueHint.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var values = valueHint.Split('|', StringSplitOptions.TrimEntries);
return values.Length == 2 &&
values.Contains("true", StringComparer.OrdinalIgnoreCase) &&
values.Contains("false", StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -833,7 +832,7 @@ protected override bool HasOptions(string helpText)
/// --severity-threshold=&lt;low|medium|high|critical&gt;
/// --json
/// </summary>
[GeneratedRegex(@"(?<long>--[\w-]+)(?:=(?:<(?<value>[^>\s]+)>?|(?<value>[^\s,]+)))?")]
[GeneratedRegex(@"(?<optional>\[)?(?<long>--[\w-]+)(?:=(?:(?<value><[^>\s]+>?)|(?<value>[^\s,]+)))?")]
private static partial Regex SnykOptionPattern();

[GeneratedRegex(@"\bUse (?:the )?-d(?: option)?\b", RegexOptions.IgnoreCase)]
Expand Down
Loading