diff --git a/changelog.d/unreleased/1700.fixed.md b/changelog.d/unreleased/1700.fixed.md new file mode 100644 index 0000000000..3e7d8e27ff --- /dev/null +++ b/changelog.d/unreleased/1700.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 1700 +affected: + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **Out-of-range query numeric flags now fail closed (#1700)** — numeric options such as `--depth 999999999` return usage errors instead of being silently coerced or clamped. + +## 日本語 + +- **範囲外の query 数値フラグを fail-closed にしました (#1700)** — `--depth 999999999` のような数値 option は silent な補正や clamp ではなく usage error を返します。 diff --git a/changelog.d/unreleased/1705-1801.fixed.md b/changelog.d/unreleased/1705-1801.fixed.md new file mode 100644 index 0000000000..c3313a09c5 --- /dev/null +++ b/changelog.d/unreleased/1705-1801.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 1705 + - 1801 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +--- + +## English + +- **Unknown query flags now fail closed (#1705, #1801)** — query commands reject unknown dash-prefixed tokens unless the user explicitly passes them after `--`. + +## 日本語 + +- **未知の query flag を fail-closed にしました (#1705, #1801)** — query command は、ユーザーが明示的に `--` の後ろへ置いた場合を除き、未知の dash 始まり token を拒否します。 diff --git a/changelog.d/unreleased/1803.fixed.md b/changelog.d/unreleased/1803.fixed.md new file mode 100644 index 0000000000..fae9de730d --- /dev/null +++ b/changelog.d/unreleased/1803.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1803 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **Unknown `index` options now stop the run (#1803)** — `cdidx index` treats unknown options as usage errors with did-you-mean hints instead of warning and continuing. + +## 日本語 + +- **未知の `index` option で実行を停止するようにしました (#1803)** — `cdidx index` は未知 option を warning で続行せず、did-you-mean hint 付きの usage error として扱います。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index daf07cfff1..81d9821947 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -164,7 +164,7 @@ internal static int Run(string[] indexArgs, JsonSerializerOptions jsonOptions, C jsonOptions, options.ParseError, CommandExitCodes.UsageError, - "Rerun `cdidx index --commits [commit-id ...]` with 7-40 hex commit object IDs.", + "Run `cdidx index --help` to see supported options.", CommandErrorCodes.UsageError); } @@ -804,14 +804,14 @@ internal static int RunBackfillFold( // Index-mode flag names recognized by `ParseArgs`. Kept in sync with the switch above - // so `Warning: unknown option ...` can suggest the closest accepted flag (#1582). Easter-egg + // so unknown option errors can suggest the closest accepted flag (#1582). Easter-egg // and random-spinner flags are excluded since they are intentionally undiscoverable. - // `ParseArgs` の switch と同期した index 系の受理フラグ一覧。`unknown option` 警告で + // `ParseArgs` の switch と同期した index 系の受理フラグ一覧。`unknown option` error で // 最も近い受理フラグを did-you-mean 提案するのに用いる (#1582)。 // easter egg や random-spinner は意図的に未公開なので除外する。 private static readonly string[] AcceptedIndexFlags = [ - "--db", "--rebuild", "--verbose", "--json", "--dry-run", "--force", + "--db", "--data-dir", "--rebuild", "--verbose", "--json", "--dry-run", "--force", "--yes", "--watch", "--debounce", "--duration-format", "--max-file-bytes", "--parallelism", "--commits", "--changed-between", "--files", "--solution", "--project", @@ -858,12 +858,13 @@ private static OptimizeFtsCommandOptions ParseOptimizeFtsArgs(string[] args) }; } - private static void WriteUnknownIndexOptionSuggestion(string token) + private static string BuildUnknownIndexOptionError(string token) { var name = TrimInlineValue(token); var suggestion = ConsoleUi.FindClosestMatch(name, AcceptedIndexFlags); - if (suggestion != null) - Console.Error.WriteLine($"Did you mean: {suggestion}?"); + return suggestion == null + ? $"unknown option '{token}'" + : $"unknown option '{token}'\nDid you mean: {suggestion}?"; } private static void WriteUnknownBackfillFoldOptionSuggestion(string token) @@ -1106,8 +1107,7 @@ public static IndexCommandOptions ParseArgs(string[] args) default: if (args[i].StartsWith('-')) { - Console.Error.WriteLine($"Warning: unknown option '{args[i]}' (ignored) / 不明なオプション '{args[i]}'(無視されます)"); - WriteUnknownIndexOptionSuggestion(args[i]); + parseError ??= BuildUnknownIndexOptionError(args[i]); } else projectPath = args[i]; diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 0298152987..1390ca23ea 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -4566,8 +4566,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) default: if (args[i].StartsWith('-')) { - if (allowNamedQuery && query == null) - query = args[i]; + AddParseError($"Error: unsupported option: {args[i]}. Use `--` before a query literal that starts with `-`."); break; } else if (query == null) diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 2ae20d0276..daba91e0f0 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -49,6 +49,26 @@ public void ParseArgs_HelpFlagSetsShowHelp() Assert.Null(options.ProjectPath); } + [Fact] + public void Run_UnknownIndexOption_ReturnsUsageError() + { + var projectRoot = CreateTempProject(); + try + { + var (exitCode, stdout, stderr) = RunAndCaptureStreams([projectRoot, "--verbos"]); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("unknown option '--verbos'", stderr); + Assert.Contains("Did you mean: --verbose?", stderr); + Assert.DoesNotContain("Warning: unknown option", stderr); + } + finally + { + DeleteDirectory(projectRoot); + } + } + [Fact] public void FormatIndexFileException_RegexTimeout_UsesBoundedExtractionMessage() { @@ -369,43 +389,19 @@ public void Run_ExistingIndexDatabase_RunsPragmaOptimizeAfterSuccessfulIndex() [Fact] public void ParseArgs_UnknownIndexOption_SuggestsClosestFlag() { - lock (TestConsoleLock.Gate) - { - var originalErr = Console.Error; - using var stderr = new StringWriter(); - try - { - Console.SetError(stderr); - IndexCommandRunner.ParseArgs([".", "--rebild"]); - Assert.Contains("Warning: unknown option '--rebild'", stderr.ToString()); - Assert.Contains("Did you mean: --rebuild?", stderr.ToString()); - } - finally - { - Console.SetError(originalErr); - } - } + var options = IndexCommandRunner.ParseArgs([".", "--rebild"]); + + Assert.Contains("unknown option '--rebild'", options.ParseError); + Assert.Contains("Did you mean: --rebuild?", options.ParseError); } [Fact] public void ParseArgs_UnknownIndexOption_NoSuggestionWhenFarFromAnyFlag() { - lock (TestConsoleLock.Gate) - { - var originalErr = Console.Error; - using var stderr = new StringWriter(); - try - { - Console.SetError(stderr); - IndexCommandRunner.ParseArgs([".", "--zzzzzzzz"]); - Assert.Contains("Warning: unknown option '--zzzzzzzz'", stderr.ToString()); - Assert.DoesNotContain("Did you mean:", stderr.ToString()); - } - finally - { - Console.SetError(originalErr); - } - } + var options = IndexCommandRunner.ParseArgs([".", "--zzzzzzzz"]); + + Assert.Contains("unknown option '--zzzzzzzz'", options.ParseError); + Assert.DoesNotContain("Did you mean:", options.ParseError); } [Fact] diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 78f07860c2..57a97c2b4f 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1133,10 +1133,11 @@ public void NormalizeQueryLanguage_MapsJavaSpelling(string input) } [Fact] - public void ParseArgs_AllowsDashPrefixedPositionalQueryLiteral() + public void ParseArgs_EndOfOptionsAllowsDashPrefixedPositionalQueryLiteralWithOptions() { - var options = QueryCommandRunner.ParseArgs(["--open-reports", "--db", "query.db"], jsonDefault: false, allowNamedQuery: true); + var options = QueryCommandRunner.ParseArgs(["--", "--open-reports", "--db", "query.db"], jsonDefault: false, allowNamedQuery: true); + Assert.Null(options.ParseError); Assert.Equal("--open-reports", options.Query); Assert.Equal("query.db", options.DbPath); } @@ -3698,6 +3699,28 @@ public void RunSearch_UnsupportedFlagTypoInInlineValueForm_SuggestsClosestFlag_I Assert.Contains("Did you mean: --path?", stderr); } + [Fact] + public void RunSearch_UnknownFlagAfterQuery_ReturnsUsageError() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( + ["foo", "--dapth", "3"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("--dapth is not supported for search", stderr); + Assert.Contains("Did you mean: --path?", stderr); + } + + [Fact] + public void ParseArgs_EndOfOptionsAllowsDashPrefixedQueryLiteral() + { + var options = QueryCommandRunner.ParseArgs(["--", "--baar"], jsonDefault: false, allowNamedQuery: true); + + Assert.Null(options.ParseError); + Assert.Equal("--baar", options.Query); + } + // `find` previously emitted only the raw `Error: unsupported option for find: --paht` // line — round-2 fix routes the unknown token through the same suggester so users see // `Did you mean: --path?`. Covers both the separated and inline `=value` forms. @@ -3748,6 +3771,20 @@ public void QueryEntrypoints_InvalidNumericOptionsReturnUsageError(string scenar Assert.DoesNotContain("database not found", stderr); } + [Fact] + public void RunImpact_OutOfRangeDepthUpperBound_ReturnsUsageError_Issue1700() + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunImpact( + ["Target", "--depth", "999999999"], + _jsonOptions)); + + Assert.Equal(CommandExitCodes.UsageError, exitCode); + Assert.Equal(string.Empty, stdout); + Assert.Contains("--depth must be less than or equal to 64", stderr); + Assert.Contains($"Usage: {ConsoleUi.GetUsageLine("impact")}", stderr); + Assert.DoesNotContain("database not found", stderr); + } + // Regression lock for #161: valid integers that fail the positive / non-negative // range check used to be swallowed silently, leaving the command to run with the // default value and write real results to stdout with exit 0. Every case below @@ -28856,7 +28893,7 @@ public void RunSearch_DoubleDashEscapesSingleOptionLookingQueryToken_Issue923() } [Fact] - public void RunSearch_PositionalQueryAcceptsOptionLookingLiteral_Issue799() + public void RunSearch_EndOfOptionsAcceptsOptionLookingLiteral() { var projectRoot = TestProjectHelper.CreateTempProject("cdidx_issue799_search_positional_literal"); try @@ -28869,7 +28906,7 @@ public void RunSearch_PositionalQueryAcceptsOptionLookingLiteral_Issue799() "--open-reports appears here\n"); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSearch( - ["--open-reports", "--path", "README.md", "--db", dbPath, "--count"], + ["--", "--open-reports", "--path", "README.md", "--db", dbPath, "--count"], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode);