Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/5210.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 5210
affected:
- src/CodeIndex/Cli/ProgramRunner.QueryArguments.cs
- tests/CodeIndex.Tests/QueryCommandRunnerIssue5210Tests.cs
---

## English

- **Explicit search modes now keep global progress control parseable (#5210)** — `search --recipe`, child recipe selectors, explicit queries, and named-query batches now accept `--no-progress` before or after their arguments without treating it as a positional query, while plain option-like ad-hoc queries remain unchanged.

## 日本語

- **明示的な search モードでグローバルな進捗制御を正しく解析するようになりました (#5210)** — `search --recipe`、recipe の child selector、明示的な query、named-query batch では、引数の前後にある `--no-progress` を位置引数 query と誤認せず受け付けるようになり、通常の option-like ad-hoc query の挙動は維持されます。
38 changes: 38 additions & 0 deletions src/CodeIndex/Cli/ProgramRunner.QueryArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ private static QueryCommandTokenRole GetQueryCommandTokenRole(string commandName
return QueryCommandTokenRole.CommandOptionValue;
}

if (HasExplicitSearchQueryMode(commandName, subArgs, withValues, flagOnly))
return QueryCommandTokenRole.None;

if (!CommandAcceptsQueryLiteral(commandName))
return QueryCommandTokenRole.None;

Expand Down Expand Up @@ -225,6 +228,41 @@ private static QueryCommandTokenRole GetQueryCommandTokenRole(string commandName
return QueryCommandTokenRole.FirstQueryLiteral;
}

private static bool HasExplicitSearchQueryMode(
string commandName,
string[] subArgs,
IReadOnlySet<string> withValues,
IReadOnlySet<string> flagOnly)
{
if (!string.Equals(commandName, "search", StringComparison.Ordinal))
return false;

for (var i = 0; i < subArgs.Length; i++)
{
var arg = subArgs[i];
if (arg == "--")
break;

if (TryGetInlineOptionName(arg, out var topLevelInlineName)
&& TopLevelValueOptionNames.Contains(topLevelInlineName))
continue;
if (TopLevelValueOptionNames.Contains(arg))
{
if (i + 1 < subArgs.Length)
i++;
continue;
}

var normalizedArg = NormalizeCommandOptionToken(arg, withValues, flagOnly, out var hasInlineValue);
if (normalizedArg is "--query" or "--recipe" or "--named-query")
return true;
if (!hasInlineValue && withValues.Contains(normalizedArg) && i + 1 < subArgs.Length)
i++;
}

return false;
}

private static bool IsInspectPathLineMode(string commandName, string[] subArgs)
{
if (!string.Equals(commandName, "inspect", StringComparison.Ordinal))
Expand Down
12 changes: 12 additions & 0 deletions tests/CodeIndex.Tests/MetricsSinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ public void TryConsumeMetricsFlag_AfterDoubleDash_PreservesQueryEscape()
Assert.Equal(new[] { "search", "--", "--metrics=foo" }, args);
}

[Fact]
public void TryConsumeMetricsFlag_MissingValueBeforeRecipe_PreservesSearchArguments_Issue5210()
{
var args = new[] { "search", "--metrics", "--recipe", "risky-code", "--json" };
var expected = args.ToArray();

Assert.True(ProgramRunner.TryConsumeMetricsFlag(ref args, out var path, out var error));
Assert.Null(path);
Assert.Empty(error);
Assert.Equal(expected, args);
}

[Fact]
public void TryParseLanguageFromArgs_ReturnsValueWhenPresent()
{
Expand Down
97 changes: 97 additions & 0 deletions tests/CodeIndex.Tests/QueryCommandRunnerIssue5210Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using CodeIndex.Cli;
using static CodeIndex.Tests.QueryCommandTestSupport;

namespace CodeIndex.Tests;

[Collection("Console sensitive")]
public sealed class QueryCommandRunnerIssue5210Tests
{
[Fact]
public void RunSearch_RecipeAndNamedQueryConsumeNoProgressAsGlobalFlag_Issue5210()
{
using var project = TestProjectHelper.CreateTempProjectScope("cdidx_search_global_flag_5210");
var dbPath = TestProjectHelper.CreateProjectDb(project.Root);
TestProjectHelper.InsertIndexedFile(
dbPath,
"src/Issue5210.cs",
"csharp",
"public static class Issue5210 { public const string Needle = \"Issue5210Needle\"; }\n");

var recipeArgs = new[]
{
new[]
{
"search", "--no-progress",
"--recipe", "phrase-risk-patterns/obsolete-production-code",
"--json=ndjson", "--results-only", "--limit", "1",
"--db", dbPath, "--max-json-bytes", "1048576",
},
new[]
{
"search",
"--recipe", "phrase-risk-patterns/obsolete-production-code",
"--json=ndjson", "--results-only", "--limit", "1",
"--db", dbPath, "--max-json-bytes", "1048576",
"--no-progress",
},
new[]
{
"search",
"--recipe", "phrase-risk-patterns/obsolete-production-code",
"--json=ndjson", "--results-only", "--limit", "1",
"--db", dbPath, "--max-json-bytes", "1048576",
"--quiet",
},
};

foreach (var args in recipeArgs)
{
var (exitCode, _, stderr) = CaptureConsole(() =>
ProgramRunner.Run(args, JsonOptions, "1.44.3-test"));

Assert.Equal(CommandExitCodes.Success, exitCode);
Assert.Equal(string.Empty, stderr);
}

var (namedExitCode, _, namedStderr) = CaptureConsole(() => ProgramRunner.Run(
[
"search", "--named-query", "needle=Issue5210Needle",
"--db", dbPath, "--json=ndjson", "--results-only", "--limit", "1",
"--no-progress",
],
JsonOptions,
"1.44.3-test"));

Assert.Equal(CommandExitCodes.Success, namedExitCode);
Assert.Equal(string.Empty, namedStderr);
}

[Fact]
public void RunSearch_NoProgressRemainsAvailableAsOptionLikeAdHocQuery_Issue5210()
{
using var project = TestProjectHelper.CreateTempProjectScope("cdidx_search_literal_flag_5210");
var dbPath = TestProjectHelper.CreateProjectDb(project.Root);
TestProjectHelper.InsertIndexedFile(
dbPath,
"src/Issue5210.cs",
"csharp",
"// --no-progress remains searchable as literal query text.\n");

var commands = new[]
{
new[] { "search", "--db", dbPath, "--json", "--count", "--no-progress" },
new[] { "search", "--query", "--no-progress", "--db", dbPath, "--json", "--count" },
};

foreach (var args in commands)
{
var (exitCode, stdout, stderr) = CaptureConsole(() =>
ProgramRunner.Run(args, JsonOptions, "1.44.3-test"));
using var result = ParseJsonOutput(stdout);

Assert.Equal(CommandExitCodes.Success, exitCode);
Assert.Equal(string.Empty, stderr);
Assert.Equal("--no-progress", result.RootElement.GetProperty("query").GetString());
}
}
}
Loading