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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- **Interactive indexing/output no longer goes visually silent after `Indexing...`** — `IndexCommandRunner` now keeps a live `Indexing...` spinner active on interactive terminals during full-scan indexing work instead of stopping it before the first file and leaving the user with a fixed line until the next 50-file progress redraw or warning. Scoped update runs now also keep a live `Updating...` spinner under the same interactive-only contract, pausing only long enough to print `[WARN]`, `[OK]`, `[SKIP]`, `[DEL]`, `[ERR]`, or progress-bar output cleanly and then resuming immediately. Redirected stdout behavior is intentionally unchanged in shape: stdout still gets a single `Indexing...` banner plus line-based progress, while warnings stay on stderr, so logs/pipes do not collect spinner-frame noise. README examples now document both the interactive and redirected-output behavior, and `SELF_IMPROVEMENT.md` now explicitly treats visible liveness for long-running human-facing CLI flows as a regression bar. Added regression coverage ensuring redirected full-scan output prints `Indexing...` exactly once and redirected verbose update output does not duplicate the `Updating ... file(s)...` banner. Affected: `src/CodeIndex/Cli/IndexCommandRunner.cs`, `tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`, `README.md`, `SELF_IMPROVEMENT.md`.

#### Fixed
- **Added explicit regression coverage for the existing C# complex enum-value fix (#357)** — Current `main` already indexes C# enum members whose assigned values start with member access, casts, parenthesized expressions, and framework constant lookups such as `K.Foo`, `(int)1.5`, `(1 + 2)`, `(int)'A'`, `System.Int32.MaxValue`, and `[Flags]` aliases like `Execute = K.Foo`, but the open issue's exact `E1` / `Permissions` dogfood fixture was not pinned end-to-end. Added an extractor-level regression that keeps those value shapes under `SymbolExtractorTests`, plus `QueryCommandRunner` integration coverage that indexes the same file and asserts both `symbols --json --exact-name` and `definition --json --exact-name` still resolve the affected members under the correct owning enum. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #357.
- **Added explicit regression coverage for the existing C# object-initializer phantom-enum fix (#374)** — Current `main` already suppresses object-initializer assignments such as `Age = 30,` and `Priority = 2` from reappearing as phantom C# enum members, but the open issue's exact dogfood fixture was not pinned end-to-end. Added an extractor-level regression that uses the full issue repro with both multiline and inline `new Person { ... }` initializers, plus a `QueryCommandRunner` integration test that round-trips the indexed file through `symbols --json --exact-name` and asserts `Age` / `Priority` resolve only to the real `Person` properties. This keeps issue #374 closable without relying on an older, narrower unit fixture alone. Affected: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`, `tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`. Closes #374.
- **C# interface and struct members now keep their immediate type container instead of falling back to the enclosing namespace** — `AssignContainers` now treats `interface` and `struct` symbols as container-stack candidates, so members such as interface events and struct properties/events inherit `container_kind` / `container_name` / `container_qualified_name` from the enclosing type rather than the surrounding namespace. Added regression coverage for the #466 interface-event repro and the same-root-cause struct-member case tracked in #468. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #466.
- **C# interface events now stay attached to their interface container in symbol output** — `AssignContainers` now treats `interface` and `struct` as real symbol containers instead of only `class` / `namespace` / `enum`, so members declared inside them inherit the innermost enclosing type instead of falling back to the surrounding namespace. This fixes the concrete `#334` dogfood fixture where `IBus.Regular`, `IBus.StaticAbs`, and `IBus.StaticVirt` were emitted as namespace-level events in `symbols`, `outline`, and `inspect`, and adds regression coverage that locks both the modifier acceptance and the interface container attribution in place. Affected: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`. Closes #466.
Expand Down Expand Up @@ -765,6 +766,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- **対話ターミナルで `Indexing...` の後に視覚的に無音化しないよう改善** — `IndexCommandRunner` は、フルスキャン index の最初のファイル処理前に `Indexing...` スピナーを止めて次の 50 ファイル進捗更新または警告まで固定行のまま放置するのではなく、対話ターミナルでは重い処理中も `Indexing...` スピナーを維持するようになった。`--files` / `--commits` の scoped update も同じ interactive-only 契約で `Updating...` スピナーを維持し、`[WARN]` / `[OK]` / `[SKIP]` / `[DEL]` / `[ERR]` / プログレスバーを表示する直前だけ一時停止し、表示後すぐ再開する。stdout をリダイレクトした場合の形は意図的に維持しており、stdout には従来どおり `Indexing...` を 1 回だけ出し、その後は行ベースの進捗だけを出し、警告は引き続き stderr に分離するため、ログや pipe にスピナーフレームが混ざらない。README には interactive / redirected の両挙動を追記し、`SELF_IMPROVEMENT.md` にも「長時間の人間向け CLI では visible liveness を保つ」ことを回帰基準として明文化した。加えて、redirected full-scan 出力で `Indexing...` が 1 回だけ出ること、redirected verbose update で `Updating ... file(s)...` バナーが重複しないことを固定する回帰テストを追加。対象: `src/CodeIndex/Cli/IndexCommandRunner.cs`、`tests/CodeIndex.Tests/IndexCommandRunnerTests.cs`、`README.md`、`SELF_IMPROVEMENT.md`。

#### 修正
- **既存の C# 複雑 enum 値修正に issue #357 の明示回帰を追加** — 現在の `main` は `K.Foo`、`(int)1.5`、`(1 + 2)`、`(int)'A'`、`System.Int32.MaxValue`、さらに `Execute = K.Foo` のような `[Flags]` alias で始まる C# enum member をすでに index できるが、open issue 本文そのままの `E1` / `Permissions` dogfood fixture は end-to-end で固定されていなかった。これに対して、同じ value shape 群を `SymbolExtractorTests` で固定する extractor-level 回帰と、同じファイルを index したうえで `symbols --json --exact-name` と `definition --json --exact-name` の両方が影響対象 member を正しい親 enum 配下へ解決し続けることを確認する `QueryCommandRunner` integration coverage を追加した。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #357。
- **同一行の C# 型本体で brace-body auto-property が outer type の偽陽性に飲まれず残るよう修正** — `SymbolExtractor` は、same-line の C# property 正規表現が `public class C { ... }` の outer type header を偽陽性マッチした場合でも、その拒否を同一行スキャン全体の終了として扱わず、次の brace 区切り宣言へ進んで後続の本物 property を評価するようになった。これにより `public class C { public int P { get; set; } }`、`public struct S { public int Q { get; set; } }`、`public interface I { int R { get; } }` のような形でも nested property が落ちない。あわせて container 判定側でも `struct` / `interface` を same-line container として認識するようにし、復旧した property が namespace ではなく `S` / `I` に正しくぶら下がるようにした。same-line class/struct/interface auto-property と、control としての same-line method/event を固定する回帰テストも追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #470。
- **既存の C# object initializer 由来 phantom enum 抑止に issue #374 の明示回帰を追加** — 現在の `main` は `Age = 30,` や `Priority = 2` のような object initializer 代入を phantom C# enum member として再出力しないが、open の issue 本文そのものの dogfood fixture は end-to-end で固定されていなかった。multiline / inline の `new Person { ... }` を両方含む issue 本文相当の full repro を使う extractor-level 回帰に加え、index 後に `symbols --json --exact-name` まで通して `Age` / `Priority` が本物の `Person` property にしか解決しないことを固定する `QueryCommandRunner` integration test を追加した。これにより、より狭い既存 unit fixture だけに依存せず issue #374 を閉じられる状態を保てる。対象: `tests/CodeIndex.Tests/SymbolExtractorTests.cs`、`tests/CodeIndex.Tests/QueryCommandRunnerTests.cs`。Closes #374。
- **C# の interface / struct メンバーが、外側 namespace ではなく直近の型コンテナを保持するよう修正** — `AssignContainers` で親候補として積める symbol kind に `interface` と `struct` を追加し、interface event や struct property/event の `container_kind` / `container_name` / `container_qualified_name` が、周囲の namespace ではなく定義元の型を指すようにした。#466 の interface event 再現と、同じ根本原因だった struct member ケース(#468)を固定する回帰テストを追加。対象: `src/CodeIndex/Indexer/SymbolExtractor.cs`, `tests/CodeIndex.Tests/SymbolExtractorTests.cs`。Closes #466。
Expand Down
158 changes: 158 additions & 0 deletions tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4814,6 +4814,164 @@ public class Creator
}
}

[Fact]
public void RunSymbols_CSharpExactNameFindsEnumMembersWithComplexConstantExpressionValues_Issue357()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_symbols_csharp_enum_complex_value_issue357");
try
{
Directory.CreateDirectory(Path.Combine(projectRoot, "src"));
File.WriteAllText(
Path.Combine(projectRoot, "src", "E.cs"),
"""
namespace CsEnumComplexValue;

public class K
{
public const int Foo = 1;
}

public enum E1
{
Plain = 0,
Hex = 0xFF,
Combined = Plain | 0,
Shifted = 1 << 3,
Arith = 1 + 2,
ConstRef = K.Foo,
Casted = (int)1.5,
Paren = (1 + 2),
CharCast = (int)'A',
MemberAccess = System.Int32.MaxValue,
}

[System.Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
All = Read | Write,
Execute = K.Foo,
}
""");

var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db");
var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run(
[projectRoot, "--json"],
_jsonOptions));

var expectedContainers = new Dictionary<string, string>(StringComparer.Ordinal)
{
["ConstRef"] = "E1",
["Casted"] = "E1",
["Paren"] = "E1",
["CharCast"] = "E1",
["MemberAccess"] = "E1",
["Execute"] = "Permissions",
};

Assert.Equal(CommandExitCodes.Success, indexExitCode);
Assert.Equal(string.Empty, indexStderr);

foreach (var pair in expectedContainers)
{
var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols(
[pair.Key, "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"],
_jsonOptions));

