From 623def2bfa0349bb30a6492c5f772d1c7255c104 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 07:32:17 +0000 Subject: [PATCH] Fail generate when --generator has no name, and accept --generator=Name [patch] GetOption only recognised `--generator Name` as two tokens. With the option last, or written `--generator=Name`, it returned null, which Generate reads as "no filter", so every generator ran and the command exited 0. Read the filter before loading anything: a bare `--generator`, one followed by another option, or an empty `--generator=` now fails with "'--generator' needs a code generator name." and writes nothing, and `--generator=Name` selects that generator. Fixes #259 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FkGdWB8Ko3mTn9JiBH973 --- Schema.Test/SchemaCommandLineTests.cs | 28 +++++++++++++++ Schema/Cli/SchemaCommandLine.cs | 50 ++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/Schema.Test/SchemaCommandLineTests.cs b/Schema.Test/SchemaCommandLineTests.cs index f530db8..9e03f9b 100644 --- a/Schema.Test/SchemaCommandLineTests.cs +++ b/Schema.Test/SchemaCommandLineTests.cs @@ -311,6 +311,34 @@ public void TestGeneratorOptionSelectsOneByName() Assert.IsTrue(unmatched.Error.Contains("no code generator named 'Cpp'", StringComparison.Ordinal), unmatched.Error); } + [TestMethod] + [DataRow("--generator")] + [DataRow("--generator=")] + [DataRow("--generator", "--verbose")] + public void TestGeneratorOptionWithoutANameFailsAndWritesNothing(params string[] option) + { + string path = WriteSchema("noname.schema.json", ValidSchema); + Result result = Run(["generate", path, .. option]); + + Assert.AreEqual(SchemaCommandLine.Failure, result.ExitCode, result.Output); + Assert.IsTrue(result.Error.Contains("'--generator' needs a code generator name", StringComparison.Ordinal), result.Error); + Assert.IsFalse(Directory.Exists(Path.Combine(workingDirectory, "out")), "Nothing should have been written."); + } + + [TestMethod] + public void TestGeneratorOptionAcceptsTheEqualsForm() + { + string path = WriteSchema("equals.schema.json", ValidSchema); + + Result matched = Run("generate", path, "--generator=CSharp"); + Assert.AreEqual(SchemaCommandLine.Success, matched.ExitCode, matched.Error); + Assert.IsTrue(File.Exists(Path.Combine(workingDirectory, "out", "Item.g.cs")), matched.Output); + + Result unmatched = Run("generate", path, "--generator=Cpp"); + Assert.AreEqual(SchemaCommandLine.Failure, unmatched.ExitCode); + Assert.IsTrue(unmatched.Error.Contains("no code generator named 'Cpp'", StringComparison.Ordinal), unmatched.Error); + } + [TestMethod] public void TestOptionsDoNotGetMistakenForTheSchemaPath() { diff --git a/Schema/Cli/SchemaCommandLine.cs b/Schema/Cli/SchemaCommandLine.cs index 4ca0a16..e106ba2 100644 --- a/Schema/Cli/SchemaCommandLine.cs +++ b/Schema/Cli/SchemaCommandLine.cs @@ -105,12 +105,14 @@ private static int UnknownCommand(string command, TextWriter output, TextWriter private static int Generate(string[] args, TextWriter output, TextWriter error) { - if (!TryLoad(args, error, out Models.Schema? schema, out string? schemaPath)) + // Read the filter before anything is loaded or written: a filter that cannot be read must + // fail the command, never fall back to running every generator. + if (!TryGetGeneratorFilter(args, error, out string? only) + || !TryLoad(args, error, out Models.Schema? schema, out string? schemaPath)) { return Failure; } - string? only = GetOption(args, GeneratorOption); List generators = [.. schema.CodeGenerators .Where(g => only is null || string.Equals(g.Name, only, StringComparison.OrdinalIgnoreCase))]; @@ -230,9 +232,47 @@ private static bool TryLoad( private static bool TakesAValue(string option) => string.Equals(option, GeneratorOption, StringComparison.OrdinalIgnoreCase); - private static string? GetOption(string[] args, string name) + /// + /// Reads the --generator filter, written either as --generator Name or + /// --generator=Name. + /// + /// + /// when the option is present without a name. is + /// only when the option is absent, which is the one case that means every + /// generator. + /// + private static bool TryGetGeneratorFilter(string[] args, TextWriter error, out string? name) { - int index = Array.FindIndex(args, a => string.Equals(a, name, StringComparison.OrdinalIgnoreCase)); - return index >= 0 && index + 1 < args.Length ? args[index + 1] : null; + name = null; + string prefix = GeneratorOption + "="; + + for (int index = 0; index < args.Length; index++) + { + string argument = args[index]; + if (string.Equals(argument, GeneratorOption, StringComparison.OrdinalIgnoreCase)) + { + // A following option is not a name: "--generator --verbose" lost its value. + name = index + 1 < args.Length && !args[index + 1].StartsWith('-') ? args[index + 1] : null; + } + else if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + name = argument[prefix.Length..]; + } + else + { + continue; + } + + if (string.IsNullOrWhiteSpace(name)) + { + error.WriteLine($"'{GeneratorOption}' needs a code generator name."); + name = null; + return false; + } + + return true; + } + + return true; } }