var rows = ParseJsonLines(stdout);
Assert.Equal(CommandExitCodes.Success, exitCode);
Assert.Equal(string.Empty, stderr);
Assert.Single(rows);
Assert.Equal(pair.Key, rows[0].RootElement.GetProperty("name").GetString());
Assert.Equal("enum", rows[0].RootElement.GetProperty("kind").GetString());
Assert.Equal("enum", rows[0].RootElement.GetProperty("container_kind").GetString());
Assert.Equal(pair.Value, rows[0].RootElement.GetProperty("container_name").GetString());
}
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void RunDefinition_CSharpExactNameFindsEnumMembersWithComplexConstantExpressionValues_Issue357()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_definition_csharp_enum_complex_value_issue357");
try
{
Directory.CreateDirectory(Path.Combine(projectRoot, "src"));
File.WriteAllText(
Path.Combine(projectRoot, "src", "E.cs"),
"""
namespace CsEnumComplexValue;

public class K
{
public const int Foo = 1;
}

public enum E1
{
Plain = 0,
Hex = 0xFF,
Combined = Plain | 0,
Shifted = 1 << 3,
Arith = 1 + 2,
ConstRef = K.Foo,
Casted = (int)1.5,
Paren = (1 + 2),
CharCast = (int)'A',
MemberAccess = System.Int32.MaxValue,
}

[System.Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
All = Read | Write,
Execute = K.Foo,
}
""");

var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db");
var (indexExitCode, _, indexStderr) = CaptureConsole(() => IndexCommandRunner.Run(
[projectRoot, "--json"],
_jsonOptions));

Assert.Equal(CommandExitCodes.Success, indexExitCode);
Assert.Equal(string.Empty, indexStderr);

foreach (var pair in new Dictionary<string, string>(StringComparer.Ordinal)
{
["ConstRef"] = "E1",
["Execute"] = "Permissions",
})
{
var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunDefinition(
[pair.Key, "--db", dbPath, "--json", "--exact-name", "--lang", "csharp"],
_jsonOptions));

var rows = ParseJsonLines(stdout);
Assert.Equal(CommandExitCodes.Success, exitCode);
Assert.Equal(string.Empty, stderr);
Assert.Single(rows);
Assert.Equal(pair.Key, rows[0].RootElement.GetProperty("name").GetString());
Assert.Equal("enum", rows[0].RootElement.GetProperty("kind").GetString());
Assert.Equal("enum", rows[0].RootElement.GetProperty("container_kind").GetString());
Assert.Equal(pair.Value, rows[0].RootElement.GetProperty("container_name").GetString());
}
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void RunSymbols_ExactNameStaleCSharpCanonicalNamesReportDegradedState()
{
Expand Down
61 changes: 61 additions & 0 deletions tests/CodeIndex.Tests/SymbolExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6771,6 +6771,67 @@ public class Creator
Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "CreateMany" && s.ContainerName == "Creator");
}

[Fact]
public void Extract_CSharp_Issue357_EnumMembersWithComplexConstantExpressionsStayIndexed()
{
// Current main already captures these enum members, but the open issue fixture was not
// pinned in tests. Keep the exact value-shape mix here so future regex tightening does
// not silently re-drop member-access, cast, or parenthesized constant expressions.
// 現在の main はこれらの enum member を抽出できるが、open issue の fixture 自体は
// テストで固定されていなかった。将来の regex 調整で member access / cast /
// parenthesized constant expression が黙って再脱落しないよう、この value-shape
// の混在をここで固定する。
var content = """
namespace CsEnumComplexValue;

public class K
{
public const int Foo = 1;
}

public enum E1
{
Plain = 0,
Hex = 0xFF,
Combined = Plain | 0,
Shifted = 1 << 3,
Arith = 1 + 2,
ConstRef = K.Foo,
Casted = (int)1.5,
Paren = (1 + 2),
CharCast = (int)'A',
MemberAccess = System.Int32.MaxValue,
}

[System.Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
All = Read | Write,
Execute = K.Foo,
}
""";
var symbols = SymbolExtractor.Extract(1, "csharp", content);

foreach (var name in new[] { "Plain", "Hex", "Combined", "Shifted", "Arith", "ConstRef", "Casted", "Paren", "CharCast", "MemberAccess" })
{
var symbol = Assert.Single(symbols.Where(s => s.Name == name));
Assert.Equal("enum", symbol.Kind);
Assert.Equal("E1", symbol.ContainerName);
Assert.Equal("enum", symbol.ContainerKind);
}

foreach (var name in new[] { "None", "Read", "Write", "All", "Execute" })
{
var symbol = Assert.Single(symbols.Where(s => s.Name == name));
Assert.Equal("enum", symbol.Kind);
Assert.Equal("Permissions", symbol.ContainerName);
Assert.Equal("enum", symbol.ContainerKind);
}
}

[Fact]
public void Extract_CSharp_DetectsRegionDirectives()
{
Expand Down
Loading