diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..d9cdb5166e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cs,csx}] +indent_style = space +indent_size = 4 + +[*.{csproj,props,targets,slnx,xml}] +indent_style = space +indent_size = 2 + +[*.{json,yml,yaml,md,sh,toml}] +indent_style = space +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..2a83bad38b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +*.cs text eol=lf +*.csproj text eol=lf +*.props text eol=lf +*.targets text eol=lf +*.sln text eol=lf +*.slnx text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.md text eol=lf +*.sh text eol=lf +*.toml text eol=lf +.editorconfig text eol=lf +Makefile text eol=lf diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index c9ab97d3ed..e6a9ee42b5 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -178,6 +178,13 @@ jobs: throw "High or critical NuGet vulnerability detected." } + - name: Verify formatting + run: dotnet format CodeIndex.sln --verify-no-changes --no-restore --verbosity minimal + + - name: Verify developer task wrapper + if: matrix.os == 'ubuntu-latest' && matrix.test-framework == 'net8.0' + run: make lint + - name: Build run: dotnet build tests/CodeIndex.Tests/CodeIndex.Tests.csproj --configuration Release --framework ${{ matrix.test-framework }} --no-restore @@ -192,6 +199,7 @@ jobs: "--no-build", "--nologo", "--settings", "tests/CodeIndex.Tests/CodeIndex.Tests.runsettings", + "--collect", "XPlat Code Coverage", "--blame-crash", "--blame-hang", "--blame-hang-timeout", "5m", @@ -237,6 +245,14 @@ jobs: TestResults/**/*Sequence*.xml TestResults/**/*.hangdump + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: Coverage-${{ matrix.os }}-${{ matrix.test-framework }} + if-no-files-found: warn + path: TestResults/**/coverage.cobertura.xml + - name: Publish if: matrix.os == 'ubuntu-latest' && matrix.test-framework == 'net8.0' run: dotnet publish src/CodeIndex/CodeIndex.csproj --configuration Release --no-build --output publish diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9802724505..20bd380478 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,13 +80,15 @@ Before opening a pull request, run the checks that match the change. For code changes, the default full validation is: ```bash -dotnet restore CodeIndex.sln -dotnet build CodeIndex.sln -c Release -dotnet test CodeIndex.sln -c Release +make lint +make build +make test ``` -Use narrower `dotnet test --filter ...` commands while iterating, then finish -with the relevant broader validation before the PR. +Set `FRAMEWORK=net9.0` when you need to match that CI lane, or call +`./dev.sh ` directly on systems without `make`. Use narrower +`dotnet test --filter ...` commands while iterating, then finish with the +relevant broader validation before the PR. ## Changelog Fragments diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 124036272a..50d3d819c8 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -7,10 +7,31 @@ ```bash dotnet build dotnet test +dotnet format CodeIndex.sln --verify-no-changes dotnet test tests/CodeIndex.Tests/CodeIndex.Tests.csproj --settings tests/CodeIndex.Tests/CodeIndex.Tests.runsettings --blame-crash --blame-hang --blame-hang-timeout 5m dotnet run --project src/CodeIndex -- [options] ``` +CI enforces repository formatting with `.editorconfig` and treats compiler +warnings as errors through `Directory.Build.props`, so local changes should pass +the format check before opening a PR. Existing trim-analysis warnings are +explicitly listed in `WarningsNotAsErrors` until they are fixed without blocking +ordinary compiler-warning enforcement, and ILLink keeps reporting trim warnings +without failing trimmed publish smoke tests. + +Common local workflows are also available through the top-level task wrappers: + +```bash +make build +make test +make lint +make coverage +make mcp-smoke +``` + +Use `FRAMEWORK=net9.0 make test` to match the net9 CI lane. On systems without +`make`, run the same tasks as `./dev.sh build`, `./dev.sh test`, and so on. + CLI help is intentionally layered: `cdidx --help` stays brief, `cdidx --help-all` prints the full command/flag/example reference, `cdidx --help-flags` prints only shared flag tables, and `cdidx --help` prints one command's usage diff --git a/Directory.Build.props b/Directory.Build.props index fb8b0db1a6..a2ba2e8677 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,6 +13,9 @@ 詳細は DEVELOPER_GUIDE.md および issue #1556 を参照。 --> true + true + $(WarningsNotAsErrors);IL2026;IL2067;IL2072;IL2075 + false diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..ac10c9bdbe --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +CONFIGURATION ?= Release +FRAMEWORK ?= net8.0 + +.PHONY: build test lint format coverage mcp-smoke clean + +build: + CONFIGURATION="$(CONFIGURATION)" FRAMEWORK="$(FRAMEWORK)" ./dev.sh build + +test: + CONFIGURATION="$(CONFIGURATION)" FRAMEWORK="$(FRAMEWORK)" ./dev.sh test + +lint: + ./dev.sh lint + +format: + ./dev.sh format + +coverage: + CONFIGURATION="$(CONFIGURATION)" FRAMEWORK="$(FRAMEWORK)" ./dev.sh coverage + +mcp-smoke: + CONFIGURATION="$(CONFIGURATION)" ./dev.sh mcp-smoke + +clean: + CONFIGURATION="$(CONFIGURATION)" ./dev.sh clean diff --git a/changelog.d/unreleased/1608.fixed.md b/changelog.d/unreleased/1608.fixed.md new file mode 100644 index 0000000000..7867169e83 --- /dev/null +++ b/changelog.d/unreleased/1608.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 1608 +affected: + - .github/workflows/dotnet.yml +--- + +## English + +- **CI now collects coverage reports (#1608)** — the build workflow runs tests with the XPlat Code Coverage collector and uploads Cobertura reports as per-lane artifacts. + +## 日本語 + +- **CI が coverage report を収集するようになりました (#1608)** — build workflow は XPlat Code Coverage collector 付きでテストを実行し、Cobertura report を lane ごとの artifact としてアップロードします。 diff --git a/changelog.d/unreleased/1609.internal.md b/changelog.d/unreleased/1609.internal.md new file mode 100644 index 0000000000..364471a613 --- /dev/null +++ b/changelog.d/unreleased/1609.internal.md @@ -0,0 +1,19 @@ +--- +category: internal +issues: + - 1609 +affected: + - .gitattributes + - .editorconfig + - Directory.Build.props + - .github/workflows/dotnet.yml + - DEVELOPER_GUIDE.md +--- + +## English + +- **Formatting and warning drift are now gated (#1609)** — the repository has root formatting metadata, CI verifies `dotnet format`, and builds treat compiler warnings as errors with an explicit allowlist for existing trim-analysis warnings. + +## 日本語 + +- **format と warning の drift を CI で検出するようになりました (#1609)** — repository root に format metadata を追加し、CI が `dotnet format` を検証し、build では既存の trim-analysis warning を明示 allowlist に入れたうえで compiler warning を error として扱います。 diff --git a/changelog.d/unreleased/1611.internal.md b/changelog.d/unreleased/1611.internal.md new file mode 100644 index 0000000000..8c802fde04 --- /dev/null +++ b/changelog.d/unreleased/1611.internal.md @@ -0,0 +1,19 @@ +--- +category: internal +issues: + - 1611 +affected: + - Makefile + - dev.sh + - .github/workflows/dotnet.yml + - CONTRIBUTING.md + - DEVELOPER_GUIDE.md +--- + +## English + +- **Common developer tasks now have shared wrappers (#1611)** — contributors can run build, test, lint, format, coverage, MCP smoke, and clean tasks through `make` or `./dev.sh`, and CI verifies the wrapper path. + +## 日本語 + +- **共通の開発タスクに共有 wrapper を追加しました (#1611)** — contributor は build / test / lint / format / coverage / MCP smoke / clean を `make` または `./dev.sh` から実行でき、CI でも wrapper 経路を検証します。 diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000000..43bf32ff6f --- /dev/null +++ b/dev.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONFIGURATION="${CONFIGURATION:-Release}" +FRAMEWORK="${FRAMEWORK:-net8.0}" +RESULTS_DIRECTORY="${RESULTS_DIRECTORY:-./TestResults}" + +usage() { + cat <<'USAGE' +Usage: ./dev.sh + +Tasks: + build Build the test project for FRAMEWORK. + test Run the test suite for FRAMEWORK. + lint Verify formatting without changing files. + format Apply dotnet format. + coverage Run tests with XPlat Code Coverage. + mcp-smoke Run a minimal MCP help/build smoke. + clean Clean build outputs and local test artifacts. +USAGE +} + +task="${1:-}" +case "$task" in + build) + dotnet build tests/CodeIndex.Tests/CodeIndex.Tests.csproj \ + --configuration "$CONFIGURATION" \ + --framework "$FRAMEWORK" + ;; + test) + dotnet test tests/CodeIndex.Tests/CodeIndex.Tests.csproj \ + --configuration "$CONFIGURATION" \ + --framework "$FRAMEWORK" \ + --settings tests/CodeIndex.Tests/CodeIndex.Tests.runsettings \ + --blame-crash \ + --blame-hang \ + --blame-hang-timeout 5m + ;; + lint) + dotnet format CodeIndex.sln --verify-no-changes --verbosity minimal + ;; + format) + dotnet format CodeIndex.sln --verbosity minimal + ;; + coverage) + dotnet test tests/CodeIndex.Tests/CodeIndex.Tests.csproj \ + --configuration "$CONFIGURATION" \ + --framework "$FRAMEWORK" \ + --settings tests/CodeIndex.Tests/CodeIndex.Tests.runsettings \ + --collect "XPlat Code Coverage" \ + --results-directory "$RESULTS_DIRECTORY" + ;; + mcp-smoke) + dotnet build src/CodeIndex/CodeIndex.csproj --configuration "$CONFIGURATION" + dotnet run --project src/CodeIndex -- mcp --help > /dev/null + ;; + clean) + dotnet clean CodeIndex.sln --configuration "$CONFIGURATION" + rm -rf "$RESULTS_DIRECTORY" publish + ;; + -h|--help|help|"") + usage + ;; + *) + usage >&2 + exit 2 + ;; +esac diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 22eff8a536..cc012e6190 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -320,55 +320,55 @@ public static string[] GetSpinnerFrames(string? easterEgg) return easterEgg switch { - "--sushi" => - [ - "\U0001f363 Slicing ", "\U0001f363 Slicing. ", "\U0001f363 Slicing.. ", "\U0001f363 Slicing... ", + "--sushi" => + [ + "\U0001f363 Slicing ", "\U0001f363 Slicing. ", "\U0001f363 Slicing.. ", "\U0001f363 Slicing... ", "\U0001f363 Shaping ", "\U0001f363 Shaping. ", "\U0001f363 Shaping.. ", "\U0001f363 Shaping... ", "\U0001f363 Pressing ", "\U0001f363 Pressing. ", "\U0001f363 Pressing.. ", "\U0001f363 Pressing... ", "\U0001f363 Itadakimasu! ", ], - "--coffee" => - [ - "\u2615 Grinding ", "\u2615 Grinding. ", "\u2615 Grinding.. ", "\u2615 Grinding... ", + "--coffee" => + [ + "\u2615 Grinding ", "\u2615 Grinding. ", "\u2615 Grinding.. ", "\u2615 Grinding... ", "\u2615 Heating ", "\u2615 Heating. ", "\u2615 Heating.. ", "\u2615 Heating... ", "\u2615 Brewing ", "\u2615 Brewing. ", "\u2615 Brewing.. ", "\u2615 Brewing... ", ], - "--ramen" => - [ - "\U0001f35c Boiling ", "\U0001f35c Boiling. ", "\U0001f35c Boiling.. ", "\U0001f35c Boiling... ", + "--ramen" => + [ + "\U0001f35c Boiling ", "\U0001f35c Boiling. ", "\U0001f35c Boiling.. ", "\U0001f35c Boiling... ", "\U0001f35c Steaming ", "\U0001f35c Steaming. ", "\U0001f35c Steaming.. ", "\U0001f35c Steaming... ", "\U0001f35c Slurping ", "\U0001f35c Slurping. ", "\U0001f35c Slurping.. ", "\U0001f35c Slurping... ", "\U0001f35c Itadakimasu! ", ], - "--wine" => - [ - "\U0001f377 Crushing ", "\U0001f377 Crushing. ", "\U0001f377 Crushing.. ", "\U0001f377 Crushing... ", + "--wine" => + [ + "\U0001f377 Crushing ", "\U0001f377 Crushing. ", "\U0001f377 Crushing.. ", "\U0001f377 Crushing... ", "\U0001f377 Aging ", "\U0001f377 Aging. ", "\U0001f377 Aging.. ", "\U0001f377 Aging... ", "\U0001f377 Pouring ", "\U0001f377 Pouring. ", "\U0001f377 Pouring.. ", "\U0001f377 Pouring... ", "\U0001f377 Sant\u00e9! ", ], - "--beer" => - [ - "\U0001f37a Tapping ", "\U0001f37a Tapping. ", "\U0001f37a Tapping.. ", "\U0001f37a Tapping... ", + "--beer" => + [ + "\U0001f37a Tapping ", "\U0001f37a Tapping. ", "\U0001f37a Tapping.. ", "\U0001f37a Tapping... ", "\U0001f37a Pouring ", "\U0001f37a Pouring. ", "\U0001f37a Pouring.. ", "\U0001f37a Pouring... ", "\U0001f37a Foaming ", "\U0001f37a Foaming. ", "\U0001f37a Foaming.. ", "\U0001f37a Foaming... ", "\U0001f37a Cheers! ", ], - "--matcha" => - [ - "\U0001f375 Sifting ", "\U0001f375 Sifting. ", "\U0001f375 Sifting.. ", "\U0001f375 Sifting... ", + "--matcha" => + [ + "\U0001f375 Sifting ", "\U0001f375 Sifting. ", "\U0001f375 Sifting.. ", "\U0001f375 Sifting... ", "\U0001f375 Pouring ", "\U0001f375 Pouring. ", "\U0001f375 Pouring.. ", "\U0001f375 Pouring... ", "\U0001f375 Whisking ", "\U0001f375 Whisking. ", "\U0001f375 Whisking.. ", "\U0001f375 Whisking... ", "\U0001f375 Douzo! ", ], - "--whisky" => - [ - "\U0001f943 Mashing ", "\U0001f943 Mashing. ", "\U0001f943 Mashing.. ", "\U0001f943 Mashing... ", + "--whisky" => + [ + "\U0001f943 Mashing ", "\U0001f943 Mashing. ", "\U0001f943 Mashing.. ", "\U0001f943 Mashing... ", "\U0001f943 Distilling ", "\U0001f943 Distilling. ", "\U0001f943 Distilling.. ", "\U0001f943 Distilling... ", "\U0001f943 Aging ", "\U0001f943 Aging. ", "\U0001f943 Aging.. ", "\U0001f943 Aging... ", "\U0001f943 Slainte! ", ], - // Default: Braille spinner / デフォルト: ブレイルスピナー + // Default: Braille spinner / デフォルト: ブレイルスピナー _ => DefaultBrailleSpinnerFrames, }; } @@ -594,11 +594,11 @@ public static void PrintEasterEggMessage(string flag, UiLanguage? languageOverri { var pair = flag switch { - "--sushi" => UiMessages.EasterEggSushi, + "--sushi" => UiMessages.EasterEggSushi, "--coffee" => UiMessages.EasterEggCoffee, - "--ramen" => UiMessages.EasterEggRamen, - "--wine" => UiMessages.EasterEggWine, - "--beer" => UiMessages.EasterEggBeer, + "--ramen" => UiMessages.EasterEggRamen, + "--wine" => UiMessages.EasterEggWine, + "--beer" => UiMessages.EasterEggBeer, "--matcha" => UiMessages.EasterEggMatcha, "--whisky" => UiMessages.EasterEggWhisky, _ => null, diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Update.cs b/src/CodeIndex/Cli/IndexCommandRunner.Update.cs index 95631c05c9..6a8722111a 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Update.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Update.cs @@ -188,7 +188,7 @@ private static int RunUpdateMode( if (!options.Json && !options.Quiet) Console.WriteLine($"Updating {ConsoleUi.Counted(targetPaths.Count, "file")}..."); CancellationTokenSource? updateCts = null; - var interactiveUpdateSpinner = !options.Json && !options.Quiet && ConsoleUi.ShouldUseInteractiveConsole(); + var interactiveUpdateSpinner = !options.Json && !options.Quiet && ConsoleUi.ShouldUseInteractiveConsole(); int updated = 0, removed = 0, skipped = 0, warnings = 0, errors = 0; var errorList = new List(); var warningList = new List(); @@ -486,77 +486,147 @@ void ThrowIfUpdateCancelled() } var pathFilter = indexer.EvaluatePathFilter(absPath); - RecordScanErrors(pathFilter.Errors); - if (pathFilter.ShouldSkip) - { - if (!pathFilter.ShouldDeleteExisting) + RecordScanErrors(pathFilter.Errors); + if (pathFilter.ShouldSkip) { - skipped++; - if (options.Verbose && !options.Json && !options.Quiet) + if (!pathFilter.ShouldDeleteExisting) { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); - ResumeUpdateSpinnerAfterConsoleWrite(); + skipped++; + if (options.Verbose && !options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine($" [SKIP] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } + continue; } - continue; - } - if (!writer.HasFileAtPath(dbPath)) - { - skipped++; - if (options.Verbose && !options.Json && !options.Quiet) + if (!writer.HasFileAtPath(dbPath)) { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); - ResumeUpdateSpinnerAfterConsoleWrite(); + skipped++; + if (options.Verbose && !options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine($" [SKIP] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } + continue; + } + + DemoteReadinessOnce(); + using var deleteTxn = writer.BeginTransaction(); + if (writer.DeleteFileByPath(dbPath)) + { + WriteProjectRootOnce(); + deleteTxn.Commit(); + removed++; + ftsMutated = true; + if (options.Verbose && !options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine($" [DEL ] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } + } + else + { + skipped++; + if (options.Verbose && !options.Json) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine($" [SKIP] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } } continue; } - DemoteReadinessOnce(); - using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(dbPath)) + var indexability = FileIndexer.GetFileIndexability(absPath); + var detection = FileIndexer.TryDetectLanguage(absPath); + if (indexability == FileIndexer.FileProbeStatus.Missing || detection.Status == FileIndexer.FileProbeStatus.Missing) { - WriteProjectRootOnce(); - deleteTxn.Commit(); - removed++; - ftsMutated = true; - if (options.Verbose && !options.Json && !options.Quiet) + var message = $"{relPath}: skipped because it was deleted during indexing."; + warnings++; + warningList.Add(new CliJsonMessage(relPath, message)); + if (!options.Json && !options.Quiet) { PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [DEL ] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); + ConsoleUi.PrintWarning(message); ResumeUpdateSpinnerAfterConsoleWrite(); } + + if (writer.HasFileAtPath(dbPath)) + { + DemoteReadinessOnce(); + using var deleteTxn = writer.BeginTransaction(); + if (writer.DeleteFileByPath(dbPath)) + { + WriteProjectRootOnce(); + deleteTxn.Commit(); + removed++; + ftsMutated = true; + } + } + else + { + skipped++; + } + continue; } - else + + if (indexability == FileIndexer.FileProbeStatus.ProbeFailed || detection.Status == FileIndexer.FileProbeStatus.ProbeFailed) { - skipped++; - if (options.Verbose && !options.Json) + DemoteReadinessOnce(); + + errors++; + errorList.Add(new CliJsonMessage(relPath, "Could not probe file for indexability/language.")); + if (!options.Json) { PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} ({DescribePathFilter(pathFilter.FilterKind)})"); + if (options.Verbose) + Console.Error.WriteLine($" [ERR ] {relPath}: Could not probe file for indexability/language."); + else + Console.Error.WriteLine($" [ERR ] {relPath}: Could not probe file for indexability/language."); ResumeUpdateSpinnerAfterConsoleWrite(); } + continue; } - continue; - } - var indexability = FileIndexer.GetFileIndexability(absPath); - var detection = FileIndexer.TryDetectLanguage(absPath); - if (indexability == FileIndexer.FileProbeStatus.Missing || detection.Status == FileIndexer.FileProbeStatus.Missing) - { - var message = $"{relPath}: skipped because it was deleted during indexing."; - warnings++; - warningList.Add(new CliJsonMessage(relPath, message)); - if (!options.Json && !options.Quiet) + if (indexability != FileIndexer.FileProbeStatus.Supported || detection.Status != FileIndexer.FileProbeStatus.Supported) { - PauseUpdateSpinnerForConsoleWrite(); - ConsoleUi.PrintWarning(message); - ResumeUpdateSpinnerAfterConsoleWrite(); - } + if (!writer.HasFileAtPath(dbPath)) + { + using var purgeTxn = writer.BeginTransaction(); + var purged = projectRootWritten + ? writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, dbPath) + : 0; + if (purged > 0) + { + DemoteReadinessOnce(); + WriteProjectRootOnce(); + purgeTxn.Commit(); + removed += purged; + ftsMutated = true; + if (options.Verbose && !options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine($" [DEL ] {relPath} (unsupported renamed target)"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } + } + else + { + skipped++; + if (options.Verbose && !options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine($" [SKIP] {relPath} (unsupported type)"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } + } + continue; + } - if (writer.HasFileAtPath(dbPath)) - { DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); if (writer.DeleteFileByPath(dbPath)) @@ -565,59 +635,17 @@ void ThrowIfUpdateCancelled() deleteTxn.Commit(); removed++; ftsMutated = true; - } - } - else - { - skipped++; - } - continue; - } - - if (indexability == FileIndexer.FileProbeStatus.ProbeFailed || detection.Status == FileIndexer.FileProbeStatus.ProbeFailed) - { - DemoteReadinessOnce(); - - errors++; - errorList.Add(new CliJsonMessage(relPath, "Could not probe file for indexability/language.")); - if (!options.Json) - { - PauseUpdateSpinnerForConsoleWrite(); - if (options.Verbose) - Console.Error.WriteLine($" [ERR ] {relPath}: Could not probe file for indexability/language."); - else - Console.Error.WriteLine($" [ERR ] {relPath}: Could not probe file for indexability/language."); - ResumeUpdateSpinnerAfterConsoleWrite(); - } - continue; - } - - if (indexability != FileIndexer.FileProbeStatus.Supported || detection.Status != FileIndexer.FileProbeStatus.Supported) - { - if (!writer.HasFileAtPath(dbPath)) - { - using var purgeTxn = writer.BeginTransaction(); - var purged = projectRootWritten - ? writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, dbPath) - : 0; - if (purged > 0) - { - DemoteReadinessOnce(); - WriteProjectRootOnce(); - purgeTxn.Commit(); - removed += purged; - ftsMutated = true; if (options.Verbose && !options.Json && !options.Quiet) { PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [DEL ] {relPath} (unsupported renamed target)"); + Console.WriteLine($" [DEL ] {relPath} (no longer indexable)"); ResumeUpdateSpinnerAfterConsoleWrite(); } } else { skipped++; - if (options.Verbose && !options.Json && !options.Quiet) + if (options.Verbose && !options.Json) { PauseUpdateSpinnerForConsoleWrite(); Console.WriteLine($" [SKIP] {relPath} (unsupported type)"); @@ -627,247 +655,219 @@ void ThrowIfUpdateCancelled() continue; } - DemoteReadinessOnce(); - using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(dbPath)) + if (FileIndexer.TryGetFileIdentity(absPath, out var identity) && !visitedFileIdentities.Add(identity)) { - WriteProjectRootOnce(); - deleteTxn.Commit(); - removed++; - ftsMutated = true; - if (options.Verbose && !options.Json && !options.Quiet) + var message = "Skipped hardlinked file because the same file content was already indexed from another path."; + warnings++; + warningList.Add(new CliJsonMessage(relPath, message)); + if (!options.Json && !options.Quiet) { PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [DEL ] {relPath} (no longer indexable)"); + ConsoleUi.PrintWarning($"{relPath}: {message}"); ResumeUpdateSpinnerAfterConsoleWrite(); } + + if (!writer.HasFileAtPath(dbPath)) + { + skipped++; + continue; + } + + DemoteReadinessOnce(); + using var deleteTxn = writer.BeginTransaction(); + if (writer.DeleteFileByPath(dbPath)) + { + WriteProjectRootOnce(); + deleteTxn.Commit(); + removed++; + ftsMutated = true; + } + else + { + skipped++; + } + continue; } - else + + var statReusableLanguage = TryDetectStatReusableLanguage(absPath); + var statMatchedId = TryGetUnchangedFileIdFromStat( + writer, + projectRoot, + absPath, + statReusableLanguage, + allowReuse: symbolKindFilterMatchesPrior + && statReusableLanguage is not ("javascript" or "typescript") + && (statReusableLanguage != "csharp" || csharpSymbolNameContractMatchesCurrent) + && (statReusableLanguage != "csharp" || !csharpWorkspace.HasStaticInterfaceContracts) + && (statReusableLanguage != "sql" || sqlGraphContractMatchesCurrent)); + if (statMatchedId != null) { skipped++; - if (options.Verbose && !options.Json) + if (options.Verbose && !options.Json && !options.Quiet) { PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} (unsupported type)"); + Console.WriteLine($" [SKIP] {relPath} (unchanged)"); ResumeUpdateSpinnerAfterConsoleWrite(); } + continue; } - continue; - } - if (FileIndexer.TryGetFileIdentity(absPath, out var identity) && !visitedFileIdentities.Add(identity)) - { - var message = "Skipped hardlinked file because the same file content was already indexed from another path."; - warnings++; - warningList.Add(new CliJsonMessage(relPath, message)); - if (!options.Json && !options.Quiet) + var (record, content, rawBytes, warning) = indexer.BuildRecordWithRawBytes(absPath, cancellationToken); + + if (warning != null && !options.Json && !options.Quiet) { PauseUpdateSpinnerForConsoleWrite(); - ConsoleUi.PrintWarning($"{relPath}: {message}"); + ConsoleUi.PrintWarning(warning); ResumeUpdateSpinnerAfterConsoleWrite(); } - if (!writer.HasFileAtPath(dbPath)) + var existingId = writer.GetUnchangedFileId( + record.Path, + record.Modified, + record.Checksum, + size: record.Size, + lines: record.Lines, + language: record.Lang, + generated: record.Generated, + allowReuse: symbolKindFilterMatchesPrior + && record.Lang is not ("javascript" or "typescript") + && (record.Lang != "csharp" || csharpSymbolNameContractMatchesCurrent) + && (record.Lang != "csharp" || !csharpWorkspace.HasStaticInterfaceContracts) + && (record.Lang != "sql" || sqlGraphContractMatchesCurrent)); + if (existingId != null) { + using var purgeTxn = writer.BeginTransaction(); + var purged = writer.PurgeStaleFilesSharingChecksum(projectRoot, record.Path, record.Checksum) + + (projectRootWritten + ? writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, record.Path) + : 0); + if (purged > 0) + { + DemoteReadinessOnce(); + WriteProjectRootOnce(); + purgeTxn.Commit(); + removed += purged; + ftsMutated = true; + } skipped++; + if (options.Verbose && !options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.WriteLine(purged > 0 + ? $" [SKIP] {relPath} (unchanged; purged {purged:N0} stale renamed path(s))" + : $" [SKIP] {relPath} (unchanged)"); + ResumeUpdateSpinnerAfterConsoleWrite(); + } continue; } DemoteReadinessOnce(); - using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(dbPath)) - { - WriteProjectRootOnce(); - deleteTxn.Commit(); - removed++; - ftsMutated = true; - } - else - { - skipped++; - } - continue; - } - - var statReusableLanguage = TryDetectStatReusableLanguage(absPath); - var statMatchedId = TryGetUnchangedFileIdFromStat( - writer, - projectRoot, - absPath, - statReusableLanguage, - allowReuse: symbolKindFilterMatchesPrior - && statReusableLanguage is not ("javascript" or "typescript") - && (statReusableLanguage != "csharp" || csharpSymbolNameContractMatchesCurrent) - && (statReusableLanguage != "csharp" || !csharpWorkspace.HasStaticInterfaceContracts) - && (statReusableLanguage != "sql" || sqlGraphContractMatchesCurrent)); - if (statMatchedId != null) - { - skipped++; - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine($" [SKIP] {relPath} (unchanged)"); - ResumeUpdateSpinnerAfterConsoleWrite(); - } - continue; - } - - var (record, content, rawBytes, warning) = indexer.BuildRecordWithRawBytes(absPath, cancellationToken); + writer.MarkBatchInProgress(); + fileBatchMarked = true; + using var txn = writer.BeginTransaction(); + writer.PurgeStaleFilesSharingChecksum(projectRoot, record.Path, record.Checksum); + if (projectRootWritten) + writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, record.Path); + WriteProjectRootOnce(); + var fileId = writer.UpsertFile(record); + currentUpdatePath = FormatIndexPhasePath(relPath, "chunking"); + var chunks = ChunkSplitter.Split(fileId, content); + writer.InsertChunks(chunks); + currentUpdatePath = FormatIndexPhasePath(relPath, "symbols"); + var symbols = ExtractSymbolsWithStallTimeout( + fileId, + record.Lang, + content, + absPath, + Path.GetFullPath(options.ProjectPath!), + currentUpdatePath, + cancellationToken); + SymbolExtractor.ApplyFamilyScope(symbols, indexer.GetFamilyScopeKey(absPath, record.Lang)); + var fileContext = new FileContext(projectRoot, record.Path, absPath, record.Lang); + postExtractionHooks.OnSymbolsExtracted(fileContext, symbols); + symbolsDroppedByKindFilter += options.SymbolKindFilter.Apply(symbols); + FileIndexer.ValidateSymbolLineRanges(record, symbols); + writer.InsertSymbols(symbols); + currentUpdatePath = FormatIndexPhasePath(relPath, "references"); + var references = ReferenceExtractor.Extract( + fileId, + record.Lang, + content, + symbols, + record.Path, + record.Lang == "csharp" ? csharpWorkspace.Symbols : null, + cancellationToken); + postExtractionHooks.OnReferencesExtracted(fileContext, references); + writer.InsertReferences(references); + // Validate content for encoding issues / エンコーディング問題を検証 + currentUpdatePath = FormatIndexPhasePath(relPath, "validating"); + var issues = FileIndexer.ValidateContent(record.Path, rawBytes, content); + writer.InsertIssues(fileId, issues); + currentUpdatePath = FormatIndexPhasePath(relPath, "committing"); + writer.ClearBatchInProgress(); + txn.Commit(); - if (warning != null && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - ConsoleUi.PrintWarning(warning); - ResumeUpdateSpinnerAfterConsoleWrite(); + updated++; + ftsMutated = true; + ThrowIfUpdateCancelled(); + WriteUpdateVerboseStatus($" [OK ] {relPath} ({chunks.Count} chunks, {symbols.Count} symbols, {references.Count} refs)"); } - - var existingId = writer.GetUnchangedFileId( - record.Path, - record.Modified, - record.Checksum, - size: record.Size, - lines: record.Lines, - language: record.Lang, - generated: record.Generated, - allowReuse: symbolKindFilterMatchesPrior - && record.Lang is not ("javascript" or "typescript") - && (record.Lang != "csharp" || csharpSymbolNameContractMatchesCurrent) - && (record.Lang != "csharp" || !csharpWorkspace.HasStaticInterfaceContracts) - && (record.Lang != "sql" || sqlGraphContractMatchesCurrent)); - if (existingId != null) + catch (IndexExtractionStalledException) { - using var purgeTxn = writer.BeginTransaction(); - var purged = writer.PurgeStaleFilesSharingChecksum(projectRoot, record.Path, record.Checksum) - + (projectRootWritten - ? writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, record.Path) - : 0); - if (purged > 0) - { - DemoteReadinessOnce(); - WriteProjectRootOnce(); - purgeTxn.Commit(); - removed += purged; - ftsMutated = true; - } - skipped++; - if (options.Verbose && !options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.WriteLine(purged > 0 - ? $" [SKIP] {relPath} (unchanged; purged {purged:N0} stale renamed path(s))" - : $" [SKIP] {relPath} (unchanged)"); - ResumeUpdateSpinnerAfterConsoleWrite(); - } - continue; + throw; } - - DemoteReadinessOnce(); - writer.MarkBatchInProgress(); - fileBatchMarked = true; - using var txn = writer.BeginTransaction(); - writer.PurgeStaleFilesSharingChecksum(projectRoot, record.Path, record.Checksum); - if (projectRootWritten) - writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, record.Path); - WriteProjectRootOnce(); - var fileId = writer.UpsertFile(record); - currentUpdatePath = FormatIndexPhasePath(relPath, "chunking"); - var chunks = ChunkSplitter.Split(fileId, content); - writer.InsertChunks(chunks); - currentUpdatePath = FormatIndexPhasePath(relPath, "symbols"); - var symbols = ExtractSymbolsWithStallTimeout( - fileId, - record.Lang, - content, - absPath, - Path.GetFullPath(options.ProjectPath!), - currentUpdatePath, - cancellationToken); - SymbolExtractor.ApplyFamilyScope(symbols, indexer.GetFamilyScopeKey(absPath, record.Lang)); - var fileContext = new FileContext(projectRoot, record.Path, absPath, record.Lang); - postExtractionHooks.OnSymbolsExtracted(fileContext, symbols); - symbolsDroppedByKindFilter += options.SymbolKindFilter.Apply(symbols); - FileIndexer.ValidateSymbolLineRanges(record, symbols); - writer.InsertSymbols(symbols); - currentUpdatePath = FormatIndexPhasePath(relPath, "references"); - var references = ReferenceExtractor.Extract( - fileId, - record.Lang, - content, - symbols, - record.Path, - record.Lang == "csharp" ? csharpWorkspace.Symbols : null, - cancellationToken); - postExtractionHooks.OnReferencesExtracted(fileContext, references); - writer.InsertReferences(references); - // Validate content for encoding issues / エンコーディング問題を検証 - currentUpdatePath = FormatIndexPhasePath(relPath, "validating"); - var issues = FileIndexer.ValidateContent(record.Path, rawBytes, content); - writer.InsertIssues(fileId, issues); - currentUpdatePath = FormatIndexPhasePath(relPath, "committing"); - writer.ClearBatchInProgress(); - txn.Commit(); - - updated++; - ftsMutated = true; - ThrowIfUpdateCancelled(); - WriteUpdateVerboseStatus($" [OK ] {relPath} ({chunks.Count} chunks, {symbols.Count} symbols, {references.Count} refs)"); - } - catch (IndexExtractionStalledException) - { - throw; - } - catch (Exception ex) - { - if (ex is FileIndexer.BinaryFileSkippedException) + catch (Exception ex) { - warnings++; - warningList.Add(new CliJsonMessage(relPath, ex.Message)); - if (!options.Json && !options.Quiet) + if (ex is FileIndexer.BinaryFileSkippedException) { - PauseUpdateSpinnerForConsoleWrite(); - ConsoleUi.PrintWarning(ex.Message); - ResumeUpdateSpinnerAfterConsoleWrite(); - } + warnings++; + warningList.Add(new CliJsonMessage(relPath, ex.Message)); + if (!options.Json && !options.Quiet) + { + PauseUpdateSpinnerForConsoleWrite(); + ConsoleUi.PrintWarning(ex.Message); + ResumeUpdateSpinnerAfterConsoleWrite(); + } - if (writer.HasFileAtPath(dbPath)) - { - DemoteReadinessOnce(); - using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(dbPath)) + if (writer.HasFileAtPath(dbPath)) { - WriteProjectRootOnce(); - deleteTxn.Commit(); - removed++; - ftsMutated = true; + DemoteReadinessOnce(); + using var deleteTxn = writer.BeginTransaction(); + if (writer.DeleteFileByPath(dbPath)) + { + WriteProjectRootOnce(); + deleteTxn.Commit(); + removed++; + ftsMutated = true; + } } + else + { + skipped++; + } + continue; } - else - { - skipped++; - } - continue; - } - if (ex is FileIndexer.FileTooLargeSkippedException fileTooLarge) - { - if (fileBatchMarked) - writer.ClearBatchInProgress(); + if (ex is FileIndexer.FileTooLargeSkippedException fileTooLarge) + { + if (fileBatchMarked) + writer.ClearBatchInProgress(); - DemoteReadinessOnce(); - writer.MarkBatchInProgress(); - using var txn = writer.BeginTransaction(); - var skippedRecord = indexer.BuildSkippedFileRecord(absPath); - writer.PurgeStaleFilesSharingChecksum(projectRoot, skippedRecord.Path, skippedRecord.Checksum); - if (projectRootWritten) - writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, skippedRecord.Path); - WriteProjectRootOnce(); - var fileId = writer.UpsertFile(skippedRecord); - writer.InsertChunks([]); - writer.InsertSymbols([]); - writer.InsertReferences([]); - writer.InsertIssues(fileId, - [ - new FileIssue + DemoteReadinessOnce(); + writer.MarkBatchInProgress(); + using var txn = writer.BeginTransaction(); + var skippedRecord = indexer.BuildSkippedFileRecord(absPath); + writer.PurgeStaleFilesSharingChecksum(projectRoot, skippedRecord.Path, skippedRecord.Checksum); + if (projectRootWritten) + writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, skippedRecord.Path); + WriteProjectRootOnce(); + var fileId = writer.UpsertFile(skippedRecord); + writer.InsertChunks([]); + writer.InsertSymbols([]); + writer.InsertReferences([]); + writer.InsertIssues(fileId, + [ + new FileIssue { Path = fileTooLarge.RelativePath, Kind = "file_too_large", @@ -875,65 +875,65 @@ void ThrowIfUpdateCancelled() Message = fileTooLarge.Message, }, ]); - writer.ClearBatchInProgress(); - txn.Commit(); - - updated++; - ftsMutated = true; - continue; - } - - if (ex is FileNotFoundException or DirectoryNotFoundException) - { - if (fileBatchMarked) writer.ClearBatchInProgress(); + txn.Commit(); - var message = $"{relPath}: skipped because it was deleted during indexing."; - warnings++; - warningList.Add(new CliJsonMessage(relPath, message)); - if (!options.Json && !options.Quiet) - { - PauseUpdateSpinnerForConsoleWrite(); - ConsoleUi.PrintWarning(message); - ResumeUpdateSpinnerAfterConsoleWrite(); + updated++; + ftsMutated = true; + continue; } - if (writer.HasFileAtPath(dbPath)) + if (ex is FileNotFoundException or DirectoryNotFoundException) { - DemoteReadinessOnce(); - using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(dbPath)) + if (fileBatchMarked) + writer.ClearBatchInProgress(); + + var message = $"{relPath}: skipped because it was deleted during indexing."; + warnings++; + warningList.Add(new CliJsonMessage(relPath, message)); + if (!options.Json && !options.Quiet) { - WriteProjectRootOnce(); - deleteTxn.Commit(); - removed++; - ftsMutated = true; + PauseUpdateSpinnerForConsoleWrite(); + ConsoleUi.PrintWarning(message); + ResumeUpdateSpinnerAfterConsoleWrite(); } + + if (writer.HasFileAtPath(dbPath)) + { + DemoteReadinessOnce(); + using var deleteTxn = writer.BeginTransaction(); + if (writer.DeleteFileByPath(dbPath)) + { + WriteProjectRootOnce(); + deleteTxn.Commit(); + removed++; + ftsMutated = true; + } + } + else + { + skipped++; + } + continue; } - else - { - skipped++; - } - continue; - } - DemoteReadinessOnce(); - if (fileBatchMarked) - writer.ClearBatchInProgress(); - GlobalToolLog.Error($"index_update_file_failed path={CollapseLineBreaks(relPath)}\n{GlobalToolLog.FormatExceptionChain(ex)}"); + DemoteReadinessOnce(); + if (fileBatchMarked) + writer.ClearBatchInProgress(); + GlobalToolLog.Error($"index_update_file_failed path={CollapseLineBreaks(relPath)}\n{GlobalToolLog.FormatExceptionChain(ex)}"); - errors++; - var errorMessage = FormatIndexFileException(ex); - errorList.Add(new CliJsonMessage(relPath, errorMessage)); - if (!options.Json) - { - PauseUpdateSpinnerForConsoleWrite(); - Console.Error.WriteLine(FormatPerFileErrorLine("ERR ", relPath, ex, errorMessage)); - ResumeUpdateSpinnerAfterConsoleWrite(); + errors++; + var errorMessage = FormatIndexFileException(ex); + errorList.Add(new CliJsonMessage(relPath, errorMessage)); + if (!options.Json) + { + PauseUpdateSpinnerForConsoleWrite(); + Console.Error.WriteLine(FormatPerFileErrorLine("ERR ", relPath, ex, errorMessage)); + ResumeUpdateSpinnerAfterConsoleWrite(); + } } } } - } finally { StopJsonPhaseHeartbeat(updateHeartbeat); diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index c08796f17d..af87acaaec 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -191,65 +191,65 @@ internal static int Run(string[] indexArgs, JsonSerializerOptions jsonOptions, C CommandErrorCodes.DbNotWritable); } - // Capture prior readiness BEFORE we clear it. Update mode (--commits / --files) only - // touches a subset of files, so trust bits the DB did NOT previously carry must not - // be fabricated after a partial pass. But bits the DB DID carry should survive — - // independently, not as a single all-or-nothing gate. Codex #86 review flagged that - // gating all three bits on `user_version == CurrentSchemaVersion` regressed pre-#86 - // DBs (user_version=3): a `--files` refresh on such a DB would silently drop Graph/ - // Issues trust too, breaking references/callers/callees/impact for the whole repo. - // update モードは元々立っていた readiness bit のみを個別に復元する。pre-#86 DB - // (user_version=3) でも Graph/Issues を巻き込んで落とさないように、単一フラグではなく - // 事前 bit をそのまま保持する。Codex #86 第 2 pass レビュー対応。 - var priorReadiness = db.GetUserVersion(); - // Also snapshot the stored fold-key version BEFORE ClearReadyFlags wipes trust. When - // a future `NameFold.Version` bump lands, a partial update must NOT restamp - // FoldReady on a DB whose untouched rows still carry the old-version fold keys — we - // can't re-fold those rows without re-reading them, so the only safe state is to leave - // fold degraded until `--rebuild`. Snapshot both version and runtime fingerprint so - // partial update does not restamp FoldReady across either algorithm drift or runtime - // casing-table drift. Issue #97. - // fold metadata を事前 snapshot する。version だけでなく fingerprint のズレでも - // partial update で FoldReady を restamp しない。 - var priorFoldVersion = db.GetMetaString("fold_key_version"); - var priorFoldFingerprint = db.GetMetaString("fold_key_fingerprint"); - var priorSymbolExtractorVersionsMatchCurrent = new DbWriter(db).SymbolExtractorVersionsMatchCurrent(); - var priorCSharpSymbolNameContractVersion = db.GetMetaString(DbContext.CSharpSymbolNameContractVersionMetaKey); - var priorMetadataTargetCsharp = db.GetMetaString(DbContext.GetMetadataTargetVersionMetaKey("csharp")); - var priorSqlGraphContractVersion = db.GetMetaString(DbContext.SqlGraphContractVersionMetaKey); - var priorHotspotFamilyVersions = GetHotspotFamilyMetaSnapshot(db, DbContext.GetHotspotFamilyVersionMetaKey); - var priorHotspotFamilyMarkerFingerprints = GetHotspotFamilyMetaSnapshot(db, DbContext.GetHotspotFamilyMarkerFingerprintMetaKey); - var priorIndexedProjectRoot = db.GetMetaString(DbContext.IndexedProjectRootMetaKey); - var priorSymbolKindFilterSignature = db.GetMetaString(SymbolKindFilterMetaKey); - // Captured BEFORE `--rebuild` drops the DB so an incremental run can warn the user when - // the worktree's HEAD has moved since the previously indexed snapshot. The same value - // is read at `status` time (without `--check`) to surface a worktree branch / HEAD - // switch via `worktree_head_changed`. Issues #1508 and #1512. - // `--rebuild` が DB を消す前に取り出す。incremental 経路で HEAD 差分を検知し、`status` - // (no `--check`) でも worktree の HEAD 切替検出に利用する。 - var priorIndexedHeadCommit = db.GetMetaString(DbContext.IndexedHeadCommitMetaKey); - var currentHeadCommit = GitHelper.TryGetHeadCommit(options.ProjectPath); - - // Don't demote readiness yet. A transient usage error in update-mode preflight - // (bad --commits hash, git unavailable, etc.) would permanently downgrade a healthy - // DB even though no data was touched. Clearing happens just before the first - // destructive / schema-changing operation, inside the mode-specific runner. - // まだ clear しない。update モードの preflight が失敗しただけで healthy な DB を - // 縮退状態に落とさないよう、clear は実際に書き込み直前で行う。 - - db.InitializeSchema(); - AddToGitExclude(options.ProjectPath, dbPath); - - var writer = new DbWriter(db); - var indexer = new FileIndexer(options.ProjectPath, ignoreCase, ignoreRuleRoot, options.MaxFileSizeBytes, directoryIgnoreCaseProbe: null, symlinkPolicy: options.SymlinkPolicy); - var currentHotspotFamilyMarkerFingerprints = GetHotspotFamilyMarkerFingerprints(indexer); - var projectRoot = Path.GetFullPath(options.ProjectPath!); - - initialExitCode = isUpdateMode - ? RunUpdateMode(writer, indexer, projectRoot, resolvedDbPath, options, stopwatch, runStartedAtUtc, spinnerFrames, jsonOptions, priorReadiness, priorFoldVersion, priorFoldFingerprint, priorSymbolExtractorVersionsMatchCurrent, priorCSharpSymbolNameContractVersion, priorMetadataTargetCsharp, priorSqlGraphContractVersion, priorHotspotFamilyVersions, priorHotspotFamilyMarkerFingerprints, currentHotspotFamilyMarkerFingerprints, priorIndexedProjectRoot, priorIndexedHeadCommit, currentHeadCommit, priorSymbolKindFilterSignature, initialCwd, indexCancellation.Token) - : RunFullScan(writer, indexer, projectRoot, resolvedDbPath, options, stopwatch, runStartedAtUtc, spinnerFrames, jsonOptions, priorFoldVersion, priorFoldFingerprint, priorSymbolExtractorVersionsMatchCurrent, priorCSharpSymbolNameContractVersion, priorMetadataTargetCsharp, priorSqlGraphContractVersion, priorHotspotFamilyVersions, priorHotspotFamilyMarkerFingerprints, currentHotspotFamilyMarkerFingerprints, priorIndexedProjectRoot, priorIndexedHeadCommit, currentHeadCommit, priorSymbolKindFilterSignature, initialCwd, showNextSteps: !databaseExistedBeforeIndex, indexCancellation.Token); - if (initialExitCode == CommandExitCodes.Success) - db.RunPlannerStatisticsMaintenance(forceAnalyze: !databaseExistedBeforeIndex); + // Capture prior readiness BEFORE we clear it. Update mode (--commits / --files) only + // touches a subset of files, so trust bits the DB did NOT previously carry must not + // be fabricated after a partial pass. But bits the DB DID carry should survive — + // independently, not as a single all-or-nothing gate. Codex #86 review flagged that + // gating all three bits on `user_version == CurrentSchemaVersion` regressed pre-#86 + // DBs (user_version=3): a `--files` refresh on such a DB would silently drop Graph/ + // Issues trust too, breaking references/callers/callees/impact for the whole repo. + // update モードは元々立っていた readiness bit のみを個別に復元する。pre-#86 DB + // (user_version=3) でも Graph/Issues を巻き込んで落とさないように、単一フラグではなく + // 事前 bit をそのまま保持する。Codex #86 第 2 pass レビュー対応。 + var priorReadiness = db.GetUserVersion(); + // Also snapshot the stored fold-key version BEFORE ClearReadyFlags wipes trust. When + // a future `NameFold.Version` bump lands, a partial update must NOT restamp + // FoldReady on a DB whose untouched rows still carry the old-version fold keys — we + // can't re-fold those rows without re-reading them, so the only safe state is to leave + // fold degraded until `--rebuild`. Snapshot both version and runtime fingerprint so + // partial update does not restamp FoldReady across either algorithm drift or runtime + // casing-table drift. Issue #97. + // fold metadata を事前 snapshot する。version だけでなく fingerprint のズレでも + // partial update で FoldReady を restamp しない。 + var priorFoldVersion = db.GetMetaString("fold_key_version"); + var priorFoldFingerprint = db.GetMetaString("fold_key_fingerprint"); + var priorSymbolExtractorVersionsMatchCurrent = new DbWriter(db).SymbolExtractorVersionsMatchCurrent(); + var priorCSharpSymbolNameContractVersion = db.GetMetaString(DbContext.CSharpSymbolNameContractVersionMetaKey); + var priorMetadataTargetCsharp = db.GetMetaString(DbContext.GetMetadataTargetVersionMetaKey("csharp")); + var priorSqlGraphContractVersion = db.GetMetaString(DbContext.SqlGraphContractVersionMetaKey); + var priorHotspotFamilyVersions = GetHotspotFamilyMetaSnapshot(db, DbContext.GetHotspotFamilyVersionMetaKey); + var priorHotspotFamilyMarkerFingerprints = GetHotspotFamilyMetaSnapshot(db, DbContext.GetHotspotFamilyMarkerFingerprintMetaKey); + var priorIndexedProjectRoot = db.GetMetaString(DbContext.IndexedProjectRootMetaKey); + var priorSymbolKindFilterSignature = db.GetMetaString(SymbolKindFilterMetaKey); + // Captured BEFORE `--rebuild` drops the DB so an incremental run can warn the user when + // the worktree's HEAD has moved since the previously indexed snapshot. The same value + // is read at `status` time (without `--check`) to surface a worktree branch / HEAD + // switch via `worktree_head_changed`. Issues #1508 and #1512. + // `--rebuild` が DB を消す前に取り出す。incremental 経路で HEAD 差分を検知し、`status` + // (no `--check`) でも worktree の HEAD 切替検出に利用する。 + var priorIndexedHeadCommit = db.GetMetaString(DbContext.IndexedHeadCommitMetaKey); + var currentHeadCommit = GitHelper.TryGetHeadCommit(options.ProjectPath); + + // Don't demote readiness yet. A transient usage error in update-mode preflight + // (bad --commits hash, git unavailable, etc.) would permanently downgrade a healthy + // DB even though no data was touched. Clearing happens just before the first + // destructive / schema-changing operation, inside the mode-specific runner. + // まだ clear しない。update モードの preflight が失敗しただけで healthy な DB を + // 縮退状態に落とさないよう、clear は実際に書き込み直前で行う。 + + db.InitializeSchema(); + AddToGitExclude(options.ProjectPath, dbPath); + + var writer = new DbWriter(db); + var indexer = new FileIndexer(options.ProjectPath, ignoreCase, ignoreRuleRoot, options.MaxFileSizeBytes, directoryIgnoreCaseProbe: null, symlinkPolicy: options.SymlinkPolicy); + var currentHotspotFamilyMarkerFingerprints = GetHotspotFamilyMarkerFingerprints(indexer); + var projectRoot = Path.GetFullPath(options.ProjectPath!); + + initialExitCode = isUpdateMode + ? RunUpdateMode(writer, indexer, projectRoot, resolvedDbPath, options, stopwatch, runStartedAtUtc, spinnerFrames, jsonOptions, priorReadiness, priorFoldVersion, priorFoldFingerprint, priorSymbolExtractorVersionsMatchCurrent, priorCSharpSymbolNameContractVersion, priorMetadataTargetCsharp, priorSqlGraphContractVersion, priorHotspotFamilyVersions, priorHotspotFamilyMarkerFingerprints, currentHotspotFamilyMarkerFingerprints, priorIndexedProjectRoot, priorIndexedHeadCommit, currentHeadCommit, priorSymbolKindFilterSignature, initialCwd, indexCancellation.Token) + : RunFullScan(writer, indexer, projectRoot, resolvedDbPath, options, stopwatch, runStartedAtUtc, spinnerFrames, jsonOptions, priorFoldVersion, priorFoldFingerprint, priorSymbolExtractorVersionsMatchCurrent, priorCSharpSymbolNameContractVersion, priorMetadataTargetCsharp, priorSqlGraphContractVersion, priorHotspotFamilyVersions, priorHotspotFamilyMarkerFingerprints, currentHotspotFamilyMarkerFingerprints, priorIndexedProjectRoot, priorIndexedHeadCommit, currentHeadCommit, priorSymbolKindFilterSignature, initialCwd, showNextSteps: !databaseExistedBeforeIndex, indexCancellation.Token); + if (initialExitCode == CommandExitCodes.Success) + db.RunPlannerStatisticsMaintenance(forceAnalyze: !databaseExistedBeforeIndex); } } catch (IndexInterruptedException ex) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index b939afffa8..f2f8f7046c 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -3552,12 +3552,12 @@ private static List GetWorkspaceFileDependencies(DbReader } foreach (var sourceDb in memberDbs) - foreach (var targetDb in memberDbs) - { - if (string.Equals(sourceDb, targetDb, StringComparison.Ordinal)) - continue; - results.AddRange(GetCrossDatabaseFileDependencies(sourceDb, targetDb, options, reverse)); - } + foreach (var targetDb in memberDbs) + { + if (string.Equals(sourceDb, targetDb, StringComparison.Ordinal)) + continue; + results.AddRange(GetCrossDatabaseFileDependencies(sourceDb, targetDb, options, reverse)); + } return results .OrderByDescending(result => result.ReferenceCount) diff --git a/src/CodeIndex/Database/DbContext.cs b/src/CodeIndex/Database/DbContext.cs index 23dbdce436..6a7395e6e0 100644 --- a/src/CodeIndex/Database/DbContext.cs +++ b/src/CodeIndex/Database/DbContext.cs @@ -1456,8 +1456,8 @@ public void InitializeSchema() _activeMigrationTransaction = transaction; try { - // Files table / ファイルテーブル - Execute(@" + // Files table / ファイルテーブル + Execute(@" CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT NOT NULL UNIQUE, @@ -1470,8 +1470,8 @@ CREATE TABLE IF NOT EXISTS files ( indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP )"); - // Chunks table / チャンクテーブル - Execute(@" + // Chunks table / チャンクテーブル + Execute(@" CREATE TABLE IF NOT EXISTS chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, @@ -1482,8 +1482,8 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, UNIQUE(file_id, chunk_index) )"); - // Shared reference-line context table / 参照行コンテキスト共有テーブル - Execute(@" + // Shared reference-line context table / 参照行コンテキスト共有テーブル + Execute(@" CREATE TABLE IF NOT EXISTS reference_lines ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, @@ -1492,11 +1492,11 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, UNIQUE(file_id, line, context) )"); - var symbolKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.SymbolKinds); - var referenceKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.ReferenceKinds); + var symbolKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.SymbolKinds); + var referenceKindCheck = SymbolKindCatalog.ToSqlCheckInList(SymbolKindCatalog.ReferenceKinds); - // Symbols table / シンボルテーブル - Execute(@" + // Symbols table / シンボルテーブル + Execute(@" CREATE TABLE IF NOT EXISTS symbols ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, @@ -1519,8 +1519,8 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + sym is_metadata_target INTEGER )"); - // Indexed references table / 参照インデックステーブル - Execute(@" + // Indexed references table / 参照インデックステーブル + Execute(@" CREATE TABLE IF NOT EXISTS symbol_references ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, @@ -1534,8 +1534,8 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + sym container_name TEXT )"); - // File validation issues table / ファイル検証問題テーブル - Execute(@" + // File validation issues table / ファイル検証問題テーブル + Execute(@" CREATE TABLE IF NOT EXISTS file_issues ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, @@ -1544,137 +1544,137 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, message TEXT NOT NULL )"); - // Key-value metadata: fold algorithm version, future per-subsystem schema markers - // that don't fit in PRAGMA user_version's 3-bit readiness bitmap. See - // NameFold.Version and DbReader fold-ready gate. - // メタデータ用 key-value: fold のアルゴリズム版数など、user_version bitmap に収まらない情報。 - Execute(@" + // Key-value metadata: fold algorithm version, future per-subsystem schema markers + // that don't fit in PRAGMA user_version's 3-bit readiness bitmap. See + // NameFold.Version and DbReader fold-ready gate. + // メタデータ用 key-value: fold のアルゴリズム版数など、user_version bitmap に収まらない情報。 + Execute(@" CREATE TABLE IF NOT EXISTS codeindex_meta ( key TEXT PRIMARY KEY NOT NULL, value TEXT )"); - NormalizeCodeIndexMetaKeys(); - - // Schema migrations for existing DBs / 既存DB向けスキーマ移行 - EnsureColumn("files", "checksum", "TEXT"); - EnsureColumn("files", "modified", "DATETIME"); - EnsureColumn("files", "generated", "INTEGER NOT NULL DEFAULT 0"); - EnsureColumn("files", "indexed_at", "DATETIME"); - EnsureColumn("symbols", "start_line", "INTEGER"); - EnsureColumn("symbols", "sub_kind", "TEXT"); - EnsureColumn("symbols", "start_column", "INTEGER"); - EnsureColumn("symbols", "end_line", "INTEGER"); - EnsureColumn("symbols", "body_start_line", "INTEGER"); - EnsureColumn("symbols", "body_end_line", "INTEGER"); - EnsureColumn("symbols", "signature", "TEXT"); - EnsureColumn("symbols", "container_kind", "TEXT"); - EnsureColumn("symbols", "container_name", "TEXT"); - EnsureColumn("symbols", "container_qualified_name", "TEXT"); - EnsureColumn("symbols", "family_key", "TEXT"); - EnsureColumn("symbols", "visibility", "TEXT"); - EnsureColumn("symbols", "return_type", "TEXT"); - EnsureColumn("symbols", "is_metadata_target", "INTEGER"); - var rebuildsSymbolReferences = !ColumnIsNotNull("symbol_references", "file_id"); - EnsureColumn( - "symbol_references", - "reference_line_id", - rebuildsSymbolReferences ? "INTEGER" : "INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL"); - // #86: Unicode-aware folded name columns for `--exact` name matching across all - // `--exact` command variants. Populated by the writer via NameFold.Fold; NULL on - // legacy rows until a full reindex, in which case the reader falls back to the - // COLLATE NOCASE path (correct for ASCII, misses non-ASCII casing — #86 fix). - // #86: --exact 用の Unicode 折り畳み列。レガシー行は NULL のまま、再 index で埋まる。 - EnsureColumn("symbols", "name_folded", "TEXT"); - EnsureColumn("symbol_references", "symbol_name_folded", "TEXT"); - EnsureColumn("symbol_references", "container_name_folded", "TEXT"); - EnsureColumn("symbol_references", "is_self_reference", "INTEGER NOT NULL DEFAULT 0"); - EnsureColumn("symbol_references", "is_mutual_recursion", "INTEGER NOT NULL DEFAULT 0"); - EnforceRequiredFileIdConstraints(); - EnforceReferenceLineSetNullConstraint(); - EnsureReferenceLinesContextKey(); - EnsureKindCheckConstraintsCurrent(); - - // Indexes / インデックス - Execute("CREATE INDEX IF NOT EXISTS idx_files_lang ON files(lang)"); - Execute("CREATE INDEX IF NOT EXISTS idx_files_modified ON files(modified)"); - Execute("CREATE INDEX IF NOT EXISTS idx_files_generated ON files(generated)"); - // idx_files_path is not needed: the UNIQUE constraint on path already creates an implicit index - // idx_files_path は不要: path の UNIQUE 制約が暗黙的にインデックスを作成済み - Execute("CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)"); - // Case-insensitive exact-match index for `symbols --exact` (and MCP `symbols` exact=true). - // Without this, `name = @q COLLATE NOCASE` falls back to a full symbols scan per query name, - // which on multi-name exact lookups becomes O(names × symbols). - // `symbols --exact` 用の大文字小文字無視 index。無いと multi-name exact でフルスキャンが N 回走る。 - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_nocase ON symbols(name COLLATE NOCASE)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_start ON symbols(start_line)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name ON symbol_references(symbol_name)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_file ON symbol_references(file_id)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container ON symbol_references(container_name)"); - // Compound indexes for common query patterns / よくあるクエリパターン用の複合インデックス - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_file_kind ON symbols(file_id, kind)"); - Execute("CREATE INDEX IF NOT EXISTS idx_files_lang_modified ON files(lang, modified)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_kind ON symbol_references(container_name, reference_kind)"); - // Indexes for new query patterns: --kind filter, visibility ranking, hotspot/unused analysis - // 新しいクエリパターン用: --kind フィルタ、可視性ランキング、ホットスポット/未使用分析 - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_visibility ON symbols(visibility)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_kind ON symbol_references(symbol_name, reference_kind)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_file ON symbol_references(symbol_name, file_id)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_mutual_folded ON symbol_references(container_name_folded, symbol_name_folded, reference_kind, is_self_reference)"); - Execute("CREATE INDEX IF NOT EXISTS idx_reference_lines_file_line ON reference_lines(file_id, line)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_reference_line ON symbol_references(reference_line_id)"); - // Case-insensitive exact-match indexes for `references --exact` / `callers --exact` / `callees --exact` (#83). - // Mirror idx_symbols_name_nocase so `= @q COLLATE NOCASE` stays O(log n) per name across graph commands. - // `references / callers / callees --exact` 用の NOCASE index。idx_symbols_name_nocase と対になる。 - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_nocase ON symbol_references(symbol_name COLLATE NOCASE)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_nocase ON symbol_references(container_name COLLATE NOCASE)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_nocase_kind ON symbol_references(symbol_name COLLATE NOCASE, reference_kind)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_nocase_file ON symbol_references(symbol_name COLLATE NOCASE, file_id)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_nocase_kind ON symbol_references(container_name COLLATE NOCASE, reference_kind)"); - // #86: Indexes on the Unicode-folded columns. Used when FoldReadyFlag is set on the - // DB (= the write path filled every folded column). Legacy / partial DBs keep using - // the NOCASE indexes above. Both sets coexist so mixed-state DBs cannot regress. - // #86: 折り畳み列のインデックス。FoldReadyFlag が立っている DB でだけ使う。 - Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_folded ON symbols(name_folded)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_symbol_name_folded ON symbol_references(symbol_name_folded)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_name_folded ON symbol_references(container_name_folded)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_symbol_name_folded_kind ON symbol_references(symbol_name_folded, reference_kind)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_symbol_name_folded_file ON symbol_references(symbol_name_folded, file_id)"); - Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_name_folded_kind ON symbol_references(container_name_folded, reference_kind)"); - - // Full-text search / 全文検索 - Execute(@" + NormalizeCodeIndexMetaKeys(); + + // Schema migrations for existing DBs / 既存DB向けスキーマ移行 + EnsureColumn("files", "checksum", "TEXT"); + EnsureColumn("files", "modified", "DATETIME"); + EnsureColumn("files", "generated", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn("files", "indexed_at", "DATETIME"); + EnsureColumn("symbols", "start_line", "INTEGER"); + EnsureColumn("symbols", "sub_kind", "TEXT"); + EnsureColumn("symbols", "start_column", "INTEGER"); + EnsureColumn("symbols", "end_line", "INTEGER"); + EnsureColumn("symbols", "body_start_line", "INTEGER"); + EnsureColumn("symbols", "body_end_line", "INTEGER"); + EnsureColumn("symbols", "signature", "TEXT"); + EnsureColumn("symbols", "container_kind", "TEXT"); + EnsureColumn("symbols", "container_name", "TEXT"); + EnsureColumn("symbols", "container_qualified_name", "TEXT"); + EnsureColumn("symbols", "family_key", "TEXT"); + EnsureColumn("symbols", "visibility", "TEXT"); + EnsureColumn("symbols", "return_type", "TEXT"); + EnsureColumn("symbols", "is_metadata_target", "INTEGER"); + var rebuildsSymbolReferences = !ColumnIsNotNull("symbol_references", "file_id"); + EnsureColumn( + "symbol_references", + "reference_line_id", + rebuildsSymbolReferences ? "INTEGER" : "INTEGER REFERENCES reference_lines(id) ON DELETE SET NULL"); + // #86: Unicode-aware folded name columns for `--exact` name matching across all + // `--exact` command variants. Populated by the writer via NameFold.Fold; NULL on + // legacy rows until a full reindex, in which case the reader falls back to the + // COLLATE NOCASE path (correct for ASCII, misses non-ASCII casing — #86 fix). + // #86: --exact 用の Unicode 折り畳み列。レガシー行は NULL のまま、再 index で埋まる。 + EnsureColumn("symbols", "name_folded", "TEXT"); + EnsureColumn("symbol_references", "symbol_name_folded", "TEXT"); + EnsureColumn("symbol_references", "container_name_folded", "TEXT"); + EnsureColumn("symbol_references", "is_self_reference", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn("symbol_references", "is_mutual_recursion", "INTEGER NOT NULL DEFAULT 0"); + EnforceRequiredFileIdConstraints(); + EnforceReferenceLineSetNullConstraint(); + EnsureReferenceLinesContextKey(); + EnsureKindCheckConstraintsCurrent(); + + // Indexes / インデックス + Execute("CREATE INDEX IF NOT EXISTS idx_files_lang ON files(lang)"); + Execute("CREATE INDEX IF NOT EXISTS idx_files_modified ON files(modified)"); + Execute("CREATE INDEX IF NOT EXISTS idx_files_generated ON files(generated)"); + // idx_files_path is not needed: the UNIQUE constraint on path already creates an implicit index + // idx_files_path は不要: path の UNIQUE 制約が暗黙的にインデックスを作成済み + Execute("CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)"); + // Case-insensitive exact-match index for `symbols --exact` (and MCP `symbols` exact=true). + // Without this, `name = @q COLLATE NOCASE` falls back to a full symbols scan per query name, + // which on multi-name exact lookups becomes O(names × symbols). + // `symbols --exact` 用の大文字小文字無視 index。無いと multi-name exact でフルスキャンが N 回走る。 + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_nocase ON symbols(name COLLATE NOCASE)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_start ON symbols(start_line)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name ON symbol_references(symbol_name)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_file ON symbol_references(file_id)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container ON symbol_references(container_name)"); + // Compound indexes for common query patterns / よくあるクエリパターン用の複合インデックス + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_file_kind ON symbols(file_id, kind)"); + Execute("CREATE INDEX IF NOT EXISTS idx_files_lang_modified ON files(lang, modified)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_kind ON symbol_references(container_name, reference_kind)"); + // Indexes for new query patterns: --kind filter, visibility ranking, hotspot/unused analysis + // 新しいクエリパターン用: --kind フィルタ、可視性ランキング、ホットスポット/未使用分析 + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_visibility ON symbols(visibility)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_kind ON symbol_references(symbol_name, reference_kind)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_file ON symbol_references(symbol_name, file_id)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_mutual_folded ON symbol_references(container_name_folded, symbol_name_folded, reference_kind, is_self_reference)"); + Execute("CREATE INDEX IF NOT EXISTS idx_reference_lines_file_line ON reference_lines(file_id, line)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_reference_line ON symbol_references(reference_line_id)"); + // Case-insensitive exact-match indexes for `references --exact` / `callers --exact` / `callees --exact` (#83). + // Mirror idx_symbols_name_nocase so `= @q COLLATE NOCASE` stays O(log n) per name across graph commands. + // `references / callers / callees --exact` 用の NOCASE index。idx_symbols_name_nocase と対になる。 + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_nocase ON symbol_references(symbol_name COLLATE NOCASE)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_nocase ON symbol_references(container_name COLLATE NOCASE)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_nocase_kind ON symbol_references(symbol_name COLLATE NOCASE, reference_kind)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_name_nocase_file ON symbol_references(symbol_name COLLATE NOCASE, file_id)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_nocase_kind ON symbol_references(container_name COLLATE NOCASE, reference_kind)"); + // #86: Indexes on the Unicode-folded columns. Used when FoldReadyFlag is set on the + // DB (= the write path filled every folded column). Legacy / partial DBs keep using + // the NOCASE indexes above. Both sets coexist so mixed-state DBs cannot regress. + // #86: 折り畳み列のインデックス。FoldReadyFlag が立っている DB でだけ使う。 + Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_folded ON symbols(name_folded)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_symbol_name_folded ON symbol_references(symbol_name_folded)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_name_folded ON symbol_references(container_name_folded)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_symbol_name_folded_kind ON symbol_references(symbol_name_folded, reference_kind)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_symbol_name_folded_file ON symbol_references(symbol_name_folded, file_id)"); + Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_name_folded_kind ON symbol_references(container_name_folded, reference_kind)"); + + // Full-text search / 全文検索 + Execute(@" CREATE VIRTUAL TABLE IF NOT EXISTS fts_chunks USING fts5( content, content='chunks', content_rowid='id' )"); - if (_rebuildFtsAfterSchemaMigration) - { - Execute("INSERT INTO fts_chunks(fts_chunks) VALUES('rebuild')"); - _rebuildFtsAfterSchemaMigration = false; - } + if (_rebuildFtsAfterSchemaMigration) + { + Execute("INSERT INTO fts_chunks(fts_chunks) VALUES('rebuild')"); + _rebuildFtsAfterSchemaMigration = false; + } - // FTS5 content-synced triggers — keep fts_chunks in sync with chunks table. - // Without these, CASCADE DELETEs on chunks leave orphan entries in fts_chunks. - // FTS5 content-synced トリガー — fts_chunksをchunksテーブルと同期する。 - // これがないとchunksのCASCADE DELETEでfts_chunksに孤立エントリが残る。 - Execute(@" + // FTS5 content-synced triggers — keep fts_chunks in sync with chunks table. + // Without these, CASCADE DELETEs on chunks leave orphan entries in fts_chunks. + // FTS5 content-synced トリガー — fts_chunksをchunksテーブルと同期する。 + // これがないとchunksのCASCADE DELETEでfts_chunksに孤立エントリが残る。 + Execute(@" CREATE TRIGGER IF NOT EXISTS fts_chunks_ai AFTER INSERT ON chunks BEGIN INSERT INTO fts_chunks(rowid, content) VALUES (new.id, new.content); END"); - Execute(@" + Execute(@" CREATE TRIGGER IF NOT EXISTS fts_chunks_ad AFTER DELETE ON chunks BEGIN INSERT INTO fts_chunks(fts_chunks, rowid, content) VALUES('delete', old.id, old.content); END"); - Execute(@" + Execute(@" CREATE TRIGGER IF NOT EXISTS fts_chunks_au AFTER UPDATE ON chunks BEGIN INSERT INTO fts_chunks(fts_chunks, rowid, content) VALUES('delete', old.id, old.content); INSERT INTO fts_chunks(rowid, content) VALUES (new.id, new.content); END"); - transaction.Commit(); + transaction.Commit(); } finally { @@ -2356,30 +2356,30 @@ is_mutual_recursion INTEGER NOT NULL DEFAULT 0 yield return ("CREATE INDEX idx_symbol_refs_container_nocase_kind", () => Execute("CREATE INDEX IF NOT EXISTS idx_symbol_refs_container_nocase_kind ON symbol_references(container_name COLLATE NOCASE, reference_kind)")); - yield return ("EnsureColumn files.checksum", () => EnsureColumn("files", "checksum", "TEXT")); - yield return ("EnsureColumn files.modified", () => EnsureColumn("files", "modified", "DATETIME")); + yield return ("EnsureColumn files.checksum", () => EnsureColumn("files", "checksum", "TEXT")); + yield return ("EnsureColumn files.modified", () => EnsureColumn("files", "modified", "DATETIME")); yield return ("EnsureColumn files.indexed_at", () => EnsureColumn("files", "indexed_at", "DATETIME")); - yield return ("EnsureColumn symbols.start_line", () => EnsureColumn("symbols", "start_line", "INTEGER")); - yield return ("EnsureColumn symbols.end_line", () => EnsureColumn("symbols", "end_line", "INTEGER")); - yield return ("EnsureColumn symbols.body_start_line", () => EnsureColumn("symbols", "body_start_line", "INTEGER")); - yield return ("EnsureColumn symbols.body_end_line", () => EnsureColumn("symbols", "body_end_line", "INTEGER")); - yield return ("EnsureColumn symbols.signature", () => EnsureColumn("symbols", "signature", "TEXT")); - yield return ("EnsureColumn symbols.container_kind", () => EnsureColumn("symbols", "container_kind", "TEXT")); - yield return ("EnsureColumn symbols.container_name", () => EnsureColumn("symbols", "container_name", "TEXT")); + yield return ("EnsureColumn symbols.start_line", () => EnsureColumn("symbols", "start_line", "INTEGER")); + yield return ("EnsureColumn symbols.end_line", () => EnsureColumn("symbols", "end_line", "INTEGER")); + yield return ("EnsureColumn symbols.body_start_line", () => EnsureColumn("symbols", "body_start_line", "INTEGER")); + yield return ("EnsureColumn symbols.body_end_line", () => EnsureColumn("symbols", "body_end_line", "INTEGER")); + yield return ("EnsureColumn symbols.signature", () => EnsureColumn("symbols", "signature", "TEXT")); + yield return ("EnsureColumn symbols.container_kind", () => EnsureColumn("symbols", "container_kind", "TEXT")); + yield return ("EnsureColumn symbols.container_name", () => EnsureColumn("symbols", "container_name", "TEXT")); yield return ("EnsureColumn symbols.container_qualified_name", () => EnsureColumn("symbols", "container_qualified_name", "TEXT")); - yield return ("EnsureColumn symbols.family_key", () => EnsureColumn("symbols", "family_key", "TEXT")); - yield return ("EnsureColumn symbols.visibility", () => EnsureColumn("symbols", "visibility", "TEXT")); - yield return ("EnsureColumn symbols.return_type", () => EnsureColumn("symbols", "return_type", "TEXT")); - yield return ("EnsureColumn symbols.is_metadata_target", () => EnsureColumn("symbols", "is_metadata_target", "INTEGER")); + yield return ("EnsureColumn symbols.family_key", () => EnsureColumn("symbols", "family_key", "TEXT")); + yield return ("EnsureColumn symbols.visibility", () => EnsureColumn("symbols", "visibility", "TEXT")); + yield return ("EnsureColumn symbols.return_type", () => EnsureColumn("symbols", "return_type", "TEXT")); + yield return ("EnsureColumn symbols.is_metadata_target", () => EnsureColumn("symbols", "is_metadata_target", "INTEGER")); yield return ("CREATE INDEX idx_symbols_name_nocase", () => Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_nocase ON symbols(name COLLATE NOCASE)")); // #86: fold columns must be ensured BEFORE the folded indexes so CREATE INDEX does // not fail on legacy DBs where the column did not exist yet. // #86: folded 列を追加してから folded index を作らないと legacy DB でクラッシュする。 - yield return ("EnsureColumn symbols.name_folded", () => EnsureColumn("symbols", "name_folded", "TEXT")); - yield return ("EnsureColumn symbol_references.symbol_name_folded", () => EnsureColumn("symbol_references", "symbol_name_folded", "TEXT")); - yield return ("EnsureColumn symbol_references.container_name_folded", () => EnsureColumn("symbol_references", "container_name_folded", "TEXT")); + yield return ("EnsureColumn symbols.name_folded", () => EnsureColumn("symbols", "name_folded", "TEXT")); + yield return ("EnsureColumn symbol_references.symbol_name_folded", () => EnsureColumn("symbol_references", "symbol_name_folded", "TEXT")); + yield return ("EnsureColumn symbol_references.container_name_folded", () => EnsureColumn("symbol_references", "container_name_folded", "TEXT")); yield return ("CREATE INDEX idx_symbols_name_folded", () => Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_folded ON symbols(name_folded)")); yield return ("CREATE INDEX idx_symbol_refs_symbol_name_folded", diff --git a/src/CodeIndex/Database/DbWriter.cs b/src/CodeIndex/Database/DbWriter.cs index a12d5c2913..d09d9573f9 100644 --- a/src/CodeIndex/Database/DbWriter.cs +++ b/src/CodeIndex/Database/DbWriter.cs @@ -1856,8 +1856,8 @@ public bool OptimizeFtsIfIncrementalWriteThresholdReached(int threshold = Defaul // end-of-run commit. Fold is only stamped after a full scan because a partial update // leaves legacy rows without folded values. // CLI / MCP 共に full-scan で graph + fold を立てる。fold は部分更新では立てない。 - public void MarkGraphReady() => SetReadyBit(DbContext.GraphReadyFlag); - public void MarkIssuesReady() => SetReadyBit(DbContext.IssuesReadyFlag); + public void MarkGraphReady() => SetReadyBit(DbContext.GraphReadyFlag); + public void MarkIssuesReady() => SetReadyBit(DbContext.IssuesReadyFlag); /// /// Stamp FoldReadyFlag AND write the current plus the @@ -3063,7 +3063,7 @@ private void SetMetaCore(string key, string? value) cmd.Parameters.AddWithValue("@key", key); return cmd.ExecuteScalar() as string; } - public void ClearReadyFlags() => Execute("PRAGMA user_version = 0"); + public void ClearReadyFlags() => Execute("PRAGMA user_version = 0"); public bool HasMetaTable() => TableExists("codeindex_meta"); diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs index 2e98a659cc..ae2c1b757e 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs @@ -896,15 +896,15 @@ private static int SkipCSharpTriviaBackward(string text, int cursor) return cursor; } - internal static bool IsCSharpPatternHeadCallSite(string[] preparedLines, int lineIndex, string preparedLine, int nameIndex) - { - var whenOffset = FindTopLevelCSharpWhenKeywordOffset(preparedLine); - if (whenOffset >= 0 && nameIndex > whenOffset) - return false; - - var cursor = nameIndex; - if (IsCSharpConstantPatternAnchor(preparedLine, ref cursor)) - return true; + internal static bool IsCSharpPatternHeadCallSite(string[] preparedLines, int lineIndex, string preparedLine, int nameIndex) + { + var whenOffset = FindTopLevelCSharpWhenKeywordOffset(preparedLine); + if (whenOffset >= 0 && nameIndex > whenOffset) + return false; + + var cursor = nameIndex; + if (IsCSharpConstantPatternAnchor(preparedLine, ref cursor)) + return true; cursor = nameIndex; cursor = SkipCSharpTriviaBackward(preparedLine, cursor); diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs index fbd580f984..6488717ca1 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs @@ -586,21 +586,21 @@ bool HasActiveSameFileCSharpTypeCandidate(string typeExpression, int lineNumber) ref pendingCSharpMultiLineTypePattern); } - bool ShouldSuppressDefinitionCall(string resolvedName, int callIndex) - { - if (definitionNames == null) - return false; - - if (language == "csharp") - { - if (context.Contains("when", StringComparison.Ordinal)) - return false; - } - - if (language != "sql") - return definitionNameIndices != null - && definitionNameIndices.TryGetValue(resolvedName, out var definitionIndex) - && callIndex == definitionIndex; + bool ShouldSuppressDefinitionCall(string resolvedName, int callIndex) + { + if (definitionNames == null) + return false; + + if (language == "csharp") + { + if (context.Contains("when", StringComparison.Ordinal)) + return false; + } + + if (language != "sql") + return definitionNameIndices != null + && definitionNameIndices.TryGetValue(resolvedName, out var definitionIndex) + && callIndex == definitionIndex; return SqlReferenceExtractor.ShouldSuppressDefinitionCall(sqlDefinitionLeafSpans, resolvedName, callIndex); } diff --git a/src/CodeIndex/Indexer/References/Support/StructuralLineMasker.cs b/src/CodeIndex/Indexer/References/Support/StructuralLineMasker.cs index 71f88fabe1..cc653982c1 100644 --- a/src/CodeIndex/Indexer/References/Support/StructuralLineMasker.cs +++ b/src/CodeIndex/Indexer/References/Support/StructuralLineMasker.cs @@ -2670,7 +2670,7 @@ private static int SkipJsSingleLineString(string line, int startIndex) if (line[p] == '\\' && p + 1 < line.Length) p += 2; else - p++; + p++; } if (p < line.Length) p++; @@ -2904,17 +2904,17 @@ private static void MaskKotlinTripleStringContents(string[] lines) // still reach the reference graph), and otherwise mask. // 外側ホール内で開いた nested triple 本体。閉じ `"""`、内側 // `${...}` ホール、それ以外は body としてマスク。 - if (pos + 2 < line.Length - && line[pos] == '"' && line[pos + 1] == '"' && line[pos + 2] == '"') - { - ReplaceWithSpaces(masked, pos, 3); - pos += 3; - nestedTripleOpen = false; - nestedHoleBraceDepth = -1; - deepNestedTripleDepth = 0; - deepNestedTripleHashCounts.Clear(); - continue; - } + if (pos + 2 < line.Length + && line[pos] == '"' && line[pos + 1] == '"' && line[pos + 2] == '"') + { + ReplaceWithSpaces(masked, pos, 3); + pos += 3; + nestedTripleOpen = false; + nestedHoleBraceDepth = -1; + deepNestedTripleDepth = 0; + deepNestedTripleHashCounts.Clear(); + continue; + } if (pos + 1 < line.Length && line[pos] == '$' && line[pos + 1] == '{') { ReplaceWithSpaces(masked, pos, 2); @@ -3745,18 +3745,18 @@ private static void MaskScalaTripleStringContents(string[] lines) // 外側ホール内で開いた nested triple 本体。閉じ `"""`、 // interpolator 付きでは `${...}` を内部ホールとして開く、 // それ以外は body としてマスク。 - if (pos + 2 < line.Length - && line[pos] == '"' && line[pos + 1] == '"' && line[pos + 2] == '"') - { - ReplaceWithSpaces(masked, pos, 3); - pos += 3; - nestedTripleOpen = false; - nestedTripleIsInterpolator = false; - nestedHoleBraceDepth = -1; - deepNestedTripleDepth = 0; - deepNestedTripleHashCounts.Clear(); - continue; - } + if (pos + 2 < line.Length + && line[pos] == '"' && line[pos + 1] == '"' && line[pos + 2] == '"') + { + ReplaceWithSpaces(masked, pos, 3); + pos += 3; + nestedTripleOpen = false; + nestedTripleIsInterpolator = false; + nestedHoleBraceDepth = -1; + deepNestedTripleDepth = 0; + deepNestedTripleHashCounts.Clear(); + continue; + } if (nestedTripleIsInterpolator && pos + 1 < line.Length && line[pos] == '$' && line[pos + 1] == '{') diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index 3473929b2a..9b2e08ce98 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -92,9 +92,9 @@ PathFilterKind.ExcludedByDefaultFile or // Extension-to-language mapping / 拡張子→言語名マッピング private static readonly Dictionary LangMap = new(StringComparer.OrdinalIgnoreCase) { - [".py"] = "python", - [".pyi"] = "python", // Python type stub (PEP 561) / Python 型スタブ - [".pyw"] = "python", // Windowed Python script / Windows 用 Python スクリプト + [".py"] = "python", + [".pyi"] = "python", // Python type stub (PEP 561) / Python 型スタブ + [".pyw"] = "python", // Windowed Python script / Windows 用 Python スクリプト // Cython's `.pyx` / `.pxd` live in their own search-only bucket: they extend Python syntax // with `cdef class` / `cpdef` / `cdef` forms that the Python regex extractor cannot parse, // so mapping them to `python` would advertise `symbol_extraction=true` while emitting zero @@ -103,191 +103,191 @@ PathFilterKind.ExcludedByDefaultFile or // Cython の `.pyx` / `.pxd` は `cdef class` / `cpdef` / `cdef` を含み Python 用正規表現では // 拾えない。python にマップすると `symbol_extraction=true` と広告しつつ 0 件しか出ない // 齟齬になるため、`.sass` / `.styl` と同じく独立の search-only バケットに分ける。 - [".pyx"] = "cython", // Cython source / Cython ソース - [".pxd"] = "cython", // Cython declaration / Cython 宣言 - [".js"] = "javascript", - [".cjs"] = "javascript", - [".mjs"] = "javascript", - [".ts"] = "typescript", - [".cts"] = "typescript", - [".mts"] = "typescript", - [".jsx"] = "javascript", - [".tsx"] = "typescript", - [".rb"] = "ruby", - [".rake"] = "ruby", // Rake tasks / Rake タスク - [".gemspec"]= "ruby", // RubyGems spec / RubyGems スペック - [".podspec"]= "ruby", // CocoaPods spec (Ruby DSL) / CocoaPods スペック + [".pyx"] = "cython", // Cython source / Cython ソース + [".pxd"] = "cython", // Cython declaration / Cython 宣言 + [".js"] = "javascript", + [".cjs"] = "javascript", + [".mjs"] = "javascript", + [".ts"] = "typescript", + [".cts"] = "typescript", + [".mts"] = "typescript", + [".jsx"] = "javascript", + [".tsx"] = "typescript", + [".rb"] = "ruby", + [".rake"] = "ruby", // Rake tasks / Rake タスク + [".gemspec"] = "ruby", // RubyGems spec / RubyGems スペック + [".podspec"] = "ruby", // CocoaPods spec (Ruby DSL) / CocoaPods スペック [".groovy"] = "groovy", - [".gvy"] = "groovy", - [".gy"] = "groovy", - [".gsh"] = "groovy", - [".go"] = "go", - [".rs"] = "rust", - [".java"] = "java", - [".kt"] = "kotlin", - [".kts"] = "kotlin", // Kotlin Script / Kotlin スクリプト (Gradle Kotlin DSL など) - [".swift"] = "swift", - [".cu"] = "cuda", - [".cuh"] = "cuda", - [".glsl"] = "glsl", - [".vert"] = "glsl", - [".frag"] = "glsl", - [".hlsl"] = "hlsl", - [".wgsl"] = "wgsl", - [".metal"] = "metal", - [".c"] = "c", - [".cpp"] = "cpp", - [".cc"] = "cpp", - [".cxx"] = "cpp", - [".h"] = "c", // Could be C or C++; defaults to C for symbol extraction - [".hh"] = "cpp", - [".hpp"] = "cpp", - [".hxx"] = "cpp", - [".cs"] = "csharp", + [".gvy"] = "groovy", + [".gy"] = "groovy", + [".gsh"] = "groovy", + [".go"] = "go", + [".rs"] = "rust", + [".java"] = "java", + [".kt"] = "kotlin", + [".kts"] = "kotlin", // Kotlin Script / Kotlin スクリプト (Gradle Kotlin DSL など) + [".swift"] = "swift", + [".cu"] = "cuda", + [".cuh"] = "cuda", + [".glsl"] = "glsl", + [".vert"] = "glsl", + [".frag"] = "glsl", + [".hlsl"] = "hlsl", + [".wgsl"] = "wgsl", + [".metal"] = "metal", + [".c"] = "c", + [".cpp"] = "cpp", + [".cc"] = "cpp", + [".cxx"] = "cpp", + [".h"] = "c", // Could be C or C++; defaults to C for symbol extraction + [".hh"] = "cpp", + [".hpp"] = "cpp", + [".hxx"] = "cpp", + [".cs"] = "csharp", [".cshtml"] = "csharp", // Razor (ASP.NET MVC/Pages) / Razor テンプレート - [".razor"] = "csharp", // Blazor component / Blazor コンポーネント - [".m"] = "objc", - [".mm"] = "objc", - [".php"] = "php", - [".s"] = "assembly", // Also used by Scheme; assembly is the more common default. - [".S"] = "assembly", - [".asm"] = "assembly", - [".nasm"] = "assembly", - [".sh"] = "shell", - [".sql"] = "sql", - [".pgsql"] = "sql", // PostgreSQL dialect / PostgreSQL 方言 - [".tsql"] = "sql", // T-SQL (SQL Server) / T-SQL (SQL Server) - [".plsql"] = "sql", // PL/SQL (Oracle) / PL/SQL (Oracle) - [".pls"] = "sql", // PL/SQL script (Oracle) / PL/SQL スクリプト (Oracle) - [".pks"] = "sql", // PL/SQL package spec (Oracle) / PL/SQL パッケージ仕様 (Oracle) - [".pkb"] = "sql", // PL/SQL package body (Oracle) / PL/SQL パッケージ本体 (Oracle) - [".plb"] = "sql", // PL/SQL wrapped source (Oracle) / PL/SQL ラップ済みソース (Oracle) - [".psql"] = "sql", // psql scripts / psql スクリプト - [".md"] = "markdown", - [".yaml"] = "yaml", - [".yml"] = "yaml", - [".json"] = "json", - [".toml"] = "toml", - [".xaml"] = "xml", // WPF/MAUI/Avalonia XAML / XAML テンプレート - [".axaml"] = "xml", // Avalonia XAML / Avalonia XAML + [".razor"] = "csharp", // Blazor component / Blazor コンポーネント + [".m"] = "objc", + [".mm"] = "objc", + [".php"] = "php", + [".s"] = "assembly", // Also used by Scheme; assembly is the more common default. + [".S"] = "assembly", + [".asm"] = "assembly", + [".nasm"] = "assembly", + [".sh"] = "shell", + [".sql"] = "sql", + [".pgsql"] = "sql", // PostgreSQL dialect / PostgreSQL 方言 + [".tsql"] = "sql", // T-SQL (SQL Server) / T-SQL (SQL Server) + [".plsql"] = "sql", // PL/SQL (Oracle) / PL/SQL (Oracle) + [".pls"] = "sql", // PL/SQL script (Oracle) / PL/SQL スクリプト (Oracle) + [".pks"] = "sql", // PL/SQL package spec (Oracle) / PL/SQL パッケージ仕様 (Oracle) + [".pkb"] = "sql", // PL/SQL package body (Oracle) / PL/SQL パッケージ本体 (Oracle) + [".plb"] = "sql", // PL/SQL wrapped source (Oracle) / PL/SQL ラップ済みソース (Oracle) + [".psql"] = "sql", // psql scripts / psql スクリプト + [".md"] = "markdown", + [".yaml"] = "yaml", + [".yml"] = "yaml", + [".json"] = "json", + [".toml"] = "toml", + [".xaml"] = "xml", // WPF/MAUI/Avalonia XAML / XAML テンプレート + [".axaml"] = "xml", // Avalonia XAML / Avalonia XAML [".csproj"] = "msbuild",// C# project file / C# プロジェクトファイル [".fsproj"] = "msbuild",// F# project file / F# プロジェクトファイル [".vbproj"] = "msbuild",// VB.NET project file / VB.NET プロジェクトファイル - [".props"] = "msbuild",// MSBuild props / MSBuild プロパティ - [".targets"]= "msbuild",// MSBuild targets / MSBuild ターゲット - [".html"] = "html", - [".htm"] = "html", // Legacy / Windows / IIS default / 旧来の Windows / IIS 既定拡張子 - [".xhtml"] = "html", // XHTML / XHTML - [".shtml"] = "html", // Server-side includes / サーバサイドインクルード - [".css"] = "css", - [".scss"] = "css", - [".less"] = "css", // Less preprocessor / Less プリプロセッサ - [".pcss"] = "css", // PostCSS / PostCSS + [".props"] = "msbuild",// MSBuild props / MSBuild プロパティ + [".targets"] = "msbuild",// MSBuild targets / MSBuild ターゲット + [".html"] = "html", + [".htm"] = "html", // Legacy / Windows / IIS default / 旧来の Windows / IIS 既定拡張子 + [".xhtml"] = "html", // XHTML / XHTML + [".shtml"] = "html", // Server-side includes / サーバサイドインクルード + [".css"] = "css", + [".scss"] = "css", + [".less"] = "css", // Less preprocessor / Less プリプロセッサ + [".pcss"] = "css", // PostCSS / PostCSS // Sass indented syntax / Stylus use indentation instead of braces, so they live in // separate search-only buckets — the CSS symbol extractor's brace-based patterns do // not apply, but exact-name search still works. // Sass インデント構文と Stylus は波括弧ではなくインデントで構造化するため、 // CSS のシンボル抽出(波括弧ベース)は使わず、検索用の別バケットに分ける。 - [".sass"] = "sass", - [".styl"] = "stylus", - [".vue"] = "vue", + [".sass"] = "sass", + [".styl"] = "stylus", + [".vue"] = "vue", [".svelte"] = "svelte", - [".tf"] = "terraform", - [".v"] = "verilog", // Verilog defaults here; SystemVerilog has its own extensions. - [".sv"] = "systemverilog", - [".svh"] = "systemverilog", - [".vhd"] = "vhdl", - [".vhdl"] = "vhdl", - [".lisp"] = "commonlisp", - [".lsp"] = "commonlisp", - [".cl"] = "commonlisp", // Common Lisp wins the default over OpenCL here. - [".rkt"] = "racket", - [".pas"] = "pascal", - [".pp"] = "pascal", - [".dpr"] = "pascal", - [".st"] = "smalltalk", + [".tf"] = "terraform", + [".v"] = "verilog", // Verilog defaults here; SystemVerilog has its own extensions. + [".sv"] = "systemverilog", + [".svh"] = "systemverilog", + [".vhd"] = "vhdl", + [".vhdl"] = "vhdl", + [".lisp"] = "commonlisp", + [".lsp"] = "commonlisp", + [".cl"] = "commonlisp", // Common Lisp wins the default over OpenCL here. + [".rkt"] = "racket", + [".pas"] = "pascal", + [".pp"] = "pascal", + [".dpr"] = "pascal", + [".st"] = "smalltalk", [".smalltalk"] = "smalltalk", - [".ada"] = "ada", - [".adb"] = "ada", - [".ads"] = "ada", - [".f"] = "fortran", - [".f77"] = "fortran", - [".f90"] = "fortran", - [".f95"] = "fortran", - [".f03"] = "fortran", - [".f08"] = "fortran", - [".for"] = "fortran", - [".ftn"] = "fortran", - [".cbl"] = "cobol", - [".cob"] = "cobol", - [".cobol"] = "cobol", - [".cpy"] = "cobol", // COBOL copybook / COBOL コピー句 - [".raku"] = "raku", - [".rakumod"]= "raku", - [".rakutest"]= "raku", - [".t"] = "perl", // Common Perl test scripts / Perl の test スクリプト - [".dart"] = "dart", - [".scala"] = "scala", - [".sc"] = "scala", - [".r"] = "r", - [".R"] = "r", - [".ex"] = "elixir", - [".exs"] = "elixir", - [".lua"] = "lua", - [".ml"] = "ocaml", - [".mli"] = "ocaml", - [".cr"] = "crystal", - [".clj"] = "clojure", - [".cljs"] = "clojure", - [".cljc"] = "clojure", - [".edn"] = "clojure", - [".d"] = "d", - [".erl"] = "erlang", - [".hrl"] = "erlang", - [".jl"] = "julia", - [".nim"] = "nim", - [".nims"] = "nim", - [".pl"] = "perl", - [".pm"] = "perl", - [".pod"] = "perl", - [".psgi"] = "perl", - [".cgi"] = "perl", - [".fcgi"] = "perl", - [".t"] = "perl", - [".sol"] = "solidity", - [".tcl"] = "tcl", - [".tk"] = "tcl", - [".fs"] = "fsharp", - [".fsx"] = "fsharp", - [".fsi"] = "fsharp", - [".bas"] = "vb", - [".cls"] = "vb", - [".ctl"] = "vb", - [".dob"] = "vb", - [".dsr"] = "vb", - [".frm"] = "vb", - [".pag"] = "vb", - [".vba"] = "vb", - [".vb"] = "vb", + [".ada"] = "ada", + [".adb"] = "ada", + [".ads"] = "ada", + [".f"] = "fortran", + [".f77"] = "fortran", + [".f90"] = "fortran", + [".f95"] = "fortran", + [".f03"] = "fortran", + [".f08"] = "fortran", + [".for"] = "fortran", + [".ftn"] = "fortran", + [".cbl"] = "cobol", + [".cob"] = "cobol", + [".cobol"] = "cobol", + [".cpy"] = "cobol", // COBOL copybook / COBOL コピー句 + [".raku"] = "raku", + [".rakumod"] = "raku", + [".rakutest"] = "raku", + [".t"] = "perl", // Common Perl test scripts / Perl の test スクリプト + [".dart"] = "dart", + [".scala"] = "scala", + [".sc"] = "scala", + [".r"] = "r", + [".R"] = "r", + [".ex"] = "elixir", + [".exs"] = "elixir", + [".lua"] = "lua", + [".ml"] = "ocaml", + [".mli"] = "ocaml", + [".cr"] = "crystal", + [".clj"] = "clojure", + [".cljs"] = "clojure", + [".cljc"] = "clojure", + [".edn"] = "clojure", + [".d"] = "d", + [".erl"] = "erlang", + [".hrl"] = "erlang", + [".jl"] = "julia", + [".nim"] = "nim", + [".nims"] = "nim", + [".pl"] = "perl", + [".pm"] = "perl", + [".pod"] = "perl", + [".psgi"] = "perl", + [".cgi"] = "perl", + [".fcgi"] = "perl", + [".t"] = "perl", + [".sol"] = "solidity", + [".tcl"] = "tcl", + [".tk"] = "tcl", + [".fs"] = "fsharp", + [".fsx"] = "fsharp", + [".fsi"] = "fsharp", + [".bas"] = "vb", + [".cls"] = "vb", + [".ctl"] = "vb", + [".dob"] = "vb", + [".dsr"] = "vb", + [".frm"] = "vb", + [".pag"] = "vb", + [".vba"] = "vb", + [".vb"] = "vb", [".vbhtml"] = "vb", - [".vbs"] = "vb", - [".hs"] = "haskell", - [".lhs"] = "haskell", - [".zig"] = "zig", - [".proto"] = "protobuf", // Protocol Buffers / Protocol Buffers 定義 - [".graphql"]= "graphql", // GraphQL schema/queries / GraphQL スキーマ・クエリ - [".gql"] = "graphql", + [".vbs"] = "vb", + [".hs"] = "haskell", + [".lhs"] = "haskell", + [".zig"] = "zig", + [".proto"] = "protobuf", // Protocol Buffers / Protocol Buffers 定義 + [".graphql"] = "graphql", // GraphQL schema/queries / GraphQL スキーマ・クエリ + [".gql"] = "graphql", [".gradle"] = "gradle", // Gradle build scripts / Gradle ビルドスクリプト - [".cmake"] = "cmake", // CMake scripts / CMake スクリプト - [".mk"] = "makefile", // Makefile fragment / Makefile フラグメント - [".ps1"] = "powershell",// PowerShell scripts / PowerShell スクリプト - [".psm1"] = "powershell",// PowerShell modules / PowerShell モジュール - [".psd1"] = "powershell",// PowerShell data files / PowerShell データファイル - [".bat"] = "batch", // Windows batch files / Windows バッチファイル - [".cmd"] = "batch", - [".bash"] = "shell", - [".zsh"] = "shell", - [".fish"] = "shell", + [".cmake"] = "cmake", // CMake scripts / CMake スクリプト + [".mk"] = "makefile", // Makefile fragment / Makefile フラグメント + [".ps1"] = "powershell",// PowerShell scripts / PowerShell スクリプト + [".psm1"] = "powershell",// PowerShell modules / PowerShell モジュール + [".psd1"] = "powershell",// PowerShell data files / PowerShell データファイル + [".bat"] = "batch", // Windows batch files / Windows バッチファイル + [".cmd"] = "batch", + [".bash"] = "shell", + [".zsh"] = "shell", + [".fish"] = "shell", [".dockerfile"] = "dockerfile", // Suffix-style Dockerfile names such as app.Dockerfile / app.Dockerfile 形式 [".containerfile"] = "dockerfile", // Suffix-style Containerfile names such as app.Containerfile / app.Containerfile 形式 }; @@ -300,33 +300,33 @@ private static readonly (string Pattern, string Language)[] DisplayOnlyLanguageE // Exact file names (case-insensitive) mapped to language / 完全一致ファイル名→言語マッピング private static readonly Dictionary FileNameMap = new(StringComparer.OrdinalIgnoreCase) { - ["Dockerfile"] = "dockerfile", - [".dockerfile"] = "dockerfile", + ["Dockerfile"] = "dockerfile", + [".dockerfile"] = "dockerfile", ["Containerfile"] = "dockerfile", // Podman's Dockerfile alternative / Podman の Dockerfile 代替 - [".containerfile"]= "dockerfile", - ["Makefile"] = "makefile", - ["GNUmakefile"] = "makefile", // GNU Make explicit filename / GNU Make 明示ファイル名 - ["Justfile"] = "justfile", // Just command runner / Just コマンドランナー - ["CMakeLists.txt"]= "cmake", - ["Vagrantfile"] = "ruby", // Vagrant uses Ruby DSL / Vagrant は Ruby DSL - ["Gemfile"] = "ruby", // Bundler dependency manifest / Bundler 依存マニフェスト - ["Rakefile"] = "ruby", // Rake task runner / Rake タスクランナー - ["Podfile"] = "ruby", // CocoaPods dependency manifest / CocoaPods 依存マニフェスト - ["Guardfile"] = "ruby", // Guard file-watcher / Guard ファイルウォッチャー - ["Capfile"] = "ruby", // Capistrano deployment / Capistrano デプロイ - ["NAMESPACE"] = "r", // R package namespace directives / R パッケージ namespace ディレクティブ - [".Rprofile"] = "r", // R startup profile / R 起動プロファイル + [".containerfile"] = "dockerfile", + ["Makefile"] = "makefile", + ["GNUmakefile"] = "makefile", // GNU Make explicit filename / GNU Make 明示ファイル名 + ["Justfile"] = "justfile", // Just command runner / Just コマンドランナー + ["CMakeLists.txt"] = "cmake", + ["Vagrantfile"] = "ruby", // Vagrant uses Ruby DSL / Vagrant は Ruby DSL + ["Gemfile"] = "ruby", // Bundler dependency manifest / Bundler 依存マニフェスト + ["Rakefile"] = "ruby", // Rake task runner / Rake タスクランナー + ["Podfile"] = "ruby", // CocoaPods dependency manifest / CocoaPods 依存マニフェスト + ["Guardfile"] = "ruby", // Guard file-watcher / Guard ファイルウォッチャー + ["Capfile"] = "ruby", // Capistrano deployment / Capistrano デプロイ + ["NAMESPACE"] = "r", // R package namespace directives / R パッケージ namespace ディレクティブ + [".Rprofile"] = "r", // R startup profile / R 起動プロファイル ["Rprofile.site"] = "r", // Site-wide R startup profile / サイト共通 R 起動プロファイル - ["BUILD"] = "python", // Bazel Starlark build file / Bazel Starlark ビルドファイル - ["BUILD.bazel"] = "python", - ["WORKSPACE"] = "python", // Bazel workspace / Bazel ワークスペース - ["WORKSPACE.bazel"]= "python", + ["BUILD"] = "python", // Bazel Starlark build file / Bazel Starlark ビルドファイル + ["BUILD.bazel"] = "python", + ["WORKSPACE"] = "python", // Bazel workspace / Bazel ワークスペース + ["WORKSPACE.bazel"] = "python", ["pyproject.toml"] = "python", // Python project manifest / Python プロジェクトマニフェスト ["requirements.txt"] = "python", // Python dependencies manifest / Python 依存関係マニフェスト - ["go.mod"] = "go", // Go module manifest / Go モジュールマニフェスト - ["go.work"] = "go", // Go workspace manifest / Go ワークスペースマニフェスト + ["go.mod"] = "go", // Go module manifest / Go モジュールマニフェスト + ["go.work"] = "go", // Go workspace manifest / Go ワークスペースマニフェスト [".editorconfig"] = "editorconfig", - [".gitignore"] = "gitignore", + [".gitignore"] = "gitignore", [".dockerignore"] = "dockerignore", }; diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.JavaScriptTypeScriptSupport.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.JavaScriptTypeScriptSupport.cs index cb0a6bd2f8..1fe39124d2 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.JavaScriptTypeScriptSupport.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.JavaScriptTypeScriptSupport.cs @@ -7510,18 +7510,18 @@ private static void TryAddJavaScriptTypeScriptSyntheticClassTarget( return; } - AddJavaScriptTypeScriptSyntheticClassTarget( - fileId, - lang, - lines, - symbols, - targets, - startIndex, - startColumn + anonymousDefaultMatch.Index, - classTokenLineIndex, - classTokenStartColumn, - containerName: "default", - visibility: TryGetGroup(anonymousDefaultMatch, "visibility")); + AddJavaScriptTypeScriptSyntheticClassTarget( + fileId, + lang, + lines, + symbols, + targets, + startIndex, + startColumn + anonymousDefaultMatch.Index, + classTokenLineIndex, + classTokenStartColumn, + containerName: "default", + visibility: TryGetGroup(anonymousDefaultMatch, "visibility")); return; } diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Ruby.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Ruby.cs index efd8986591..bb7958fecf 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Ruby.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Ruby.cs @@ -51,276 +51,276 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) FindRubyRange : (lines.Length, bodyStartLine, lines.Length); } -private static string MaskRubyLineForBodyScan(string line, RubyMaskState state) -{ - if (line.Length == 0) - return line; - - var masked = line.ToCharArray(); - - if (state.Mode == RubyScanMode.Heredoc) + private static string MaskRubyLineForBodyScan(string line, RubyMaskState state) { - for (int i = 0; i < masked.Length; i++) - masked[i] = ' '; + if (line.Length == 0) + return line; - if (IsRubyHeredocTerminatorLine(line, state.HeredocTerminator!, state.HeredocAllowsIndentation)) + var masked = line.ToCharArray(); + + if (state.Mode == RubyScanMode.Heredoc) { - state.Mode = RubyScanMode.Code; - state.HeredocTerminator = null; - state.HeredocAllowsIndentation = false; - } + for (int i = 0; i < masked.Length; i++) + masked[i] = ' '; - return new string(masked); - } + if (IsRubyHeredocTerminatorLine(line, state.HeredocTerminator!, state.HeredocAllowsIndentation)) + { + state.Mode = RubyScanMode.Code; + state.HeredocTerminator = null; + state.HeredocAllowsIndentation = false; + } - for (int i = 0; i < masked.Length; i++) - { - if (state.Mode == RubyScanMode.SingleQuote) + return new string(masked); + } + + for (int i = 0; i < masked.Length; i++) { - masked[i] = ' '; - if (line[i] == '\\' && i + 1 < masked.Length) + if (state.Mode == RubyScanMode.SingleQuote) { - masked[++i] = ' '; + masked[i] = ' '; + if (line[i] == '\\' && i + 1 < masked.Length) + { + masked[++i] = ' '; + continue; + } + + if (line[i] == '\'') + state.Mode = RubyScanMode.Code; + continue; } - if (line[i] == '\'') - state.Mode = RubyScanMode.Code; + if (state.Mode == RubyScanMode.DoubleQuote) + { + masked[i] = ' '; + if (line[i] == '\\' && i + 1 < masked.Length) + { + masked[++i] = ' '; + continue; + } - continue; - } + if (line[i] == '"') + state.Mode = RubyScanMode.Code; - if (state.Mode == RubyScanMode.DoubleQuote) - { - masked[i] = ' '; - if (line[i] == '\\' && i + 1 < masked.Length) - { - masked[++i] = ' '; continue; } - if (line[i] == '"') - state.Mode = RubyScanMode.Code; + if (state.Mode == RubyScanMode.PercentLiteral) + { + masked[i] = ' '; + if (line[i] == '\\' && i + 1 < masked.Length) + { + masked[++i] = ' '; + continue; + } - continue; - } + if (state.PercentDelimiterIsPaired && line[i] == state.PercentOpenDelimiter) + { + state.PercentDelimiterDepth++; + continue; + } - if (state.Mode == RubyScanMode.PercentLiteral) - { - masked[i] = ' '; - if (line[i] == '\\' && i + 1 < masked.Length) + if (line[i] == state.PercentCloseDelimiter) + { + if (state.PercentDelimiterIsPaired && state.PercentDelimiterDepth > 0) + { + state.PercentDelimiterDepth--; + continue; + } + + state.Mode = RubyScanMode.Code; + state.PercentOpenDelimiter = default; + state.PercentCloseDelimiter = default; + state.PercentDelimiterIsPaired = false; + state.PercentDelimiterDepth = 0; + } + + continue; + } + + if (line[i] == '#') + { + for (int j = i; j < masked.Length; j++) + masked[j] = ' '; + break; + } + + if (line[i] == '\'') { - masked[++i] = ' '; + masked[i] = ' '; + state.Mode = RubyScanMode.SingleQuote; continue; } - if (state.PercentDelimiterIsPaired && line[i] == state.PercentOpenDelimiter) + if (line[i] == '"') { - state.PercentDelimiterDepth++; + masked[i] = ' '; + state.Mode = RubyScanMode.DoubleQuote; continue; } - if (line[i] == state.PercentCloseDelimiter) + if (TryStartRubyPercentLiteral(line, i, out var consumedChars, out var openDelimiter, out var closeDelimiter, out var isPaired)) { - if (state.PercentDelimiterIsPaired && state.PercentDelimiterDepth > 0) - { - state.PercentDelimiterDepth--; - continue; - } + for (int j = 0; j < consumedChars && i + j < masked.Length; j++) + masked[i + j] = ' '; - state.Mode = RubyScanMode.Code; - state.PercentOpenDelimiter = default; - state.PercentCloseDelimiter = default; - state.PercentDelimiterIsPaired = false; + state.Mode = RubyScanMode.PercentLiteral; + state.PercentOpenDelimiter = openDelimiter; + state.PercentCloseDelimiter = closeDelimiter; + state.PercentDelimiterIsPaired = isPaired; state.PercentDelimiterDepth = 0; + i += consumedChars - 1; + continue; } - continue; - } + if (TryStartRubyHeredoc(line, i, out consumedChars, out var heredocTerminator, out var heredocAllowsIndentation)) + { + for (int j = i; j < masked.Length; j++) + masked[j] = ' '; - if (line[i] == '#') - { - for (int j = i; j < masked.Length; j++) - masked[j] = ' '; - break; + state.Mode = RubyScanMode.Heredoc; + state.HeredocTerminator = heredocTerminator; + state.HeredocAllowsIndentation = heredocAllowsIndentation; + return new string(masked); + } } - if (line[i] == '\'') + return new string(masked); + } + + private static bool TryStartRubyPercentLiteral(string line, int index, out int consumedChars, out char openDelimiter, out char closeDelimiter, out bool isPaired) + { + consumedChars = 0; + openDelimiter = default; + closeDelimiter = default; + isPaired = false; + + if (index + 2 >= line.Length || line[index] != '%' || !IsRubyPercentLiteralKind(line[index + 1])) + return false; + + var delimiter = line[index + 2]; + if (!TryGetRubyPercentLiteralDelimiterPair(delimiter, out openDelimiter, out closeDelimiter, out isPaired)) + return false; + + consumedChars = 3; + return true; + } + + private static bool IsRubyPercentLiteralKind(char ch) + => ch is 'q' or 'Q' or 'r' or 'w' or 'W' or 'i' or 'I' or 'x' or 'X'; + + private static bool TryGetRubyPercentLiteralDelimiterPair(char delimiter, out char openDelimiter, out char closeDelimiter, out bool isPaired) + { + if (delimiter == '(') { - masked[i] = ' '; - state.Mode = RubyScanMode.SingleQuote; - continue; + openDelimiter = '('; + closeDelimiter = ')'; + isPaired = true; + return true; } - if (line[i] == '"') + if (delimiter == '[') { - masked[i] = ' '; - state.Mode = RubyScanMode.DoubleQuote; - continue; + openDelimiter = '['; + closeDelimiter = ']'; + isPaired = true; + return true; } - if (TryStartRubyPercentLiteral(line, i, out var consumedChars, out var openDelimiter, out var closeDelimiter, out var isPaired)) + if (delimiter == '{') { - for (int j = 0; j < consumedChars && i + j < masked.Length; j++) - masked[i + j] = ' '; - - state.Mode = RubyScanMode.PercentLiteral; - state.PercentOpenDelimiter = openDelimiter; - state.PercentCloseDelimiter = closeDelimiter; - state.PercentDelimiterIsPaired = isPaired; - state.PercentDelimiterDepth = 0; - i += consumedChars - 1; - continue; + openDelimiter = '{'; + closeDelimiter = '}'; + isPaired = true; + return true; } - if (TryStartRubyHeredoc(line, i, out consumedChars, out var heredocTerminator, out var heredocAllowsIndentation)) + if (delimiter == '<') { - for (int j = i; j < masked.Length; j++) - masked[j] = ' '; - - state.Mode = RubyScanMode.Heredoc; - state.HeredocTerminator = heredocTerminator; - state.HeredocAllowsIndentation = heredocAllowsIndentation; - return new string(masked); + openDelimiter = '<'; + closeDelimiter = '>'; + isPaired = true; + return true; } - } - - return new string(masked); -} -private static bool TryStartRubyPercentLiteral(string line, int index, out int consumedChars, out char openDelimiter, out char closeDelimiter, out bool isPaired) -{ - consumedChars = 0; - openDelimiter = default; - closeDelimiter = default; - isPaired = false; - - if (index + 2 >= line.Length || line[index] != '%' || !IsRubyPercentLiteralKind(line[index + 1])) - return false; - - var delimiter = line[index + 2]; - if (!TryGetRubyPercentLiteralDelimiterPair(delimiter, out openDelimiter, out closeDelimiter, out isPaired)) - return false; - - consumedChars = 3; - return true; -} - -private static bool IsRubyPercentLiteralKind(char ch) - => ch is 'q' or 'Q' or 'r' or 'w' or 'W' or 'i' or 'I' or 'x' or 'X'; - -private static bool TryGetRubyPercentLiteralDelimiterPair(char delimiter, out char openDelimiter, out char closeDelimiter, out bool isPaired) -{ - if (delimiter == '(') - { - openDelimiter = '('; - closeDelimiter = ')'; - isPaired = true; - return true; - } - - if (delimiter == '[') - { - openDelimiter = '['; - closeDelimiter = ']'; - isPaired = true; + openDelimiter = delimiter; + closeDelimiter = delimiter; + isPaired = false; return true; } - if (delimiter == '{') + private static bool TryStartRubyHeredoc(string line, int index, out int consumedChars, out string terminator, out bool allowsIndentation) { - openDelimiter = '{'; - closeDelimiter = '}'; - isPaired = true; - return true; - } + consumedChars = 0; + terminator = string.Empty; + allowsIndentation = false; - if (delimiter == '<') - { - openDelimiter = '<'; - closeDelimiter = '>'; - isPaired = true; - return true; - } + if (index + 1 >= line.Length || line[index] != '<' || line[index + 1] != '<') + return false; - openDelimiter = delimiter; - closeDelimiter = delimiter; - isPaired = false; - return true; -} + var scanIndex = index + 2; + if (scanIndex < line.Length && line[scanIndex] is '-' or '~') + { + allowsIndentation = true; + scanIndex++; + } -private static bool TryStartRubyHeredoc(string line, int index, out int consumedChars, out string terminator, out bool allowsIndentation) -{ - consumedChars = 0; - terminator = string.Empty; - allowsIndentation = false; + if (scanIndex >= line.Length) + return false; - if (index + 1 >= line.Length || line[index] != '<' || line[index + 1] != '<') - return false; + if (line[scanIndex] is '\'' or '"' or '`') + { + var quote = line[scanIndex]; + scanIndex++; + var start = scanIndex; + while (scanIndex < line.Length && line[scanIndex] != quote) + scanIndex++; - var scanIndex = index + 2; - if (scanIndex < line.Length && line[scanIndex] is '-' or '~') - { - allowsIndentation = true; - scanIndex++; - } + if (scanIndex >= line.Length || scanIndex == start) + return false; - if (scanIndex >= line.Length) - return false; + terminator = line[start..scanIndex]; + consumedChars = scanIndex + 1 - index; + return true; + } - if (line[scanIndex] is '\'' or '"' or '`') - { - var quote = line[scanIndex]; - scanIndex++; - var start = scanIndex; - while (scanIndex < line.Length && line[scanIndex] != quote) + var startIndex = scanIndex; + while (scanIndex < line.Length && (char.IsLetterOrDigit(line[scanIndex]) || line[scanIndex] == '_')) scanIndex++; - if (scanIndex >= line.Length || scanIndex == start) + if (scanIndex == startIndex) return false; - terminator = line[start..scanIndex]; - consumedChars = scanIndex + 1 - index; + terminator = line[startIndex..scanIndex]; + consumedChars = scanIndex - index; return true; } - var startIndex = scanIndex; - while (scanIndex < line.Length && (char.IsLetterOrDigit(line[scanIndex]) || line[scanIndex] == '_')) - scanIndex++; - - if (scanIndex == startIndex) - return false; - - terminator = line[startIndex..scanIndex]; - consumedChars = scanIndex - index; - return true; -} - -private static bool IsRubyHeredocTerminatorLine(string line, string terminator, bool allowsIndentation) -{ - var trimmed = allowsIndentation ? line.Trim() : line.TrimEnd(); - return trimmed == terminator; -} + private static bool IsRubyHeredocTerminatorLine(string line, string terminator, bool allowsIndentation) + { + var trimmed = allowsIndentation ? line.Trim() : line.TrimEnd(); + return trimmed == terminator; + } private static readonly Regex RubyBlockStartRegex = new(@"^\s*(?:(?:class|module|def|if|unless|case|begin|do|while|until|for)\b|(?:namespace|factory)\s+:\w+\b.*\bdo\b|shared_examples(?:_for)?\s+['""].*['""]\s*do\b|(?:subject|let!?)\s*\(\s*:\w+\s*\)\s*do\b|[A-Z][A-Za-z0-9_]*\s*=\s*(?:Class|Struct)\.new\b.*\bdo\b)", RegexOptions.Compiled); private static readonly Regex RubyBlockTokenRegex = new(@"\b(?:class|module|def|if|unless|case|begin|do|while|until|for|end)\b", RegexOptions.Compiled); -private enum RubyScanMode -{ - Code, - SingleQuote, - DoubleQuote, - PercentLiteral, - Heredoc, -} -private sealed class RubyMaskState -{ - public RubyScanMode Mode { get; set; } = RubyScanMode.Code; - public char PercentOpenDelimiter { get; set; } - public char PercentCloseDelimiter { get; set; } - public bool PercentDelimiterIsPaired { get; set; } - public int PercentDelimiterDepth { get; set; } - public string? HeredocTerminator { get; set; } - public bool HeredocAllowsIndentation { get; set; } -} + private enum RubyScanMode + { + Code, + SingleQuote, + DoubleQuote, + PercentLiteral, + Heredoc, + } + private sealed class RubyMaskState + { + public RubyScanMode Mode { get; set; } = RubyScanMode.Code; + public char PercentOpenDelimiter { get; set; } + public char PercentCloseDelimiter { get; set; } + public bool PercentDelimiterIsPaired { get; set; } + public int PercentDelimiterDepth { get; set; } + public string? HeredocTerminator { get; set; } + public bool HeredocAllowsIndentation { get; set; } + } } diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 3c2b4f2b7b..5591e8a9b6 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -2580,1357 +2580,1357 @@ public static List Extract(long fileId, string? lang, string conte if (lang is "java" or "kotlin" && javaLeadingAnnotationOffset > 0) absoluteStartColumn = lineOffset + javaLeadingAnnotationOffset; var nextSameLineOffsetAfterRejectedCSharpProperty = -1; - if (ShouldSkipCSharpSwitchExpressionPropertyCandidate(lang, pattern, patternMatchLine, csharpSwitchExpressionLines, i) - || TrySkipCSharpBracePropertyCandidate( - lang, - pattern, - patternMatchLine, - absoluteStartColumn, - match.Value.Contains("=>", StringComparison.Ordinal), - out nextSameLineOffsetAfterRejectedCSharpProperty)) - { - // False-positive C# property matches can happen at the start of a - // same-line type header (`public class C { ... }`) because the - // property regex allows omitted visibility/modifier runs and can - // initially treat the header as `returnType + name + {`. Do not break - // the whole same-line scan on that rejection — advance to the next - // brace-delimited statement so a real nested property later on the - // same physical line still gets a chance to match. Closes #470. - // C# の property 正規表現は visibility / modifier 省略を許すため、 - // 同一行の型ヘッダ先頭 (`public class C { ... }`) を一旦 - // `returnType + name + {` と誤認することがある。この偽候補を弾いた - // ときに同一行スキャン全体を break せず、次の brace 区切り宣言へ進めて - // 後続の本物 property にもマッチ機会を残す。Closes #470. - lineOffset = nextSameLineOffsetAfterRejectedCSharpProperty >= 0 - ? nextSameLineOffsetAfterRejectedCSharpProperty - : FindNextSameLineBraceStatementStart( - matchLine, - absoluteStartColumn + Math.Max(1, match.Length), - lang); - continue; - } + if (ShouldSkipCSharpSwitchExpressionPropertyCandidate(lang, pattern, patternMatchLine, csharpSwitchExpressionLines, i) + || TrySkipCSharpBracePropertyCandidate( + lang, + pattern, + patternMatchLine, + absoluteStartColumn, + match.Value.Contains("=>", StringComparison.Ordinal), + out nextSameLineOffsetAfterRejectedCSharpProperty)) + { + // False-positive C# property matches can happen at the start of a + // same-line type header (`public class C { ... }`) because the + // property regex allows omitted visibility/modifier runs and can + // initially treat the header as `returnType + name + {`. Do not break + // the whole same-line scan on that rejection — advance to the next + // brace-delimited statement so a real nested property later on the + // same physical line still gets a chance to match. Closes #470. + // C# の property 正規表現は visibility / modifier 省略を許すため、 + // 同一行の型ヘッダ先頭 (`public class C { ... }`) を一旦 + // `returnType + name + {` と誤認することがある。この偽候補を弾いた + // ときに同一行スキャン全体を break せず、次の brace 区切り宣言へ進めて + // 後続の本物 property にもマッチ機会を残す。Closes #470. + lineOffset = nextSameLineOffsetAfterRejectedCSharpProperty >= 0 + ? nextSameLineOffsetAfterRejectedCSharpProperty + : FindNextSameLineBraceStatementStart( + matchLine, + absoluteStartColumn + Math.Max(1, match.Length), + lang); + continue; + } - // Gate the C# plain-field pattern (kind `property`, BodyStyle.None) to - // lines that sit directly inside a type body. Without this gate, local - // variable declarations inside method / property / accessor / lambda - // bodies match the same shape and leak into `symbols`, `definition`, - // `outline`, `inspect`, and `unused` as phantom property symbols. - // Closes #298 follow-up (codex review blocker). - // C# の通常フィールド用パターン(kind `property` かつ BodyStyle.None)は - // 型本体(class / struct / interface / record / enum の直下)でしか - // 許可しない。このゲートを入れないと、メソッド・プロパティ・アクセサ・ - // ラムダの内部にあるローカル変数宣言が同じ形でマッチしてしまい、 - // `symbols` / `definition` / `outline` / `inspect` / `unused` に - // 擬似シンボルが混入する。Closes #298 の codex レビュー blocker 対応。 - if (ShouldSkipCssNestedSelectorCandidate(lang, pattern, patternMatchLine, cssQualifiedRuleAncestors, i)) - break; + // Gate the C# plain-field pattern (kind `property`, BodyStyle.None) to + // lines that sit directly inside a type body. Without this gate, local + // variable declarations inside method / property / accessor / lambda + // bodies match the same shape and leak into `symbols`, `definition`, + // `outline`, `inspect`, and `unused` as phantom property symbols. + // Closes #298 follow-up (codex review blocker). + // C# の通常フィールド用パターン(kind `property` かつ BodyStyle.None)は + // 型本体(class / struct / interface / record / enum の直下)でしか + // 許可しない。このゲートを入れないと、メソッド・プロパティ・アクセサ・ + // ラムダの内部にあるローカル変数宣言が同じ形でマッチしてしまい、 + // `symbols` / `definition` / `outline` / `inspect` / `unused` に + // 擬似シンボルが混入する。Closes #298 の codex レビュー blocker 対応。 + if (ShouldSkipCssNestedSelectorCandidate(lang, pattern, patternMatchLine, cssQualifiedRuleAncestors, i)) + break; - // JS/TS HOC binding gate: the `styled.` / `styled(` / `styled\`` regex - // branch matches three shapes — factory capture (`const F = styled.div;`), - // plain call (`const F = styled(Component);`), and tagged template - // (`const F = styled.div\`...\``). Only the tagged-template shape - // actually declares a styled-component binding; the other two produce - // a factory / a styled wrapper-of-component without a component body - // on that line and must stay 0-symbol. This gate looks at the raw - // (unmasked) line because StructuralLineMasker.MaskJsTsTemplateLiteralContents - // replaces template-literal delimiters with space, so the masked - // `patternMatchLine` cannot see the backtick. Closes #240 follow-up - // (codex review #5 blocker). - // JS/TS HOC 束縛ゲート: `styled.` / `styled(` / `styled\`` の regex - // 分岐は 3 形状にマッチする — factory 捕捉(`const F = styled.div;`)、 - // 素の呼び出し(`const F = styled(Component);`)、タグ付きテンプレート - // (`const F = styled.div\`...\``)。実際に styled-component 束縛を - // 生むのはタグ付きテンプレート形のみで、前者 2 つはその行で component - // 本体を生やさないため 0 シンボルに保つ必要がある。このゲートは raw 行 - // (マスク前)を参照する — `StructuralLineMasker.MaskJsTsTemplateLiteralContents` - // がテンプレート区切りを空白にマスクするため、マスク後の - // `patternMatchLine` ではバッククォートが見えないことへの対処。 - // Closes #240 follow-up(codex レビュー #5 の blocker 対応)。 - if (ShouldSkipJavaScriptTypeScriptStyledFactoryCandidate(lang, pattern, match, lineOffset, lines, i)) - { - lineOffset = FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, lineOffset + Math.Max(1, match.Length)); - continue; - } + // JS/TS HOC binding gate: the `styled.` / `styled(` / `styled\`` regex + // branch matches three shapes — factory capture (`const F = styled.div;`), + // plain call (`const F = styled(Component);`), and tagged template + // (`const F = styled.div\`...\``). Only the tagged-template shape + // actually declares a styled-component binding; the other two produce + // a factory / a styled wrapper-of-component without a component body + // on that line and must stay 0-symbol. This gate looks at the raw + // (unmasked) line because StructuralLineMasker.MaskJsTsTemplateLiteralContents + // replaces template-literal delimiters with space, so the masked + // `patternMatchLine` cannot see the backtick. Closes #240 follow-up + // (codex review #5 blocker). + // JS/TS HOC 束縛ゲート: `styled.` / `styled(` / `styled\`` の regex + // 分岐は 3 形状にマッチする — factory 捕捉(`const F = styled.div;`)、 + // 素の呼び出し(`const F = styled(Component);`)、タグ付きテンプレート + // (`const F = styled.div\`...\``)。実際に styled-component 束縛を + // 生むのはタグ付きテンプレート形のみで、前者 2 つはその行で component + // 本体を生やさないため 0 シンボルに保つ必要がある。このゲートは raw 行 + // (マスク前)を参照する — `StructuralLineMasker.MaskJsTsTemplateLiteralContents` + // がテンプレート区切りを空白にマスクするため、マスク後の + // `patternMatchLine` ではバッククォートが見えないことへの対処。 + // Closes #240 follow-up(codex レビュー #5 の blocker 対応)。 + if (ShouldSkipJavaScriptTypeScriptStyledFactoryCandidate(lang, pattern, match, lineOffset, lines, i)) + { + lineOffset = FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, lineOffset + Math.Max(1, match.Length)); + continue; + } - // For C#, collapsed-space column (from CollapseCSharpGenericTypeWhitespace) - // has to be translated back to raw-space before it can be compared against - // CSharpTypeBodyScope's per-line transitions, which were built from - // structural (raw) columns. Only translate when the pattern match runs on - // the per-line collapsed string (single-line case); multi-line merged - // candidates use a different composed string whose column domain does not - // line up with a single line's map, so we leave the column alone there to - // preserve pre-existing behavior. Closes #400. - // C# では CollapseCSharpGenericTypeWhitespace で空白を取り除いた列を、 - // structural 行の生列で構築された CSharpTypeBodyScope に渡す前に - // raw 列へ戻す必要がある。複数行を結合した match では単一行の map が - // 使えないため、単一行ケース(per-line collapsed line そのものにマッチした - // 場合)だけ変換する。Closes #400. - var csharpNormalizedStartColumn = lang == "csharp" - ? SkipWhitespace(patternMatchLine, absoluteStartColumn) - : absoluteStartColumn; - var csharpGateRawStartColumn = csharpNormalizedStartColumn; - if (lang == "csharp" - && csharpMatchLines != null - && ReferenceEquals(patternMatchLine, csharpMatchLines[i])) - { - csharpGateRawStartColumn = TranslateCSharpCollapsedColumnToRaw( - csharpMatchColumnToRaw, - i, - csharpNormalizedStartColumn, - line.Length); - } + // For C#, collapsed-space column (from CollapseCSharpGenericTypeWhitespace) + // has to be translated back to raw-space before it can be compared against + // CSharpTypeBodyScope's per-line transitions, which were built from + // structural (raw) columns. Only translate when the pattern match runs on + // the per-line collapsed string (single-line case); multi-line merged + // candidates use a different composed string whose column domain does not + // line up with a single line's map, so we leave the column alone there to + // preserve pre-existing behavior. Closes #400. + // C# では CollapseCSharpGenericTypeWhitespace で空白を取り除いた列を、 + // structural 行の生列で構築された CSharpTypeBodyScope に渡す前に + // raw 列へ戻す必要がある。複数行を結合した match では単一行の map が + // 使えないため、単一行ケース(per-line collapsed line そのものにマッチした + // 場合)だけ変換する。Closes #400. + var csharpNormalizedStartColumn = lang == "csharp" + ? SkipWhitespace(patternMatchLine, absoluteStartColumn) + : absoluteStartColumn; + var csharpGateRawStartColumn = csharpNormalizedStartColumn; + if (lang == "csharp" + && csharpMatchLines != null + && ReferenceEquals(patternMatchLine, csharpMatchLines[i])) + { + csharpGateRawStartColumn = TranslateCSharpCollapsedColumnToRaw( + csharpMatchColumnToRaw, + i, + csharpNormalizedStartColumn, + line.Length); + } - if (lang == "dart" - && ReferenceEquals(pattern.Regex, DartBareConstConstructorRegex) - && !dartInsideClassBody!.IsInsideClassBodyAt(i)) - { - // Bare `const` constructors need class-body context; otherwise - // `const Widget(key: k)` expressions become phantom symbols. - // bare な `const` コンストラクタは class 本体内でのみ許可する。 - // そうしないと `const Widget(key: k)` の式を phantom symbol にしてしまう。 - lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); - continue; - } + if (lang == "dart" + && ReferenceEquals(pattern.Regex, DartBareConstConstructorRegex) + && !dartInsideClassBody!.IsInsideClassBodyAt(i)) + { + // Bare `const` constructors need class-body context; otherwise + // `const Widget(key: k)` expressions become phantom symbols. + // bare な `const` コンストラクタは class 本体内でのみ許可する。 + // そうしないと `const Widget(key: k)` の式を phantom symbol にしてしまう。 + lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); + continue; + } - // C# candidates that only become visible after string-literal content is - // blanked (for example, code inside an interpolation hole of an outer - // string) must not be emitted as declarations. A real declaration starts in - // root code, not in nested interpolation code. Gate on the raw-line start - // column so exact definition / inspect lookups do not pick up call-site - // fragments from interpolated log strings. Closes #790. - // C# では、外側文字列本文を空白化した結果として見えるようになった候補 - // (例: 補間文字列ホール内のコード)を宣言として emit してはならない。 - // 本物の宣言は root code から始まり、入れ子の補間コードからは始まらない。 - // raw 行上の開始列でゲートし、補間ログ文字列内の呼び出し断片が - // exact definition / inspect に混入しないようにする。Closes #790. - if (lang == "csharp" - && csharpLineStartStates != null - && !IsCSharpRootCodePosition(line, csharpLineStartStates[i], csharpGateRawStartColumn)) - { - lineOffset = FindNextSameLineBraceStatementStart( - matchLine, - absoluteStartColumn + Math.Max(1, match.Length), - lang); - continue; - } + // C# candidates that only become visible after string-literal content is + // blanked (for example, code inside an interpolation hole of an outer + // string) must not be emitted as declarations. A real declaration starts in + // root code, not in nested interpolation code. Gate on the raw-line start + // column so exact definition / inspect lookups do not pick up call-site + // fragments from interpolated log strings. Closes #790. + // C# では、外側文字列本文を空白化した結果として見えるようになった候補 + // (例: 補間文字列ホール内のコード)を宣言として emit してはならない。 + // 本物の宣言は root code から始まり、入れ子の補間コードからは始まらない。 + // raw 行上の開始列でゲートし、補間ログ文字列内の呼び出し断片が + // exact definition / inspect に混入しないようにする。Closes #790. + if (lang == "csharp" + && csharpLineStartStates != null + && !IsCSharpRootCodePosition(line, csharpLineStartStates[i], csharpGateRawStartColumn)) + { + lineOffset = FindNextSameLineBraceStatementStart( + matchLine, + absoluteStartColumn + Math.Max(1, match.Length), + lang); + continue; + } - if (lang == "csharp" - && pattern.Kind == "function" - && HasCSharpTokenBeforeIndex(matchLine, "when", absoluteStartColumn + match.Groups["name"].Index)) - { - lineOffset = absoluteStartColumn + Math.Max(1, match.Length); - continue; - } - if (lang == "csharp" - && pattern.BodyStyle == BodyStyle.None - && (pattern.Kind == "property" || IsCSharpFieldLikeFunctionPattern(pattern)) - && csharpInsideTypeBody != null - && !csharpInsideTypeBody.IsInsideTypeBodyAt(i, csharpGateRawStartColumn)) - { - // Move the cursor past this same-line candidate so a later - // column on the same line (e.g. a real field that lives after - // a same-line method body or similar non-type-body scope) can - // still be evaluated against its own column-aware scope. - // Without this advance, the outer `while` would exit the line - // entirely on the first rejection and drop any following match. - // 同一行に続く別候補(例: 同一行の method 本体など非型本体の - // 後ろにある実フィールド)を取りこぼさないよう、次の候補探索 - // 位置へ進める。この進行が無いと最初の拒否で while ループが - // 行を抜けてしまい、後続候補が失われる。Closes #400. - lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); - continue; - } - if (lang == "csharp" - && pattern.BodyStyle == BodyStyle.None - && (pattern.Kind == "property" || IsCSharpFieldLikeFunctionPattern(pattern)) - && IsInsidePreviouslyEmittedCSharpMemberBody(lines, symbols, i + 1, csharpGateRawStartColumn)) - { - // Brace-based type-body scope tracking correctly rejects locals inside - // block bodies, but multi-line expression-bodied members have no brace - // transition for their continuation lines. Without an additional guard, - // those later lines can still match the plain-field regex and emit - // phantom `property` rows like `Red` from `value is\n Red\n or Red;`. - // Only reject lines after the member's declaration line so same-line - // siblings such as `int M() => 0; int X;` keep working through the - // existing column-aware scope gate. Closes #779. - // brace ベースの型本体スコープ追跡は block body 内の local を弾けるが、 - // 複数行の式本体メンバーには continuation 行用の brace 遷移が無い。 - // そのため追加ガードが無いと `value is\n Red\n or Red;` の後続行が - // plain-field regex にマッチして `property Red` の phantom を出してしまう。 - // `int M() => 0; int X;` のような same-line sibling は既存の列単位 - // ゲートで扱えるよう、宣言行そのものではなく後続行だけを拒否する。 - // Closes #779. - lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); - continue; - } - if (lang == "rust" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None - && pattern.ReturnTypeGroup != null - && !IsRustDirectTraitBodyMember(symbols, i + 1)) - { - break; - } - var rawReturnType = NormalizeCSharpImplicitPartialMethodReturnType( - lang, - pattern, - match, - TryGetGroup(match, pattern.ReturnTypeGroup)); - if (lang == "csharp" - && pattern.ReturnTypeGroup != null - && HasInvalidCSharpReturnTypeSuffix(rawReturnType)) - { - lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); - continue; - } - if (lang == "csharp" - && pattern.Kind == "function" - && HasCSharpTokenBeforeIndex(matchLine, "when", absoluteStartColumn + match.Groups["name"].Index)) - { - lineOffset = absoluteStartColumn + Math.Max(1, match.Length); - continue; - } - if (lang == "csharp" - && pattern.Kind == "property" - && IsStandaloneCSharpAccessorCandidate(patternMatchLine)) - { - lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); - continue; - } - if (privateScopeColumns != null - && pattern.Kind == "class" - && IsJavaScriptTypeScriptMatchInPrivateScope(privateScopeColumns, i, absoluteStartColumn, matchLine, includeBlockScope: true)) - { - if (lang is "javascript" or "typescript") + if (lang == "csharp" + && pattern.Kind == "function" + && HasCSharpTokenBeforeIndex(matchLine, "when", absoluteStartColumn + match.Groups["name"].Index)) { - var skippedEndColumn = pattern.BodyStyle == BodyStyle.Brace - ? FindJavaScriptTypeScriptSameLineBraceEndColumn(line, absoluteStartColumn, lang) - : -1; - lineOffset = skippedEndColumn >= absoluteStartColumn - ? FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, skippedEndColumn + 1) - : FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, absoluteStartColumn + Math.Max(1, match.Length)); + lineOffset = absoluteStartColumn + Math.Max(1, match.Length); continue; } + if (lang == "csharp" + && pattern.BodyStyle == BodyStyle.None + && (pattern.Kind == "property" || IsCSharpFieldLikeFunctionPattern(pattern)) + && csharpInsideTypeBody != null + && !csharpInsideTypeBody.IsInsideTypeBodyAt(i, csharpGateRawStartColumn)) + { + // Move the cursor past this same-line candidate so a later + // column on the same line (e.g. a real field that lives after + // a same-line method body or similar non-type-body scope) can + // still be evaluated against its own column-aware scope. + // Without this advance, the outer `while` would exit the line + // entirely on the first rejection and drop any following match. + // 同一行に続く別候補(例: 同一行の method 本体など非型本体の + // 後ろにある実フィールド)を取りこぼさないよう、次の候補探索 + // 位置へ進める。この進行が無いと最初の拒否で while ループが + // 行を抜けてしまい、後続候補が失われる。Closes #400. + lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); + continue; + } + if (lang == "csharp" + && pattern.BodyStyle == BodyStyle.None + && (pattern.Kind == "property" || IsCSharpFieldLikeFunctionPattern(pattern)) + && IsInsidePreviouslyEmittedCSharpMemberBody(lines, symbols, i + 1, csharpGateRawStartColumn)) + { + // Brace-based type-body scope tracking correctly rejects locals inside + // block bodies, but multi-line expression-bodied members have no brace + // transition for their continuation lines. Without an additional guard, + // those later lines can still match the plain-field regex and emit + // phantom `property` rows like `Red` from `value is\n Red\n or Red;`. + // Only reject lines after the member's declaration line so same-line + // siblings such as `int M() => 0; int X;` keep working through the + // existing column-aware scope gate. Closes #779. + // brace ベースの型本体スコープ追跡は block body 内の local を弾けるが、 + // 複数行の式本体メンバーには continuation 行用の brace 遷移が無い。 + // そのため追加ガードが無いと `value is\n Red\n or Red;` の後続行が + // plain-field regex にマッチして `property Red` の phantom を出してしまう。 + // `int M() => 0; int X;` のような same-line sibling は既存の列単位 + // ゲートで扱えるよう、宣言行そのものではなく後続行だけを拒否する。 + // Closes #779. + lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); + continue; + } + if (lang == "rust" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None + && pattern.ReturnTypeGroup != null + && !IsRustDirectTraitBodyMember(symbols, i + 1)) + { + break; + } + var rawReturnType = NormalizeCSharpImplicitPartialMethodReturnType( + lang, + pattern, + match, + TryGetGroup(match, pattern.ReturnTypeGroup)); + if (lang == "csharp" + && pattern.ReturnTypeGroup != null + && HasInvalidCSharpReturnTypeSuffix(rawReturnType)) + { + lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); + continue; + } + if (lang == "csharp" + && pattern.Kind == "function" + && HasCSharpTokenBeforeIndex(matchLine, "when", absoluteStartColumn + match.Groups["name"].Index)) + { + lineOffset = absoluteStartColumn + Math.Max(1, match.Length); + continue; + } + if (lang == "csharp" + && pattern.Kind == "property" + && IsStandaloneCSharpAccessorCandidate(patternMatchLine)) + { + lineOffset = FindNextSameLineBraceStatementStart(matchLine, absoluteStartColumn + Math.Max(1, match.Length), lang); + continue; + } + if (privateScopeColumns != null + && pattern.Kind == "class" + && IsJavaScriptTypeScriptMatchInPrivateScope(privateScopeColumns, i, absoluteStartColumn, matchLine, includeBlockScope: true)) + { + if (lang is "javascript" or "typescript") + { + var skippedEndColumn = pattern.BodyStyle == BodyStyle.Brace + ? FindJavaScriptTypeScriptSameLineBraceEndColumn(line, absoluteStartColumn, lang) + : -1; + lineOffset = skippedEndColumn >= absoluteStartColumn + ? FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, skippedEndColumn + 1) + : FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, absoluteStartColumn + Math.Max(1, match.Length)); + continue; + } - break; - } + break; + } - if (privateScopeColumns != null - && pattern.Kind == "class" - && TryGetGroup(match, pattern.VisibilityGroup) != "export" - && IsJavaScriptTypeScriptMatchInNamespaceScope(privateScopeColumns, i, absoluteStartColumn, matchLine)) - { - if (lang is "javascript" or "typescript") + if (privateScopeColumns != null + && pattern.Kind == "class" + && TryGetGroup(match, pattern.VisibilityGroup) != "export" + && IsJavaScriptTypeScriptMatchInNamespaceScope(privateScopeColumns, i, absoluteStartColumn, matchLine)) { - var skippedEndColumn = pattern.BodyStyle == BodyStyle.Brace - ? FindJavaScriptTypeScriptSameLineBraceEndColumn(line, absoluteStartColumn, lang) - : -1; - lineOffset = skippedEndColumn >= absoluteStartColumn - ? FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, skippedEndColumn + 1) - : FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, absoluteStartColumn + Math.Max(1, match.Length)); - continue; + if (lang is "javascript" or "typescript") + { + var skippedEndColumn = pattern.BodyStyle == BodyStyle.Brace + ? FindJavaScriptTypeScriptSameLineBraceEndColumn(line, absoluteStartColumn, lang) + : -1; + lineOffset = skippedEndColumn >= absoluteStartColumn + ? FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, skippedEndColumn + 1) + : FindNextJavaScriptTypeScriptStatementStart(patternMatchLine, absoluteStartColumn + Math.Max(1, match.Length)); + continue; + } + + break; } - break; - } + var name = match.Groups["name"].Success + ? match.Groups["name"].Value.Trim() + : match.Value.Trim(); + name = NormalizeExtractedSymbolName(lang, name, match, matchLine); + if (pattern.Kind == "import" && lang is "javascript" or "typescript") + name = ResolveJavaScriptTypeScriptModuleSpecifier(lang, filePath, projectRoot, name); + var rubyAttrNames = lang == "ruby" + && pattern.Kind == "property" + ? TryExpandRubyAttrDeclaratorList(patternMatchLine, absoluteStartColumn, match, name) + : null; - var name = match.Groups["name"].Success - ? match.Groups["name"].Value.Trim() - : match.Value.Trim(); - name = NormalizeExtractedSymbolName(lang, name, match, matchLine); - if (pattern.Kind == "import" && lang is "javascript" or "typescript") - name = ResolveJavaScriptTypeScriptModuleSpecifier(lang, filePath, projectRoot, name); - var rubyAttrNames = lang == "ruby" - && pattern.Kind == "property" - ? TryExpandRubyAttrDeclaratorList(patternMatchLine, absoluteStartColumn, match, name) - : null; - - var rangeLines = lang == "css" && cssScannerLines != null - ? cssScannerLines - : structuralLines; - var scalaBracelessClassEndLine = lang == "scala" && pattern.Kind == "class" - ? TryFindScalaBracelessClassEndLine(lines, i, absoluteStartColumn) - : null; - var (endLine, bodyStartLine, bodyEndLine) = lang is "kotlin" or "scala" - && pattern.Kind == "function" - && TryFindKotlinScalaExpressionBodyEndLine(line, absoluteStartColumn) - ? (i + 1, null, null) - : scalaBracelessClassEndLine.HasValue - ? (scalaBracelessClassEndLine.Value + 1, null, null) - : lang == "csharp" && pattern.BodyStyle == BodyStyle.Brace && csharpMatchLines != null - ? FindCSharpBraceRange(csharpMatchLines, i, absoluteStartColumn, linesAreSanitized: true) - : ResolveRange(rangeLines, i, pattern.BodyStyle, lang, absoluteStartColumn); - if (fortranContinuationCandidate != null) - endLine = Math.Max(endLine, fortranContinuationCandidate.Value.LastConsumedLineIndex + 1); - var startLine = i + 1; - if (lang == "csharp" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None - && csharpPropertyCandidate.ExpressionBodyEndLineIndex.HasValue) - { - endLine = Math.Max(endLine, csharpPropertyCandidate.ExpressionBodyEndLineIndex.Value + 1); - } + var rangeLines = lang == "css" && cssScannerLines != null + ? cssScannerLines + : structuralLines; + var scalaBracelessClassEndLine = lang == "scala" && pattern.Kind == "class" + ? TryFindScalaBracelessClassEndLine(lines, i, absoluteStartColumn) + : null; + var (endLine, bodyStartLine, bodyEndLine) = lang is "kotlin" or "scala" + && pattern.Kind == "function" + && TryFindKotlinScalaExpressionBodyEndLine(line, absoluteStartColumn) + ? (i + 1, null, null) + : scalaBracelessClassEndLine.HasValue + ? (scalaBracelessClassEndLine.Value + 1, null, null) + : lang == "csharp" && pattern.BodyStyle == BodyStyle.Brace && csharpMatchLines != null + ? FindCSharpBraceRange(csharpMatchLines, i, absoluteStartColumn, linesAreSanitized: true) + : ResolveRange(rangeLines, i, pattern.BodyStyle, lang, absoluteStartColumn); + if (fortranContinuationCandidate != null) + endLine = Math.Max(endLine, fortranContinuationCandidate.Value.LastConsumedLineIndex + 1); + var startLine = i + 1; + if (lang == "csharp" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None + && csharpPropertyCandidate.ExpressionBodyEndLineIndex.HasValue) + { + endLine = Math.Max(endLine, csharpPropertyCandidate.ExpressionBodyEndLineIndex.Value + 1); + } - // Python @property decorator: reclassify the def as property - // Python @property デコレータ: def を property に再分類 - var kind = pattern.Kind; - string? pythonSubKind = null; - if (kind == "function" && lang == "python" && HasPythonPropertyDecorator(lines, i)) - { - kind = "property"; - pythonSubKind = GetPythonPropertyAccessorSubKind(lines, i); - } - else if (kind == "function" && lang == "python" && IsPythonClassHook(name)) - { - kind = "class_hook"; - pythonSubKind = "dunder"; - (endLine, bodyStartLine, bodyEndLine) = FindPythonIndentedBodyRange(lines, i); - } - else if (kind == "function" && lang is "javascript" or "typescript") - { - kind = ResolveJavaScriptTypeScriptFunctionKind( - TryGetGroup(match, "async") != null, - TryGetGroup(match, "generator") != null); - } + // Python @property decorator: reclassify the def as property + // Python @property デコレータ: def を property に再分類 + var kind = pattern.Kind; + string? pythonSubKind = null; + if (kind == "function" && lang == "python" && HasPythonPropertyDecorator(lines, i)) + { + kind = "property"; + pythonSubKind = GetPythonPropertyAccessorSubKind(lines, i); + } + else if (kind == "function" && lang == "python" && IsPythonClassHook(name)) + { + kind = "class_hook"; + pythonSubKind = "dunder"; + (endLine, bodyStartLine, bodyEndLine) = FindPythonIndentedBodyRange(lines, i); + } + else if (kind == "function" && lang is "javascript" or "typescript") + { + kind = ResolveJavaScriptTypeScriptFunctionKind( + TryGetGroup(match, "async") != null, + TryGetGroup(match, "generator") != null); + } - if (lang == "css") - name = ResolveCssSymbolName(matchLine[absoluteStartColumn..], name, lines, i, endLine); + if (lang == "css") + name = ResolveCssSymbolName(matchLine[absoluteStartColumn..], name, lines, i, endLine); - if (lang == "css" && string.IsNullOrWhiteSpace(name)) - { - var skippedEndColumn = pattern.BodyStyle == BodyStyle.Brace + if (lang == "css" && string.IsNullOrWhiteSpace(name)) + { + var skippedEndColumn = pattern.BodyStyle == BodyStyle.Brace + && bodyEndLine == startLine + ? FindSameLineBraceEndColumn(line, absoluteStartColumn, lang, kind) + : -1; + if (skippedEndColumn >= absoluteStartColumn) + { + lineOffset = FindNextSameLineBraceStatementStart(matchLine, skippedEndColumn + 1, lang); + continue; + } + + stopAfterFirstPatternMatch = true; + break; + } + + var csharpSingleLineCollapsedMatch = lang == "csharp" + && csharpMatchLines != null + && ReferenceEquals(patternMatchLine, csharpMatchLines[i]); + var csharpSignatureRawStartColumn = csharpGateRawStartColumn; + var csharpSameLineBraceStartColumn = csharpSingleLineCollapsedMatch + ? absoluteStartColumn + : csharpSignatureRawStartColumn; + var sameLineEndColumn = pattern.BodyStyle == BodyStyle.Brace && bodyEndLine == startLine - ? FindSameLineBraceEndColumn(line, absoluteStartColumn, lang, kind) + ? (lang == "csharp" && csharpSingleLineCollapsedMatch + ? FindCSharpSameLineBraceEndColumnFromSanitized(patternMatchLine, csharpSameLineBraceStartColumn) + : FindSameLineBraceEndColumn(line, csharpSameLineBraceStartColumn, lang, kind)) : -1; - if (skippedEndColumn >= absoluteStartColumn) + var sameLineEndUsesRawColumns = pattern.BodyStyle == BodyStyle.Brace + && bodyEndLine == startLine + && !(lang == "csharp" && csharpSingleLineCollapsedMatch); + if (lang == "csharp" + && csharpSingleLineCollapsedMatch + && CanUseCSharpSameLineSemicolonEndColumn(kind)) { - lineOffset = FindNextSameLineBraceStatementStart(matchLine, skippedEndColumn + 1, lang); - continue; + var semicolonEndColumn = FindCSharpSameLineSemicolonEndColumn(patternMatchLine, absoluteStartColumn); + if (semicolonEndColumn >= absoluteStartColumn + && (sameLineEndColumn < absoluteStartColumn || semicolonEndColumn < sameLineEndColumn)) + { + sameLineEndColumn = semicolonEndColumn; + sameLineEndUsesRawColumns = false; + } } - - stopAfterFirstPatternMatch = true; - break; - } - - var csharpSingleLineCollapsedMatch = lang == "csharp" - && csharpMatchLines != null - && ReferenceEquals(patternMatchLine, csharpMatchLines[i]); - var csharpSignatureRawStartColumn = csharpGateRawStartColumn; - var csharpSameLineBraceStartColumn = csharpSingleLineCollapsedMatch - ? absoluteStartColumn - : csharpSignatureRawStartColumn; - var sameLineEndColumn = pattern.BodyStyle == BodyStyle.Brace - && bodyEndLine == startLine - ? (lang == "csharp" && csharpSingleLineCollapsedMatch - ? FindCSharpSameLineBraceEndColumnFromSanitized(patternMatchLine, csharpSameLineBraceStartColumn) - : FindSameLineBraceEndColumn(line, csharpSameLineBraceStartColumn, lang, kind)) - : -1; - var sameLineEndUsesRawColumns = pattern.BodyStyle == BodyStyle.Brace - && bodyEndLine == startLine - && !(lang == "csharp" && csharpSingleLineCollapsedMatch); - if (lang == "csharp" - && csharpSingleLineCollapsedMatch - && CanUseCSharpSameLineSemicolonEndColumn(kind)) - { - var semicolonEndColumn = FindCSharpSameLineSemicolonEndColumn(patternMatchLine, absoluteStartColumn); - if (semicolonEndColumn >= absoluteStartColumn - && (sameLineEndColumn < absoluteStartColumn || semicolonEndColumn < sameLineEndColumn)) + if (lang == "csharp" + && kind == "event" + && pattern.BodyStyle == BodyStyle.None + && HasCSharpEventAccessorStart(patternMatchLine[absoluteStartColumn..])) + { + // Same-line accessor events (`event E { add {} remove {} }`) share the + // sibling-stream requirement with semicolon-bodied members: their + // signature must stop at the accessor block so later same-line siblings + // can restart the full pattern scan. Without this brace clamp, the + // stored event signature swallows the following declaration and the + // later sibling never reaches earlier patterns such as property. + // Closes #520. + // 同一行 accessor event (`event E { add {} remove {} }`) も semicolon 系 + // member と同様に sibling stream として扱う必要がある。そのため + // accessor block の閉じ `}` で signature を切り、後続の same-line + // sibling が property など先頭側 pattern へ再到達できるようにする。 + // これが無いと event signature が後続宣言を飲み込み、後続 sibling が + // earlier pattern に届かない。Closes #520. + var braceEndColumn = csharpSingleLineCollapsedMatch + ? FindCSharpSameLineBraceEndColumnFromSanitized(patternMatchLine, csharpSameLineBraceStartColumn) + : FindSameLineBraceEndColumn(line, csharpSameLineBraceStartColumn, lang, kind); + if (braceEndColumn >= absoluteStartColumn + && (sameLineEndColumn < absoluteStartColumn || braceEndColumn < sameLineEndColumn)) + { + sameLineEndColumn = braceEndColumn; + sameLineEndUsesRawColumns = !(lang == "csharp" && csharpSingleLineCollapsedMatch); + } + } + if (sameLineEndColumn < absoluteStartColumn + && lang == "csharp" + && kind == "enum" + && pattern.BodyStyle == BodyStyle.None) { - sameLineEndColumn = semicolonEndColumn; + sameLineEndColumn = FindCSharpSameLineEnumMemberEndColumn(patternMatchLine, absoluteStartColumn); sameLineEndUsesRawColumns = false; } - } - if (lang == "csharp" - && kind == "event" - && pattern.BodyStyle == BodyStyle.None - && HasCSharpEventAccessorStart(patternMatchLine[absoluteStartColumn..])) - { - // Same-line accessor events (`event E { add {} remove {} }`) share the - // sibling-stream requirement with semicolon-bodied members: their - // signature must stop at the accessor block so later same-line siblings - // can restart the full pattern scan. Without this brace clamp, the - // stored event signature swallows the following declaration and the - // later sibling never reaches earlier patterns such as property. - // Closes #520. - // 同一行 accessor event (`event E { add {} remove {} }`) も semicolon 系 - // member と同様に sibling stream として扱う必要がある。そのため - // accessor block の閉じ `}` で signature を切り、後続の same-line - // sibling が property など先頭側 pattern へ再到達できるようにする。 - // これが無いと event signature が後続宣言を飲み込み、後続 sibling が - // earlier pattern に届かない。Closes #520. - var braceEndColumn = csharpSingleLineCollapsedMatch - ? FindCSharpSameLineBraceEndColumnFromSanitized(patternMatchLine, csharpSameLineBraceStartColumn) - : FindSameLineBraceEndColumn(line, csharpSameLineBraceStartColumn, lang, kind); - if (braceEndColumn >= absoluteStartColumn - && (sameLineEndColumn < absoluteStartColumn || braceEndColumn < sameLineEndColumn)) + string signature; + if (csharpWrappedModifierPrefix != null) { - sameLineEndColumn = braceEndColumn; - sameLineEndUsesRawColumns = !(lang == "csharp" && csharpSingleLineCollapsedMatch); + // Wrapped ctor signature: prepend the modifier prefix recovered from + // preceding modifier-only lines so the stored signature reflects the + // full declaration (`static Foo() { ... }`) rather than only the name + // line. Honor the same-line brace body truncation when present so the + // signature does not absorb the entire ctor body. Closes #348. + // ラップされたコンストラクタのシグネチャ: 直前のモディファイアのみ行から + // 復元した prefix を付与し、識別子行だけでなく宣言全体 + // (`static Foo() { ... }`) を保存する。同一行に brace 本体が閉じる + // ケースではその末尾で切り詰め、シグネチャが本体全体を飲み込まない + // ようにする。Closes #348. + var nameLineStartColumn = csharpSingleLineCollapsedMatch + ? (sameLineEndUsesRawColumns + ? csharpSignatureRawStartColumn + : csharpSignatureRawStartColumn) + : absoluteStartColumn; + var nameLineEndExclusive = sameLineEndColumn >= absoluteStartColumn + ? (sameLineEndUsesRawColumns + ? Math.Min(sameLineEndColumn + 1, line.Length) + : Math.Min( + TranslateCSharpCollapsedColumnToRaw( + csharpMatchColumnToRaw, + i, + sameLineEndColumn, + line.Length) + 1, + line.Length)) + : line.Length; + var nameLineContent = sameLineEndColumn >= absoluteStartColumn + ? line[nameLineStartColumn..nameLineEndExclusive] + : line[nameLineStartColumn..]; + signature = (csharpWrappedModifierPrefix + " " + nameLineContent.TrimStart()).Trim(); } - } - if (sameLineEndColumn < absoluteStartColumn - && lang == "csharp" - && kind == "enum" - && pattern.BodyStyle == BodyStyle.None) - { - sameLineEndColumn = FindCSharpSameLineEnumMemberEndColumn(patternMatchLine, absoluteStartColumn); - sameLineEndUsesRawColumns = false; - } - string signature; - if (csharpWrappedModifierPrefix != null) - { - // Wrapped ctor signature: prepend the modifier prefix recovered from - // preceding modifier-only lines so the stored signature reflects the - // full declaration (`static Foo() { ... }`) rather than only the name - // line. Honor the same-line brace body truncation when present so the - // signature does not absorb the entire ctor body. Closes #348. - // ラップされたコンストラクタのシグネチャ: 直前のモディファイアのみ行から - // 復元した prefix を付与し、識別子行だけでなく宣言全体 - // (`static Foo() { ... }`) を保存する。同一行に brace 本体が閉じる - // ケースではその末尾で切り詰め、シグネチャが本体全体を飲み込まない - // ようにする。Closes #348. - var nameLineStartColumn = csharpSingleLineCollapsedMatch - ? (sameLineEndUsesRawColumns - ? csharpSignatureRawStartColumn - : csharpSignatureRawStartColumn) - : absoluteStartColumn; - var nameLineEndExclusive = sameLineEndColumn >= absoluteStartColumn - ? (sameLineEndUsesRawColumns - ? Math.Min(sameLineEndColumn + 1, line.Length) - : Math.Min( - TranslateCSharpCollapsedColumnToRaw( + else if (sameLineEndColumn >= absoluteStartColumn) + { + if (lang == "csharp" + && csharpSingleLineCollapsedMatch) + { + var rawStart = csharpSignatureRawStartColumn; + var rawEndInclusive = sameLineEndUsesRawColumns + ? sameLineEndColumn + : TranslateCSharpCollapsedColumnToRaw( csharpMatchColumnToRaw, i, sameLineEndColumn, - line.Length) + 1, - line.Length)) - : line.Length; - var nameLineContent = sameLineEndColumn >= absoluteStartColumn - ? line[nameLineStartColumn..nameLineEndExclusive] - : line[nameLineStartColumn..]; - signature = (csharpWrappedModifierPrefix + " " + nameLineContent.TrimStart()).Trim(); - } - else if (sameLineEndColumn >= absoluteStartColumn) - { - if (lang == "csharp" - && csharpSingleLineCollapsedMatch) + line.Length); + var rawEndExclusive = Math.Min(rawEndInclusive + 1, line.Length); + if (rawStart > line.Length) + rawStart = line.Length; + if (rawEndExclusive <= rawStart) + rawEndExclusive = Math.Min(rawStart + Math.Max(1, match.Length), line.Length); + signature = line[rawStart..rawEndExclusive].Trim(); + } + else + { + var signatureStartColumn = csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns + ? csharpSignatureRawStartColumn + : absoluteStartColumn; + var signatureEndExclusive = Math.Min(sameLineEndColumn + 1, line.Length); + if (signatureEndExclusive <= signatureStartColumn) + signatureEndExclusive = Math.Min(signatureStartColumn + Math.Max(1, match.Length), line.Length); + signature = line[signatureStartColumn..signatureEndExclusive].Trim(); + } + } + else if (lang == "csharp" + && pattern.BodyStyle == BodyStyle.None + && TryFindCSharpSemicolonTerminatedSignatureExtent( + lines, + i, + csharpGateRawStartColumn, + out var csharpFieldSignatureLastLineIndex, + out var csharpFieldSignatureLastLineExclusiveEndColumn) + && csharpFieldSignatureLastLineIndex > i) { - var rawStart = csharpSignatureRawStartColumn; - var rawEndInclusive = sameLineEndUsesRawColumns - ? sameLineEndColumn - : TranslateCSharpCollapsedColumnToRaw( - csharpMatchColumnToRaw, - i, - sameLineEndColumn, - line.Length); - var rawEndExclusive = Math.Min(rawEndInclusive + 1, line.Length); - if (rawStart > line.Length) - rawStart = line.Length; - if (rawEndExclusive <= rawStart) - rawEndExclusive = Math.Min(rawStart + Math.Max(1, match.Length), line.Length); - signature = line[rawStart..rawEndExclusive].Trim(); + signature = BuildCSharpMultilineSignature( + lines, + i, + csharpGateRawStartColumn, + csharpFieldSignatureLastLineIndex, + csharpFieldSignatureLastLineExclusiveEndColumn); } - else + else if (lang == "csharp" + && pattern.BodyStyle == BodyStyle.Brace + && IsCSharpMultilineExpressionBodiedMember( + lines, + i, + csharpSignatureRawStartColumn) + && TryFindCSharpSemicolonTerminatedSignatureExtent( + lines, + i, + csharpSignatureRawStartColumn, + out var csharpSemicolonSignatureLastLineIndex, + out var csharpSemicolonSignatureLastLineExclusiveEndColumn) + && csharpSemicolonSignatureLastLineIndex > i) { - var signatureStartColumn = csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns - ? csharpSignatureRawStartColumn - : absoluteStartColumn; - var signatureEndExclusive = Math.Min(sameLineEndColumn + 1, line.Length); - if (signatureEndExclusive <= signatureStartColumn) - signatureEndExclusive = Math.Min(signatureStartColumn + Math.Max(1, match.Length), line.Length); - signature = line[signatureStartColumn..signatureEndExclusive].Trim(); + signature = BuildCSharpMultilineSignature( + lines, + i, + csharpSignatureRawStartColumn, + csharpSemicolonSignatureLastLineIndex, + csharpSemicolonSignatureLastLineExclusiveEndColumn); } - } - else if (lang == "csharp" - && pattern.BodyStyle == BodyStyle.None - && TryFindCSharpSemicolonTerminatedSignatureExtent( - lines, - i, - csharpGateRawStartColumn, - out var csharpFieldSignatureLastLineIndex, - out var csharpFieldSignatureLastLineExclusiveEndColumn) - && csharpFieldSignatureLastLineIndex > i) - { - signature = BuildCSharpMultilineSignature( - lines, - i, - csharpGateRawStartColumn, - csharpFieldSignatureLastLineIndex, - csharpFieldSignatureLastLineExclusiveEndColumn); - } - else if (lang == "csharp" - && pattern.BodyStyle == BodyStyle.Brace - && IsCSharpMultilineExpressionBodiedMember( - lines, - i, - csharpSignatureRawStartColumn) - && TryFindCSharpSemicolonTerminatedSignatureExtent( - lines, - i, - csharpSignatureRawStartColumn, - out var csharpSemicolonSignatureLastLineIndex, - out var csharpSemicolonSignatureLastLineExclusiveEndColumn) - && csharpSemicolonSignatureLastLineIndex > i) - { - signature = BuildCSharpMultilineSignature( - lines, - i, - csharpSignatureRawStartColumn, - csharpSemicolonSignatureLastLineIndex, - csharpSemicolonSignatureLastLineExclusiveEndColumn); - } - else if (lang == "csharp" && csharpPropertyCandidate.LastConsumedLineIndex > i) - { - signature = BuildCSharpMultilineSignature( - lines, - i, - csharpSignatureRawStartColumn, - csharpPropertyCandidate.SignatureLastLineIndex, - csharpPropertyCandidate.SignatureLastLineExclusiveEndColumn); - } - else if (lang == "csharp" - && pattern.Kind is "class" or "struct" or "interface" or "enum" - && TryFindCSharpTypeHeaderExtent( - lines, - i, - csharpSignatureRawStartColumn, - out var csharpTypeHeaderLastLineIndex, - out var csharpTypeHeaderLastLineExclusiveEndColumn) - && csharpTypeHeaderLastLineIndex > i) - { - // Wrapped C# type header: base list and `where` clauses often continue - // onto following lines before the body-opening `{` or primary-ctor `;`. - // Join them so consumers like ReferenceExtractor can resolve the base - // type from the stored signature instead of silently treating the class - // as having no base. Uses the comment-stripping variant so trailing or - // interleaved `//` / `/* */` comments do not leak into the signature. - // Closes #382. - // 折り返された C# 型ヘッダ: base リストや `where` 句は本体開きの `{` - // または primary-ctor 終端の `;` までに複数行へまたがることが多い。 - // 継続行を連結して保存し、ReferenceExtractor などが保存済み - // シグネチャから base 型を解決できるようにする。末尾や途中に混じる - // `//` / `/* */` コメントを signature から除去する variant を使う。 - // Closes #382. - signature = BuildCSharpTypeHeaderSignature( - lines, - i, - csharpSignatureRawStartColumn, - csharpTypeHeaderLastLineIndex, - csharpTypeHeaderLastLineExclusiveEndColumn); - } - else if (lang == "csharp" - && pattern.Kind is "event" or "delegate" - && pattern.BodyStyle == BodyStyle.None) - { - // Same-line C# semicolon-style declarations such as - // `event EventHandler E; }` or `delegate void D(); }` must stop at the - // declaration terminator instead of absorbing the enclosing type's - // closing brace into the stored signature. Reuse the same statement-end - // scanner as plain fields so nested `{}` inside accessor-style events - // still stay balanced while the outer `}` remains excluded. - // Closes #473 follow-up. - // `event EventHandler E; }` や `delegate void D(); }` のような - // 同一行 C# のセミコロン終端宣言は、囲む型本体の `}` を signature に - // 含めてはならない。plain field と同じ statement-end scanner を再利用し、 - // アクセサ式 event 内部の `{}` は釣り合いを保ったまま、外側 `}` だけを - // 除外する。Closes #473 follow-up. - var statementEnd = FindCSharpSameLineStatementEnd(patternMatchLine, absoluteStartColumn); - if (statementEnd > line.Length) - statementEnd = line.Length; - if (statementEnd <= absoluteStartColumn) - statementEnd = Math.Min(absoluteStartColumn + Math.Max(1, match.Length), line.Length); - signature = line[absoluteStartColumn..statementEnd].Trim(); - } - else if (lang == "java" - && pattern.BodyStyle == BodyStyle.Brace - && bodyStartLine == null) - { - var statementEnd = FindJavaSameLineStatementEnd(line, absoluteStartColumn); - if (statementEnd > line.Length) - statementEnd = line.Length; - if (statementEnd <= absoluteStartColumn) - statementEnd = Math.Min(absoluteStartColumn + Math.Max(1, match.Length), line.Length); - signature = line[absoluteStartColumn..statementEnd].Trim(); - } - else if (lang == "csharp" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None) - { - // For a plain C# field (kind `property`, BodyStyle.None), clamp the - // signature to the end of the field's declaration statement (the - // terminating `;`, or — if an unbalanced `}` from a same-line - // enclosing type body is hit first — the position of that `}`). - // This keeps initializer-backed fields such as - // `private int _x = 42;` carrying a full `private int _x = 42;` - // signature instead of being truncated at `=`, and still prevents - // `public int X; } }` inside a same-line nested type from leaking - // the trailing `} }` into X's signature (which would break the - // same-line `ContainsSymbol` check in `AssignContainers` and make - // X attach to `Outer` instead of `Inner`). Closes #400. - // C# の通常フィールド(kind `property`、BodyStyle.None)では、signature を - // 宣言文の終端(`;` まで、または同一行の囲む型本体の閉じ `}` が先に - // 来ればその位置)までで clamp する。`private int _x = 42;` のような - // 初期化子付きフィールドでも signature が `=` で切れず完全に残り、かつ - // `public int X; } }` のような同一行ネスト型内のフィールドでも - // trailing `} }` が signature に混入せず、AssignContainers の - // ContainsSymbol 判定が正しく動いて X が Inner ではなく Outer に - // ぶら下がる事故が起きない。Closes #400. - var statementEnd = FindCSharpSameLineStatementEnd(patternMatchLine, absoluteStartColumn); - if (csharpMatchLines != null - && ReferenceEquals(patternMatchLine, csharpMatchLines[i])) + else if (lang == "csharp" && csharpPropertyCandidate.LastConsumedLineIndex > i) { - // Single-line candidate: translate both endpoints through the - // per-line collapsed→raw column map so the raw slice keeps the - // `;` terminator and does not absorb a phantom leading `;` from - // the next declarator on the same line. Without this, a line like - // `public Dictionary Map = new(); public int B;` - // returned `Map` without `;` and `B` with a leading `;` because - // the collapsed-space endpoints no longer lined up with raw - // character positions. Closes #400. - // 単一行候補では、per-line collapsed→raw map で両端点を raw 列に - // 戻してから slice する。こうしないと、 - // `public Dictionary Map = new(); public int B;` のような行で - // `Map` の終端 `;` が欠け、後続の `B` の先頭に `;` が混入する。Closes #400. - var rawStart = TranslateCSharpCollapsedColumnToRaw( - csharpMatchColumnToRaw, + signature = BuildCSharpMultilineSignature( + lines, i, - absoluteStartColumn, - line.Length); - var rawEnd = TranslateCSharpCollapsedColumnToRaw( - csharpMatchColumnToRaw, + csharpSignatureRawStartColumn, + csharpPropertyCandidate.SignatureLastLineIndex, + csharpPropertyCandidate.SignatureLastLineExclusiveEndColumn); + } + else if (lang == "csharp" + && pattern.Kind is "class" or "struct" or "interface" or "enum" + && TryFindCSharpTypeHeaderExtent( + lines, i, - statementEnd, - line.Length); - if (rawEnd > line.Length) - rawEnd = line.Length; - if (rawStart > line.Length) - rawStart = line.Length; - if (rawEnd <= rawStart) - rawEnd = Math.Min(rawStart + Math.Max(1, match.Length), line.Length); - signature = line[rawStart..rawEnd].Trim(); + csharpSignatureRawStartColumn, + out var csharpTypeHeaderLastLineIndex, + out var csharpTypeHeaderLastLineExclusiveEndColumn) + && csharpTypeHeaderLastLineIndex > i) + { + // Wrapped C# type header: base list and `where` clauses often continue + // onto following lines before the body-opening `{` or primary-ctor `;`. + // Join them so consumers like ReferenceExtractor can resolve the base + // type from the stored signature instead of silently treating the class + // as having no base. Uses the comment-stripping variant so trailing or + // interleaved `//` / `/* */` comments do not leak into the signature. + // Closes #382. + // 折り返された C# 型ヘッダ: base リストや `where` 句は本体開きの `{` + // または primary-ctor 終端の `;` までに複数行へまたがることが多い。 + // 継続行を連結して保存し、ReferenceExtractor などが保存済み + // シグネチャから base 型を解決できるようにする。末尾や途中に混じる + // `//` / `/* */` コメントを signature から除去する variant を使う。 + // Closes #382. + signature = BuildCSharpTypeHeaderSignature( + lines, + i, + csharpSignatureRawStartColumn, + csharpTypeHeaderLastLineIndex, + csharpTypeHeaderLastLineExclusiveEndColumn); } - else + else if (lang == "csharp" + && pattern.Kind is "event" or "delegate" + && pattern.BodyStyle == BodyStyle.None) { + // Same-line C# semicolon-style declarations such as + // `event EventHandler E; }` or `delegate void D(); }` must stop at the + // declaration terminator instead of absorbing the enclosing type's + // closing brace into the stored signature. Reuse the same statement-end + // scanner as plain fields so nested `{}` inside accessor-style events + // still stay balanced while the outer `}` remains excluded. + // Closes #473 follow-up. + // `event EventHandler E; }` や `delegate void D(); }` のような + // 同一行 C# のセミコロン終端宣言は、囲む型本体の `}` を signature に + // 含めてはならない。plain field と同じ statement-end scanner を再利用し、 + // アクセサ式 event 内部の `{}` は釣り合いを保ったまま、外側 `}` だけを + // 除外する。Closes #473 follow-up. + var statementEnd = FindCSharpSameLineStatementEnd(patternMatchLine, absoluteStartColumn); if (statementEnd > line.Length) statementEnd = line.Length; if (statementEnd <= absoluteStartColumn) statementEnd = Math.Min(absoluteStartColumn + Math.Max(1, match.Length), line.Length); signature = line[absoluteStartColumn..statementEnd].Trim(); } - } - else - { - signature = lang == "fortran" - ? patternMatchLine[absoluteStartColumn..].Trim() - : line[absoluteStartColumn..].Trim(); - } - if (lang == "python" && pattern.Kind is "function" or "class") - signature = BuildPythonLogicalHeaderSignature(lines, i, absoluteStartColumn); - - List? fortranProcedureNames = null; - if (lang == "fortran" - && pattern.Kind == "function" - && name.Contains(',') - && signature.Contains("procedure", StringComparison.OrdinalIgnoreCase)) - { - var names = name.Split(','); - for (var index = 0; index < names.Length; index++) - names[index] = names[index].Trim(); - - if (names.Any(static candidate => candidate.Length > 0)) - fortranProcedureNames = names.Where(static candidate => candidate.Length > 0).ToList(); - } + else if (lang == "java" + && pattern.BodyStyle == BodyStyle.Brace + && bodyStartLine == null) + { + var statementEnd = FindJavaSameLineStatementEnd(line, absoluteStartColumn); + if (statementEnd > line.Length) + statementEnd = line.Length; + if (statementEnd <= absoluteStartColumn) + statementEnd = Math.Min(absoluteStartColumn + Math.Max(1, match.Length), line.Length); + signature = line[absoluteStartColumn..statementEnd].Trim(); + } + else if (lang == "csharp" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None) + { + // For a plain C# field (kind `property`, BodyStyle.None), clamp the + // signature to the end of the field's declaration statement (the + // terminating `;`, or — if an unbalanced `}` from a same-line + // enclosing type body is hit first — the position of that `}`). + // This keeps initializer-backed fields such as + // `private int _x = 42;` carrying a full `private int _x = 42;` + // signature instead of being truncated at `=`, and still prevents + // `public int X; } }` inside a same-line nested type from leaking + // the trailing `} }` into X's signature (which would break the + // same-line `ContainsSymbol` check in `AssignContainers` and make + // X attach to `Outer` instead of `Inner`). Closes #400. + // C# の通常フィールド(kind `property`、BodyStyle.None)では、signature を + // 宣言文の終端(`;` まで、または同一行の囲む型本体の閉じ `}` が先に + // 来ればその位置)までで clamp する。`private int _x = 42;` のような + // 初期化子付きフィールドでも signature が `=` で切れず完全に残り、かつ + // `public int X; } }` のような同一行ネスト型内のフィールドでも + // trailing `} }` が signature に混入せず、AssignContainers の + // ContainsSymbol 判定が正しく動いて X が Inner ではなく Outer に + // ぶら下がる事故が起きない。Closes #400. + var statementEnd = FindCSharpSameLineStatementEnd(patternMatchLine, absoluteStartColumn); + if (csharpMatchLines != null + && ReferenceEquals(patternMatchLine, csharpMatchLines[i])) + { + // Single-line candidate: translate both endpoints through the + // per-line collapsed→raw column map so the raw slice keeps the + // `;` terminator and does not absorb a phantom leading `;` from + // the next declarator on the same line. Without this, a line like + // `public Dictionary Map = new(); public int B;` + // returned `Map` without `;` and `B` with a leading `;` because + // the collapsed-space endpoints no longer lined up with raw + // character positions. Closes #400. + // 単一行候補では、per-line collapsed→raw map で両端点を raw 列に + // 戻してから slice する。こうしないと、 + // `public Dictionary Map = new(); public int B;` のような行で + // `Map` の終端 `;` が欠け、後続の `B` の先頭に `;` が混入する。Closes #400. + var rawStart = TranslateCSharpCollapsedColumnToRaw( + csharpMatchColumnToRaw, + i, + absoluteStartColumn, + line.Length); + var rawEnd = TranslateCSharpCollapsedColumnToRaw( + csharpMatchColumnToRaw, + i, + statementEnd, + line.Length); + if (rawEnd > line.Length) + rawEnd = line.Length; + if (rawStart > line.Length) + rawStart = line.Length; + if (rawEnd <= rawStart) + rawEnd = Math.Min(rawStart + Math.Max(1, match.Length), line.Length); + signature = line[rawStart..rawEnd].Trim(); + } + else + { + if (statementEnd > line.Length) + statementEnd = line.Length; + if (statementEnd <= absoluteStartColumn) + statementEnd = Math.Min(absoluteStartColumn + Math.Max(1, match.Length), line.Length); + signature = line[absoluteStartColumn..statementEnd].Trim(); + } + } + else + { + signature = lang == "fortran" + ? patternMatchLine[absoluteStartColumn..].Trim() + : line[absoluteStartColumn..].Trim(); + } + if (lang == "python" && pattern.Kind is "function" or "class") + signature = BuildPythonLogicalHeaderSignature(lines, i, absoluteStartColumn); - if (lang == "cpp" - && IsCppTemplateSpecializationSymbol(kind, name, signature, lines, i)) - { - kind = "specialization"; - } + List? fortranProcedureNames = null; + if (lang == "fortran" + && pattern.Kind == "function" + && name.Contains(',') + && signature.Contains("procedure", StringComparison.OrdinalIgnoreCase)) + { + var names = name.Split(','); + for (var index = 0; index < names.Length; index++) + names[index] = names[index].Trim(); - var suppressJavaStatementSymbol = false; - if (lang == "java" && pattern.Kind == "function") - { - var trimmedSignature = signature.TrimStart(); - suppressJavaStatementSymbol = name == "switch" - || trimmedSignature.StartsWith("return ", StringComparison.Ordinal) - || trimmedSignature.StartsWith("switch ", StringComparison.Ordinal) - || trimmedSignature.StartsWith("case ", StringComparison.Ordinal); - } + if (names.Any(static candidate => candidate.Length > 0)) + fortranProcedureNames = names.Where(static candidate => candidate.Length > 0).ToList(); + } - if (!suppressJavaStatementSymbol) - { - if (lang == "csharp" - && pattern.Kind == "function" - && IsCSharpTestMethod(lines, i)) + if (lang == "cpp" + && IsCppTemplateSpecializationSymbol(kind, name, signature, lines, i)) { - kind = "test.method"; + kind = "specialization"; } - var pythonImportEntries = lang == "python" && pattern.Kind == "import" - ? TryExpandPythonImportSymbols(lines, i, absoluteStartColumn, pythonModulePrefix) - : null; - var declaratorEntries = lang == "csharp" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None - ? TryExpandCSharpFieldDeclaratorList(patternMatchLine, absoluteStartColumn, match, pattern.ReturnTypeGroup, name) - : null; - var swiftEnumCaseEntries = lang == "swift" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None - ? TryExpandSwiftEnumCaseDeclaratorList(patternMatchLine, absoluteStartColumn, match) - : null; - var fortranEnumeratorEntries = lang == "fortran" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None - ? TryExpandFortranEnumeratorDeclaratorList(patternMatchLine, match) - : null; - var fortranParameterEntries = lang == "fortran" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None - ? TryExpandFortranParameterDeclaratorList(patternMatchLine, match) - : null; + var suppressJavaStatementSymbol = false; + if (lang == "java" && pattern.Kind == "function") + { + var trimmedSignature = signature.TrimStart(); + suppressJavaStatementSymbol = name == "switch" + || trimmedSignature.StartsWith("return ", StringComparison.Ordinal) + || trimmedSignature.StartsWith("switch ", StringComparison.Ordinal) + || trimmedSignature.StartsWith("case ", StringComparison.Ordinal); + } - if (pythonImportEntries != null) + if (!suppressJavaStatementSymbol) { - foreach (var entry in pythonImportEntries) + if (lang == "csharp" + && pattern.Kind == "function" + && IsCSharpTestMethod(lines, i)) { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = entry.Name, - Line = startLine, - StartLine = startLine, - StartColumn = entry.StartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, - line); + kind = "test.method"; } - } - else if (declaratorEntries != null) - { - foreach (var entry in declaratorEntries) + + var pythonImportEntries = lang == "python" && pattern.Kind == "import" + ? TryExpandPythonImportSymbols(lines, i, absoluteStartColumn, pythonModulePrefix) + : null; + var declaratorEntries = lang == "csharp" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None + ? TryExpandCSharpFieldDeclaratorList(patternMatchLine, absoluteStartColumn, match, pattern.ReturnTypeGroup, name) + : null; + var swiftEnumCaseEntries = lang == "swift" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None + ? TryExpandSwiftEnumCaseDeclaratorList(patternMatchLine, absoluteStartColumn, match) + : null; + var fortranEnumeratorEntries = lang == "fortran" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None + ? TryExpandFortranEnumeratorDeclaratorList(patternMatchLine, match) + : null; + var fortranParameterEntries = lang == "fortran" + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None + ? TryExpandFortranParameterDeclaratorList(patternMatchLine, match) + : null; + + if (pythonImportEntries != null) { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = entry.Name, - Line = startLine, - StartLine = startLine, - StartColumn = csharpSingleLineCollapsedMatch - ? csharpSignatureRawStartColumn - : absoluteStartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(entry.ReturnType), - }, - line); + foreach (var entry in pythonImportEntries) + { + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = entry.Name, + Line = startLine, + StartLine = startLine, + StartColumn = entry.StartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, + line); + } } - } - else if (swiftEnumCaseEntries != null) - { - foreach (var entry in swiftEnumCaseEntries) + else if (declaratorEntries != null) { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = entry.Name, - Line = startLine, - StartLine = startLine, - StartColumn = entry.StartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(entry.ReturnType), - }, - line); + foreach (var entry in declaratorEntries) + { + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = entry.Name, + Line = startLine, + StartLine = startLine, + StartColumn = csharpSingleLineCollapsedMatch + ? csharpSignatureRawStartColumn + : absoluteStartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(entry.ReturnType), + }, + line); + } } - } - else if (fortranEnumeratorEntries != null) - { - foreach (var entry in fortranEnumeratorEntries) + else if (swiftEnumCaseEntries != null) { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = entry.Name, - Line = startLine, - StartLine = startLine, - StartColumn = entry.StartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, - line); + foreach (var entry in swiftEnumCaseEntries) + { + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = entry.Name, + Line = startLine, + StartLine = startLine, + StartColumn = entry.StartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(entry.ReturnType), + }, + line); + } } - } - else if (fortranParameterEntries != null) - { - foreach (var entry in fortranParameterEntries) + else if (fortranEnumeratorEntries != null) { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = entry.Name, - Line = startLine, - StartLine = startLine, - StartColumn = entry.StartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, - line); + foreach (var entry in fortranEnumeratorEntries) + { + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = entry.Name, + Line = startLine, + StartLine = startLine, + StartColumn = entry.StartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, + line); + } } - } - else if (fortranProcedureNames != null) - { - foreach (var procedureName in fortranProcedureNames) + else if (fortranParameterEntries != null) { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = procedureName, - Line = startLine, - StartLine = startLine, - StartColumn = csharpSingleLineCollapsedMatch - ? csharpSignatureRawStartColumn - : absoluteStartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, - line); + foreach (var entry in fortranParameterEntries) + { + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = entry.Name, + Line = startLine, + StartLine = startLine, + StartColumn = entry.StartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, + line); + } } - } - else if (rubyAttrNames != null) - { - var rubyAttrSearchStart = absoluteStartColumn; - foreach (var rubyAttrName in rubyAttrNames) + else if (fortranProcedureNames != null) { - var rubyAttrStartColumn = rubyAttrSearchStart; - if (!string.Equals(rubyAttrName, name, StringComparison.Ordinal)) + foreach (var procedureName in fortranProcedureNames) { - var foundRubyAttrStart = patternMatchLine.IndexOf(rubyAttrName, rubyAttrSearchStart, StringComparison.Ordinal); - if (foundRubyAttrStart >= 0) - rubyAttrStartColumn = foundRubyAttrStart; + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = procedureName, + Line = startLine, + StartLine = startLine, + StartColumn = csharpSingleLineCollapsedMatch + ? csharpSignatureRawStartColumn + : absoluteStartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, + line); } - - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord + } + else if (rubyAttrNames != null) + { + var rubyAttrSearchStart = absoluteStartColumn; + foreach (var rubyAttrName in rubyAttrNames) + { + var rubyAttrStartColumn = rubyAttrSearchStart; + if (!string.Equals(rubyAttrName, name, StringComparison.Ordinal)) { - FileId = fileId, - Kind = kind, - Name = rubyAttrName, - Line = startLine, - StartLine = startLine, - StartColumn = rubyAttrStartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, - line); + var foundRubyAttrStart = patternMatchLine.IndexOf(rubyAttrName, rubyAttrSearchStart, StringComparison.Ordinal); + if (foundRubyAttrStart >= 0) + rubyAttrStartColumn = foundRubyAttrStart; + } - rubyAttrSearchStart = rubyAttrStartColumn + Math.Max(1, rubyAttrName.Length); + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = rubyAttrName, + Line = startLine, + StartLine = startLine, + StartColumn = rubyAttrStartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, + line); + + rubyAttrSearchStart = rubyAttrStartColumn + Math.Max(1, rubyAttrName.Length); + } } - } - else - { - AddSymbolRecord( - symbols, - cssSeenSymbols, - startLine, - new SymbolRecord - { - FileId = fileId, - Kind = kind, - Name = name, - Line = startLine, - StartLine = startLine, - StartColumn = lang == "rust" && pattern.Kind == "function" - ? match.Groups["name"].Index - : (csharpSingleLineCollapsedMatch - ? csharpSignatureRawStartColumn - : absoluteStartColumn), - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - FamilyKey = lang == "cpp" && kind == "specialization" ? name : null, - SubKind = pythonSubKind ?? ResolveLanguageSubKind(lang, kind, signature, patternMatchLine), - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, - line); - - if (lang == "objc" - && pattern.Kind == "class" - && TryGetObjCCategoryDisplayName(patternMatchLine[absoluteStartColumn..], name, out var categoryDisplayName)) + else { AddSymbolRecord( symbols, cssSeenSymbols, startLine, - new SymbolRecord - { - FileId = fileId, - Kind = "class", - Name = categoryDisplayName, - Line = startLine, - StartLine = startLine, - StartColumn = csharpSingleLineCollapsedMatch - ? csharpSignatureRawStartColumn - : absoluteStartColumn, - EndLine = Math.Max(startLine, endLine), - BodyStartLine = bodyStartLine, - BodyEndLine = bodyEndLine, - Signature = signature, - Visibility = TryGetGroup(match, pattern.VisibilityGroup), - ReturnType = NormalizeMetadata(rawReturnType), - }, + new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = name, + Line = startLine, + StartLine = startLine, + StartColumn = lang == "rust" && pattern.Kind == "function" + ? match.Groups["name"].Index + : (csharpSingleLineCollapsedMatch + ? csharpSignatureRawStartColumn + : absoluteStartColumn), + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + FamilyKey = lang == "cpp" && kind == "specialization" ? name : null, + SubKind = pythonSubKind ?? ResolveLanguageSubKind(lang, kind, signature, patternMatchLine), + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, line); + + if (lang == "objc" + && pattern.Kind == "class" + && TryGetObjCCategoryDisplayName(patternMatchLine[absoluteStartColumn..], name, out var categoryDisplayName)) + { + AddSymbolRecord( + symbols, + cssSeenSymbols, + startLine, + new SymbolRecord + { + FileId = fileId, + Kind = "class", + Name = categoryDisplayName, + Line = startLine, + StartLine = startLine, + StartColumn = csharpSingleLineCollapsedMatch + ? csharpSignatureRawStartColumn + : absoluteStartColumn, + EndLine = Math.Max(startLine, endLine), + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = signature, + Visibility = TryGetGroup(match, pattern.VisibilityGroup), + ReturnType = NormalizeMetadata(rawReturnType), + }, + line); + } } } - } - - if (lang == "css" - && pattern.Kind == "namespace" - && pattern.BodyStyle == BodyStyle.Brace - && cssScannerLines != null) - { - TryAddCssMediaFeatureSymbols( - fileId, - line, - cssScannerLines[i], - i, - symbols, - cssSeenSymbols); - } - if (lang == "css" - && pattern.Kind == "class" - && pattern.BodyStyle == BodyStyle.Brace - && cssScannerLines != null) - { - var openingBraceIndex = cssScannerLines[i].IndexOf('{', absoluteStartColumn); - if (openingBraceIndex > absoluteStartColumn) + if (lang == "css" + && pattern.Kind == "namespace" + && pattern.BodyStyle == BodyStyle.Brace + && cssScannerLines != null) { - TryAddCssSelectorListSegments( + TryAddCssMediaFeatureSymbols( fileId, - line[absoluteStartColumn..openingBraceIndex], - cssScannerLines[i][absoluteStartColumn..openingBraceIndex], - cssScannerLines, + line, + cssScannerLines[i], i, - openingBraceIndex, - patterns, symbols, cssSeenSymbols); } - } - if (lang == "csharp" - && pattern.Kind == "property" - && csharpPropertyCandidate.ExpressionBodyEndLineIndex.HasValue) - { - csharpSuppressedContinuationUntil = Math.Max(csharpSuppressedContinuationUntil, csharpPropertyCandidate.ExpressionBodyEndLineIndex.Value); - } + if (lang == "css" + && pattern.Kind == "class" + && pattern.BodyStyle == BodyStyle.Brace + && cssScannerLines != null) + { + var openingBraceIndex = cssScannerLines[i].IndexOf('{', absoluteStartColumn); + if (openingBraceIndex > absoluteStartColumn) + { + TryAddCssSelectorListSegments( + fileId, + line[absoluteStartColumn..openingBraceIndex], + cssScannerLines[i][absoluteStartColumn..openingBraceIndex], + cssScannerLines, + i, + openingBraceIndex, + patterns, + symbols, + cssSeenSymbols); + } + } - if (lang == "csharp" - && pattern.Kind is "event" or "delegate" - && pattern.BodyStyle == BodyStyle.None - && (TryGetCSharpSameLineEventSiblingOffset(patternMatchLine, absoluteStartColumn, out var nextSemicolonSiblingOffset) - || TryGetCSharpSameLineSemicolonSiblingOffset(patternMatchLine, absoluteStartColumn, out nextSemicolonSiblingOffset))) - { - restartPatternScanOffset = nextSemicolonSiblingOffset; - break; - } + if (lang == "csharp" + && pattern.Kind == "property" + && csharpPropertyCandidate.ExpressionBodyEndLineIndex.HasValue) + { + csharpSuppressedContinuationUntil = Math.Max(csharpSuppressedContinuationUntil, csharpPropertyCandidate.ExpressionBodyEndLineIndex.Value); + } - if (lang == "java" - && pattern.BodyStyle == BodyStyle.Brace - && bodyStartLine == null - && TryGetJavaSameLineSemicolonSiblingOffset(patternMatchLine, absoluteStartColumn, out var nextJavaSiblingOffset)) - { - // Body-less Java members inside `interface` / `@interface` / abstract-style - // declarations can share one physical line (`String[] value(); int age();`). - // Restart at the next sibling after the top-level `;` instead of stopping at - // the first match, or later members on the same line disappear. Closes #788. - // Java の body-less member(`interface` / `@interface` / abstract 形)は - // `String[] value(); int age();` のように 1 行へ並ぶ。top-level `;` - // の直後から sibling へ再開しないと、同一行の後続 member が最初の 1 個で - // 途切れて消える。Closes #788. - restartPatternScanOffset = nextJavaSiblingOffset; - break; - } + if (lang == "csharp" + && pattern.Kind is "event" or "delegate" + && pattern.BodyStyle == BodyStyle.None + && (TryGetCSharpSameLineEventSiblingOffset(patternMatchLine, absoluteStartColumn, out var nextSemicolonSiblingOffset) + || TryGetCSharpSameLineSemicolonSiblingOffset(patternMatchLine, absoluteStartColumn, out nextSemicolonSiblingOffset))) + { + restartPatternScanOffset = nextSemicolonSiblingOffset; + break; + } - CollectRecordPrimaryComponentSymbols( - fileId, - lang, - lines, - i, - absoluteStartColumn, - kind, - name, - pendingRecordPrimaryComponents, - symbols); - - if (lang == "csharp" && pattern.Kind == "function") - { - CollectCSharpCallableParameterSymbols( + if (lang == "java" + && pattern.BodyStyle == BodyStyle.Brace + && bodyStartLine == null + && TryGetJavaSameLineSemicolonSiblingOffset(patternMatchLine, absoluteStartColumn, out var nextJavaSiblingOffset)) + { + // Body-less Java members inside `interface` / `@interface` / abstract-style + // declarations can share one physical line (`String[] value(); int age();`). + // Restart at the next sibling after the top-level `;` instead of stopping at + // the first match, or later members on the same line disappear. Closes #788. + // Java の body-less member(`interface` / `@interface` / abstract 形)は + // `String[] value(); int age();` のように 1 行へ並ぶ。top-level `;` + // の直後から sibling へ再開しないと、同一行の後続 member が最初の 1 個で + // 途切れて消える。Closes #788. + restartPatternScanOffset = nextJavaSiblingOffset; + break; + } + + CollectRecordPrimaryComponentSymbols( fileId, - signature, - startLine, + lang, + lines, + i, + absoluteStartColumn, kind, name, + pendingRecordPrimaryComponents, symbols); - } - // C# plain-field (kind `property`, BodyStyle.None) matches need their own - // advance path. The generic `sameLineEndColumn`-based advance below resolves - // to -1 for BodyStyle.None and would set `stopAfterFirstPatternMatch`, which - // prevents structural siblings on the same line (e.g. the enclosing - // `public class C` in `public class C { public int X; }`) from being - // captured by later patterns. Instead, advance past the field terminator - // and continue the same-pattern scan so multiple same-line fields are - // still collected, and skip the stop flag so later patterns can still run. - // Closes #400. - // C# 通常フィールド(kind `property`、BodyStyle.None)は専用の前進経路を使う。 - // 既定の `sameLineEndColumn` ベースの前進は BodyStyle.None では -1 に落ち、 - // `stopAfterFirstPatternMatch` を立ててしまうため、同一行に存在する構造宣言 - // (例: `public class C { public int X; }` の外側 class)を後続パターンで - // 取得できなくなる。代わりにフィールド終端を越えて同一パターンのスキャンを - // 続け、stop フラグを立てずに次のパターンにも機会を残す。Closes #400. - if (lang == "csharp" - && pattern.Kind == "property" - && pattern.BodyStyle == BodyStyle.None) - { - // Advance past the end of the full field declaration statement - // (the top-level `;`, with paren / bracket / brace depth tracking - // so `{` / `;` inside an initializer cannot short-circuit the - // scan) and continue. Using the statement end rather than the - // regex match end keeps later same-line field statements visible - // to the same pattern: without this, `A = 1; B;` stopped after - // capturing `A` and dropped `B`, and `A, B; C;` stopped after - // expanding `A, B` as a declarator list and dropped `C`. It also - // avoids the earlier regression where advancing to the match end - // (which sits on `=` when the field has an initializer) made the - // regex re-match the tail `1, _b, _c =` as a bogus field with - // `return_type = "1, _b,"`. If the scanner hits an unbalanced - // `}` (the closing brace of the enclosing type body) before a - // `;`, break out without setting `stopAfterFirstPatternMatch` so - // later unrelated patterns on the same line still get a chance - // to run. Closes #400. - // フィールド宣言文全体の終端(`;`、paren / bracket / brace 深さを - // 追って初期化子内の `{` や `;` で途切れないようにする)まで進めて - // 同一パターンで scan を続ける。regex match の末尾ではなく文の - // 終端で advance するのが肝心で、これが無いと `A = 1; B;` は - // `A` を拾った時点で止まって `B` を取り落とし、`A, B; C;` は - // `A, B` を declarator list として展開した時点で `C` を取り落とす。 - // さらに、match の末尾(初期化子付きなら `=`)まで進めて continue - // すると正規表現が残りの `1, _b, _c =` を `return_type = "1, _b,"` - // の偽フィールドとして再マッチしていた旧 regression も再発しない。 - // `;` より先に囲む型本体の閉じ `}`(深さ 0)に到達した場合は、 - // `stopAfterFirstPatternMatch` を立てずに break して同一行の他 - // パターン(class 等)へ機会を残す。Closes #400. - var statementEnd = FindCSharpSameLineStatementEnd(patternMatchLine, absoluteStartColumn); - if (statementEnd < patternMatchLine.Length - && patternMatchLine[statementEnd] == '}') - { - break; - } - // Only continue the same-pattern same-line scan when the regex - // ran on a per-line single-line candidate (patternMatchLine === - // csharpMatchLines[i]). For multi-line merged candidates, - // BuildCSharpPropertyMatchLine joined the header line with one - // or more continuation lines, so absoluteStartColumn sits in - // the merged-string column domain and does not line up with - // lines[i]'s raw columns. Continuing past statementEnd into a - // second regex hit would then feed a column > lines[i].Length - // into BuildCSharpMultilineSignature (which slices - // lines[startLineIndex][startColumn..]) and crash indexing with - // `startIndex cannot be larger than length of string`. The - // continuation line is revisited by the outer physical-line - // loop anyway (csharpSuppressedContinuationUntil is only bumped - // for expression-bodied properties), so for multi-line merged - // candidates we break here and let the outer loop handle any - // additional fields on that line. Closes #400. - // same-pattern での同一行 scan 継続は、per-line の単一行候補 - // (patternMatchLine === csharpMatchLines[i])のときだけ許す。 - // BuildCSharpPropertyMatchLine が header 行と continuation 行を - // マージした複数行候補では、absoluteStartColumn がマージ後文字列の - // 列を指しており lines[i] の raw 列として使えない。この状態で - // statementEnd を越えて 2 個目の regex ヒットに進むと、 - // BuildCSharpMultilineSignature の lines[startLineIndex][startColumn..] - // で範囲外アクセスとなり - // 「startIndex cannot be larger than length of string」で indexing が - // 落ちる。continuation 行は外側の物理行ループが再訪する - // (csharpSuppressedContinuationUntil は expression-bodied property - // でしか進まない)ため、複数行候補ではここで break して後続の - // 同一行フィールド抽出を外側ループに任せる。Closes #400. - if (csharpMatchLines == null - || !ReferenceEquals(patternMatchLine, csharpMatchLines[i])) + if (lang == "csharp" && pattern.Kind == "function") { - break; + CollectCSharpCallableParameterSymbols( + fileId, + signature, + startLine, + kind, + name, + symbols); } - var advance = statementEnd; - if (advance <= lineOffset) - advance = lineOffset + 1; - if (advance >= patternMatchLine.Length) - break; - lineOffset = advance; - continue; - } - if (!CanContinueScanningSameLineBraceBody(lang, kind, pattern.BodyStyle, bodyEndLine, startLine, sameLineEndColumn, absoluteStartColumn)) - { + // C# plain-field (kind `property`, BodyStyle.None) matches need their own + // advance path. The generic `sameLineEndColumn`-based advance below resolves + // to -1 for BodyStyle.None and would set `stopAfterFirstPatternMatch`, which + // prevents structural siblings on the same line (e.g. the enclosing + // `public class C` in `public class C { public int X; }`) from being + // captured by later patterns. Instead, advance past the field terminator + // and continue the same-pattern scan so multiple same-line fields are + // still collected, and skip the stop flag so later patterns can still run. + // Closes #400. + // C# 通常フィールド(kind `property`、BodyStyle.None)は専用の前進経路を使う。 + // 既定の `sameLineEndColumn` ベースの前進は BodyStyle.None では -1 に落ち、 + // `stopAfterFirstPatternMatch` を立ててしまうため、同一行に存在する構造宣言 + // (例: `public class C { public int X; }` の外側 class)を後続パターンで + // 取得できなくなる。代わりにフィールド終端を越えて同一パターンのスキャンを + // 続け、stop フラグを立てずに次のパターンにも機会を残す。Closes #400. if (lang == "csharp" - && pattern.BodyStyle == BodyStyle.Brace - && bodyStartLine == startLine - && kind is "class" or "struct" or "interface" or "enum" or "namespace") + && pattern.Kind == "property" + && pattern.BodyStyle == BodyStyle.None) { - // Hybrid same-line C# type headers can open the body on the header - // line and still close on a later line (`class C { int P { get; }` - // + next-line `}`). They are not compact same-line bodies, so the - // generic same-line brace-body path does not restart inside them. - // Explicitly restart just after the opening `{` so the first member - // that shares the header line is still visible to the full pattern - // list. Closes #580. - // ハイブリッドな C# の same-line 型ヘッダは、本体開始 `{` がヘッダ行に - // ありつつ閉じ `}` は後続行に置かれうる (`class C { int P { get; }` - // + 次行 `}`)。これは compact な same-line body ではないため、 - // 既定の same-line brace-body 経路だけでは本体内へ再開できない。 - // そこで開始 `{` の直後から明示的に再開し、ヘッダ行を共有する最初の - // member も通常の pattern 列で拾えるようにする。Closes #580. - var nextHeaderLineMemberOffset = FindNextSameLineNonClosingBraceStatementStart( - matchLine, - absoluteStartColumn + Math.Max(1, match.Length), - lang); - if (nextHeaderLineMemberOffset > absoluteStartColumn - && nextHeaderLineMemberOffset < matchLine.Length) + // Advance past the end of the full field declaration statement + // (the top-level `;`, with paren / bracket / brace depth tracking + // so `{` / `;` inside an initializer cannot short-circuit the + // scan) and continue. Using the statement end rather than the + // regex match end keeps later same-line field statements visible + // to the same pattern: without this, `A = 1; B;` stopped after + // capturing `A` and dropped `B`, and `A, B; C;` stopped after + // expanding `A, B` as a declarator list and dropped `C`. It also + // avoids the earlier regression where advancing to the match end + // (which sits on `=` when the field has an initializer) made the + // regex re-match the tail `1, _b, _c =` as a bogus field with + // `return_type = "1, _b,"`. If the scanner hits an unbalanced + // `}` (the closing brace of the enclosing type body) before a + // `;`, break out without setting `stopAfterFirstPatternMatch` so + // later unrelated patterns on the same line still get a chance + // to run. Closes #400. + // フィールド宣言文全体の終端(`;`、paren / bracket / brace 深さを + // 追って初期化子内の `{` や `;` で途切れないようにする)まで進めて + // 同一パターンで scan を続ける。regex match の末尾ではなく文の + // 終端で advance するのが肝心で、これが無いと `A = 1; B;` は + // `A` を拾った時点で止まって `B` を取り落とし、`A, B; C;` は + // `A, B` を declarator list として展開した時点で `C` を取り落とす。 + // さらに、match の末尾(初期化子付きなら `=`)まで進めて continue + // すると正規表現が残りの `1, _b, _c =` を `return_type = "1, _b,"` + // の偽フィールドとして再マッチしていた旧 regression も再発しない。 + // `;` より先に囲む型本体の閉じ `}`(深さ 0)に到達した場合は、 + // `stopAfterFirstPatternMatch` を立てずに break して同一行の他 + // パターン(class 等)へ機会を残す。Closes #400. + var statementEnd = FindCSharpSameLineStatementEnd(patternMatchLine, absoluteStartColumn); + if (statementEnd < patternMatchLine.Length + && patternMatchLine[statementEnd] == '}') + { + break; + } + // Only continue the same-pattern same-line scan when the regex + // ran on a per-line single-line candidate (patternMatchLine === + // csharpMatchLines[i]). For multi-line merged candidates, + // BuildCSharpPropertyMatchLine joined the header line with one + // or more continuation lines, so absoluteStartColumn sits in + // the merged-string column domain and does not line up with + // lines[i]'s raw columns. Continuing past statementEnd into a + // second regex hit would then feed a column > lines[i].Length + // into BuildCSharpMultilineSignature (which slices + // lines[startLineIndex][startColumn..]) and crash indexing with + // `startIndex cannot be larger than length of string`. The + // continuation line is revisited by the outer physical-line + // loop anyway (csharpSuppressedContinuationUntil is only bumped + // for expression-bodied properties), so for multi-line merged + // candidates we break here and let the outer loop handle any + // additional fields on that line. Closes #400. + // same-pattern での同一行 scan 継続は、per-line の単一行候補 + // (patternMatchLine === csharpMatchLines[i])のときだけ許す。 + // BuildCSharpPropertyMatchLine が header 行と continuation 行を + // マージした複数行候補では、absoluteStartColumn がマージ後文字列の + // 列を指しており lines[i] の raw 列として使えない。この状態で + // statementEnd を越えて 2 個目の regex ヒットに進むと、 + // BuildCSharpMultilineSignature の lines[startLineIndex][startColumn..] + // で範囲外アクセスとなり + // 「startIndex cannot be larger than length of string」で indexing が + // 落ちる。continuation 行は外側の物理行ループが再訪する + // (csharpSuppressedContinuationUntil は expression-bodied property + // でしか進まない)ため、複数行候補ではここで break して後続の + // 同一行フィールド抽出を外側ループに任せる。Closes #400. + if (csharpMatchLines == null + || !ReferenceEquals(patternMatchLine, csharpMatchLines[i])) { - restartPatternScanOffset = nextHeaderLineMemberOffset; break; } + var advance = statementEnd; + if (advance <= lineOffset) + advance = lineOffset + 1; + if (advance >= patternMatchLine.Length) + break; + lineOffset = advance; + continue; } - if (lang == "csharp" - && sameLineEndColumn >= absoluteStartColumn - && CanRestartCSharpSameLineSiblingScan(kind)) + if (!CanContinueScanningSameLineBraceBody(lang, kind, pattern.BodyStyle, bodyEndLine, startLine, sameLineEndColumn, absoluteStartColumn)) { - // Compact same-line C# members form a sibling stream rather than a - // single terminal match: after `event E;`, `void M();`, or - // `int P { get; set; }`, later same-line declarations still need - // to reach earlier patterns in the list. Restart from the next - // top-level statement boundary so mixed-kind siblings like - // `event + property`, `method + property`, and `property + event` - // are all visible. When there is no later statement, keep the old - // stop-after-first-match behavior to avoid reopening duplicate - // paths on ordinary single-declaration lines. Closes #470 / #473. - // 同一行のコンパクトな C# member は 1 回限りの terminal match ではなく、 - // sibling 宣言のストリームとして扱う。`event E;` や `void M();`、 - // `int P { get; set; }` の後ろに続く宣言も、pattern 列の先頭側にある - // property などへ到達できる必要がある。そこで次の top-level 文境界から - // pattern 列全体を再走査し、`event + property`、`method + property`、 - // `property + event` のような mixed-kind sibling をすべて可視化する。 - // 後続宣言が無い行では従来どおり stop-after-first-match を維持し、 - // 通常の単独宣言行で duplicate 経路を再び開かない。Closes #470 / #473. - if (csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns) + if (lang == "csharp" + && pattern.BodyStyle == BodyStyle.Brace + && bodyStartLine == startLine + && kind is "class" or "struct" or "interface" or "enum" or "namespace") { - var rawNextSiblingOffset = FindNextSameLineNonClosingBraceStatementStart(line, sameLineEndColumn + 1, lang); - if (rawNextSiblingOffset > sameLineEndColumn) + // Hybrid same-line C# type headers can open the body on the header + // line and still close on a later line (`class C { int P { get; }` + // + next-line `}`). They are not compact same-line bodies, so the + // generic same-line brace-body path does not restart inside them. + // Explicitly restart just after the opening `{` so the first member + // that shares the header line is still visible to the full pattern + // list. Closes #580. + // ハイブリッドな C# の same-line 型ヘッダは、本体開始 `{` がヘッダ行に + // ありつつ閉じ `}` は後続行に置かれうる (`class C { int P { get; }` + // + 次行 `}`)。これは compact な same-line body ではないため、 + // 既定の same-line brace-body 経路だけでは本体内へ再開できない。 + // そこで開始 `{` の直後から明示的に再開し、ヘッダ行を共有する最初の + // member も通常の pattern 列で拾えるようにする。Closes #580. + var nextHeaderLineMemberOffset = FindNextSameLineNonClosingBraceStatementStart( + matchLine, + absoluteStartColumn + Math.Max(1, match.Length), + lang); + if (nextHeaderLineMemberOffset > absoluteStartColumn + && nextHeaderLineMemberOffset < matchLine.Length) { - restartPatternScanOffset = TranslateCSharpRawColumnToCollapsed( - csharpMatchColumnToRaw, - i, - rawNextSiblingOffset, - matchLine.Length, - line.Length); + restartPatternScanOffset = nextHeaderLineMemberOffset; break; } } - else + + if (lang == "csharp" + && sameLineEndColumn >= absoluteStartColumn + && CanRestartCSharpSameLineSiblingScan(kind)) { - var nextSiblingOffset = FindNextSameLineNonClosingBraceStatementStart(matchLine, sameLineEndColumn + 1, lang); - if (nextSiblingOffset > sameLineEndColumn - && nextSiblingOffset < matchLine.Length) + // Compact same-line C# members form a sibling stream rather than a + // single terminal match: after `event E;`, `void M();`, or + // `int P { get; set; }`, later same-line declarations still need + // to reach earlier patterns in the list. Restart from the next + // top-level statement boundary so mixed-kind siblings like + // `event + property`, `method + property`, and `property + event` + // are all visible. When there is no later statement, keep the old + // stop-after-first-match behavior to avoid reopening duplicate + // paths on ordinary single-declaration lines. Closes #470 / #473. + // 同一行のコンパクトな C# member は 1 回限りの terminal match ではなく、 + // sibling 宣言のストリームとして扱う。`event E;` や `void M();`、 + // `int P { get; set; }` の後ろに続く宣言も、pattern 列の先頭側にある + // property などへ到達できる必要がある。そこで次の top-level 文境界から + // pattern 列全体を再走査し、`event + property`、`method + property`、 + // `property + event` のような mixed-kind sibling をすべて可視化する。 + // 後続宣言が無い行では従来どおり stop-after-first-match を維持し、 + // 通常の単独宣言行で duplicate 経路を再び開かない。Closes #470 / #473. + if (csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns) { - restartPatternScanOffset = nextSiblingOffset; - break; + var rawNextSiblingOffset = FindNextSameLineNonClosingBraceStatementStart(line, sameLineEndColumn + 1, lang); + if (rawNextSiblingOffset > sameLineEndColumn) + { + restartPatternScanOffset = TranslateCSharpRawColumnToCollapsed( + csharpMatchColumnToRaw, + i, + rawNextSiblingOffset, + matchLine.Length, + line.Length); + break; + } } + else + { + var nextSiblingOffset = FindNextSameLineNonClosingBraceStatementStart(matchLine, sameLineEndColumn + 1, lang); + if (nextSiblingOffset > sameLineEndColumn + && nextSiblingOffset < matchLine.Length) + { + restartPatternScanOffset = nextSiblingOffset; + break; + } + } + } + + // Batch `set` assignments can legitimately repeat on a single line via + // `&` command-chaining (`set A=1 & set B=2`), parenthesized grouping + // (`if ... ( set P=1 ) else set Q=2`), or `for`-loop bodies + // (`for %%I in (1) do set LOOPVAR=%%I`). The brace-body rescan path + // above is JS/TS/CSS/C#-only, so drive the advance explicitly for the + // batch property pattern instead of short-circuiting after the first + // match. Forward progress is guaranteed because `match.Length >= 1` + // (the regex requires a literal `set\s+NAME=` tail). + // batch の `set` 代入は `&` 連結や `( ... ) else ... `、`for ... do ...` で + // 1 行に複数回現れうる。上の brace-body 再スキャンは JS/TS/CSS/C# 限定なので、 + // batch の property パターンだけは explicit に advance して追加マッチも拾う。 + // 前進は `match.Length >= 1` (正規表現が `set\s+NAME=` を要求するため) で保証される。 + if (lang == "batch" + && pattern.BodyStyle == BodyStyle.None + && pattern.Kind == "property") + { + var nextBatchOffset = absoluteStartColumn + Math.Max(1, match.Length); + if (nextBatchOffset <= lineOffset) + break; + lineOffset = nextBatchOffset; + continue; } + + // Stop after first match per line to avoid duplicate symbols + // (e.g. C# method pattern + constructor pattern both matching) + // 1行につき最初のマッチのみ採用し重複を防ぐ + stopAfterFirstPatternMatch = true; + break; } - // Batch `set` assignments can legitimately repeat on a single line via - // `&` command-chaining (`set A=1 & set B=2`), parenthesized grouping - // (`if ... ( set P=1 ) else set Q=2`), or `for`-loop bodies - // (`for %%I in (1) do set LOOPVAR=%%I`). The brace-body rescan path - // above is JS/TS/CSS/C#-only, so drive the advance explicitly for the - // batch property pattern instead of short-circuiting after the first - // match. Forward progress is guaranteed because `match.Length >= 1` - // (the regex requires a literal `set\s+NAME=` tail). - // batch の `set` 代入は `&` 連結や `( ... ) else ... `、`for ... do ...` で - // 1 行に複数回現れうる。上の brace-body 再スキャンは JS/TS/CSS/C# 限定なので、 - // batch の property パターンだけは explicit に advance して追加マッチも拾う。 - // 前進は `match.Length >= 1` (正規表現が `set\s+NAME=` を要求するため) で保証される。 - if (lang == "batch" - && pattern.BodyStyle == BodyStyle.None - && pattern.Kind == "property") + // For C# class-like kinds with a same-line brace body, step into the body + // (advance just past the match header) instead of jumping past the closing + // `}`. This lets nested same-line declarations be captured, e.g. + // `public class Outer { public class Inner { public int X; } }` matches + // Outer and Inner, with X correctly attached to Inner. JavaScript/TypeScript + // does not need this because class-body members there are extracted via the + // separate JS/TS lexer/state machine; the brace-skip path only handles + // same-line siblings like `class A {} class B {}`. Closes #400. + // C# の class 系 kind は同一行の `{...}` 本体を飛び越えず、ヘッダ直後へ + // 進めて本体内部の宣言(例: `public class Outer { public class Inner { ... } }` + // の Inner)を拾えるようにする。JavaScript/TypeScript は class body の + // member 抽出を専用 lexer/state machine で行うため従来通り終端の後ろへ + // 進め、同一行 sibling(`class A {} class B {}` など)だけを扱う。Closes #400. + var sameLineRestartComparisonColumn = csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns + ? TranslateCSharpRawColumnToCollapsed( + csharpMatchColumnToRaw, + i, + sameLineEndColumn, + matchLine.Length, + line.Length) + : sameLineEndColumn; + if (CanStepIntoSameLineTypeBody(lang, kind)) { - var nextBatchOffset = absoluteStartColumn + Math.Max(1, match.Length); - if (nextBatchOffset <= lineOffset) + var nextTypeBodyOffset = FindNextSameLineNonClosingBraceStatementStart( + matchLine, + absoluteStartColumn + Math.Max(1, match.Length), + lang); + if (nextTypeBodyOffset > absoluteStartColumn + && nextTypeBodyOffset < sameLineRestartComparisonColumn + && (nextTypeBodyOffset >= matchLine.Length || matchLine[nextTypeBodyOffset] != '}')) + { + restartPatternScanOffset = nextTypeBodyOffset; break; - lineOffset = nextBatchOffset; - continue; + } } - // Stop after first match per line to avoid duplicate symbols - // (e.g. C# method pattern + constructor pattern both matching) - // 1行につき最初のマッチのみ採用し重複を防ぐ - stopAfterFirstPatternMatch = true; - break; - } - - // For C# class-like kinds with a same-line brace body, step into the body - // (advance just past the match header) instead of jumping past the closing - // `}`. This lets nested same-line declarations be captured, e.g. - // `public class Outer { public class Inner { public int X; } }` matches - // Outer and Inner, with X correctly attached to Inner. JavaScript/TypeScript - // does not need this because class-body members there are extracted via the - // separate JS/TS lexer/state machine; the brace-skip path only handles - // same-line siblings like `class A {} class B {}`. Closes #400. - // C# の class 系 kind は同一行の `{...}` 本体を飛び越えず、ヘッダ直後へ - // 進めて本体内部の宣言(例: `public class Outer { public class Inner { ... } }` - // の Inner)を拾えるようにする。JavaScript/TypeScript は class body の - // member 抽出を専用 lexer/state machine で行うため従来通り終端の後ろへ - // 進め、同一行 sibling(`class A {} class B {}` など)だけを扱う。Closes #400. - var sameLineRestartComparisonColumn = csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns - ? TranslateCSharpRawColumnToCollapsed( - csharpMatchColumnToRaw, - i, - sameLineEndColumn, - matchLine.Length, - line.Length) - : sameLineEndColumn; - if (CanStepIntoSameLineTypeBody(lang, kind)) - { - var nextTypeBodyOffset = FindNextSameLineNonClosingBraceStatementStart( - matchLine, - absoluteStartColumn + Math.Max(1, match.Length), - lang); - if (nextTypeBodyOffset > absoluteStartColumn - && nextTypeBodyOffset < sameLineRestartComparisonColumn - && (nextTypeBodyOffset >= matchLine.Length || matchLine[nextTypeBodyOffset] != '}')) + var nextSameLineOffset = -1; + if (csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns) + { + var rawNextSameLineOffset = FindNextSameLineNonClosingBraceStatementStart(line, sameLineEndColumn + 1, lang); + if (rawNextSameLineOffset > sameLineEndColumn) + { + nextSameLineOffset = TranslateCSharpRawColumnToCollapsed( + csharpMatchColumnToRaw, + i, + rawNextSameLineOffset, + matchLine.Length, + line.Length); + } + } + else { - restartPatternScanOffset = nextTypeBodyOffset; + nextSameLineOffset = FindNextSameLineNonClosingBraceStatementStart(matchLine, sameLineEndColumn + 1, lang); + } + var sameLineAdvanceComparisonColumn = sameLineRestartComparisonColumn; + if (CanStepIntoSameLineTypeBody(lang, kind) + && nextSameLineOffset > sameLineAdvanceComparisonColumn + && nextSameLineOffset < matchLine.Length + && matchLine[nextSameLineOffset] != '}') + { + restartPatternScanOffset = nextSameLineOffset; break; } - } - - var nextSameLineOffset = -1; - if (csharpSingleLineCollapsedMatch && sameLineEndUsesRawColumns) - { - var rawNextSameLineOffset = FindNextSameLineNonClosingBraceStatementStart(line, sameLineEndColumn + 1, lang); - if (rawNextSameLineOffset > sameLineEndColumn) + if (lang == "csharp" + && kind == "property" + && pattern.BodyStyle == BodyStyle.Brace + && nextSameLineOffset > sameLineAdvanceComparisonColumn + && nextSameLineOffset < matchLine.Length) { - nextSameLineOffset = TranslateCSharpRawColumnToCollapsed( - csharpMatchColumnToRaw, - i, - rawNextSameLineOffset, - matchLine.Length, - line.Length); + // A same-line brace-body property that is followed by another sibling + // declaration (`P { get; set; } public void M() { }`) must hand control + // back to the whole pattern list at the next statement start, otherwise + // earlier rows like the C# method regex never get a chance to see the + // trailing sibling and mixed-kind lines lose one side. + // Closes #473 follow-up. + // 後続 sibling 宣言を伴う same-line brace-body property + // (`P { get; set; } public void M() { }`) は、次の文開始位置から + // pattern 全体へ制御を戻す必要がある。そうしないと、C# method regex + // のような earlier row が後続 sibling を見られず、mixed-kind の + // 同一行で片側が欠落する。Closes #473 follow-up. + restartPatternScanOffset = nextSameLineOffset; + break; } - } - else - { - nextSameLineOffset = FindNextSameLineNonClosingBraceStatementStart(matchLine, sameLineEndColumn + 1, lang); - } - var sameLineAdvanceComparisonColumn = sameLineRestartComparisonColumn; - if (CanStepIntoSameLineTypeBody(lang, kind) - && nextSameLineOffset > sameLineAdvanceComparisonColumn - && nextSameLineOffset < matchLine.Length - && matchLine[nextSameLineOffset] != '}') - { - restartPatternScanOffset = nextSameLineOffset; - break; - } - if (lang == "csharp" - && kind == "property" - && pattern.BodyStyle == BodyStyle.Brace - && nextSameLineOffset > sameLineAdvanceComparisonColumn - && nextSameLineOffset < matchLine.Length) - { - // A same-line brace-body property that is followed by another sibling - // declaration (`P { get; set; } public void M() { }`) must hand control - // back to the whole pattern list at the next statement start, otherwise - // earlier rows like the C# method regex never get a chance to see the - // trailing sibling and mixed-kind lines lose one side. - // Closes #473 follow-up. - // 後続 sibling 宣言を伴う same-line brace-body property - // (`P { get; set; } public void M() { }`) は、次の文開始位置から - // pattern 全体へ制御を戻す必要がある。そうしないと、C# method regex - // のような earlier row が後続 sibling を見られず、mixed-kind の - // 同一行で片側が欠落する。Closes #473 follow-up. - restartPatternScanOffset = nextSameLineOffset; - break; - } - lineOffset = nextSameLineOffset; - } + lineOffset = nextSameLineOffset; + } - if (restartPatternScanOffset >= 0 || stopAfterFirstPatternMatch) - break; + if (restartPatternScanOffset >= 0 || stopAfterFirstPatternMatch) + break; } if (restartPatternScanOffset >= 0) @@ -5319,18 +5319,18 @@ private static (int EndLine, int? BodyStartLine, int? BodyEndLine) ResolveRange( { BodyStyle.Brace when lang is "javascript" or "typescript" => FindJavaScriptBraceRange(lines, startIndex, lang, startColumn), BodyStyle.Brace when lang == "csharp" => FindCSharpBraceRange(lines, startIndex, startColumn), - BodyStyle.Brace when lang == "java" => FindJavaBraceRange(lines, startIndex, startColumn), - BodyStyle.Brace => FindBraceRange(lines, startIndex, startColumn, lang), - BodyStyle.Indent => FindIndentRange(lines, startIndex), - BodyStyle.RubyEnd => FindRubyRange(lines, startIndex), - BodyStyle.FortranEnd => FindFortranRange(lines, startIndex), - BodyStyle.ElixirEnd => FindElixirRange(lines, startIndex), - BodyStyle.VisualBasicEnd => FindVisualBasicRange(lines, startIndex), - BodyStyle.PascalEnd => FindPascalRange(lines, startIndex), - BodyStyle.SmalltalkMethod => FindSmalltalkMethodRange(lines, startIndex), - BodyStyle.SqlProcBody => FindSqlProcBodyRange(lines, startIndex), - _ => (startIndex + 1, null, null), - }; + BodyStyle.Brace when lang == "java" => FindJavaBraceRange(lines, startIndex, startColumn), + BodyStyle.Brace => FindBraceRange(lines, startIndex, startColumn, lang), + BodyStyle.Indent => FindIndentRange(lines, startIndex), + BodyStyle.RubyEnd => FindRubyRange(lines, startIndex), + BodyStyle.FortranEnd => FindFortranRange(lines, startIndex), + BodyStyle.ElixirEnd => FindElixirRange(lines, startIndex), + BodyStyle.VisualBasicEnd => FindVisualBasicRange(lines, startIndex), + BodyStyle.PascalEnd => FindPascalRange(lines, startIndex), + BodyStyle.SmalltalkMethod => FindSmalltalkMethodRange(lines, startIndex), + BodyStyle.SqlProcBody => FindSqlProcBodyRange(lines, startIndex), + _ => (startIndex + 1, null, null), + }; } @@ -8181,7 +8181,7 @@ private static string StripRecordComponentComments(string text) continue; } - if (ch == '\'' ) + if (ch == '\'') { inSingleQuote = true; builder.Append(ch); diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index 3c07ae5c59..b33d0fe690 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -2956,8 +2956,8 @@ private JsonNode ExecuteUnusedSymbols(JsonNode? id, JsonNode? args) // Add graph-support metadata for AI trust decisions // AI の信頼判断のためにグラフ対応メタデータを追加 - bool? graphSupported = lang != null ? ReferenceExtractor.SupportsLanguage(lang) : null; - var graphSupportReason = ReferenceExtractor.BuildGraphSupportReason(lang, graphSupported); + bool? graphSupported = lang != null ? ReferenceExtractor.SupportsLanguage(lang) : null; + var graphSupportReason = ReferenceExtractor.BuildGraphSupportReason(lang, graphSupported); return WithDbReader(id, args, reader => { diff --git a/tests/CodeIndex.Tests/ConcurrencyTests.cs b/tests/CodeIndex.Tests/ConcurrencyTests.cs index c2920c072f..b715d8c326 100644 --- a/tests/CodeIndex.Tests/ConcurrencyTests.cs +++ b/tests/CodeIndex.Tests/ConcurrencyTests.cs @@ -31,7 +31,10 @@ public async Task ConcurrentReads_DoNotBlock() var writer = new DbWriter(_db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/app.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = "src/app.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = ManualTimeProvider.FixtureUtcNow.UtcDateTime, Checksum = "abc", }); @@ -63,7 +66,10 @@ public async Task ConcurrentReadDuringWrite_Succeeds() // Pre-seed a file so reads have something to find writer.UpsertFile(new FileRecord { - Path = "src/seed.cs", Lang = "csharp", Size = 50, Lines = 5, + Path = "src/seed.cs", + Lang = "csharp", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), Checksum = "seed", }); @@ -77,7 +83,10 @@ public async Task ConcurrentReadDuringWrite_Succeeds() var w = new DbWriter(writeDb.Connection); w.UpsertFile(new FileRecord { - Path = $"src/file{i}.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = $"src/file{i}.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = DateTime.UtcNow, Checksum = $"hash{i}", }); @@ -125,7 +134,10 @@ public async Task GetStatus_ReferencesAndFilesStaySnapshotConsistent_UnderConcur { var fileId = writer.UpsertFile(new FileRecord { - Path = $"src/seed{seedIndex}.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = $"src/seed{seedIndex}.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = ManualTimeProvider.FixtureUtcNow.UtcDateTime, Checksum = $"seed{seedIndex}", }); @@ -154,7 +166,10 @@ public async Task GetStatus_ReferencesAndFilesStaySnapshotConsistent_UnderConcur using var txn = w.BeginTransaction(); var fileId = w.UpsertFile(new FileRecord { - Path = $"src/added{extra}.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = $"src/added{extra}.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = ManualTimeProvider.FixtureUtcNow.UtcDateTime, Checksum = $"added{extra}", }); @@ -222,7 +237,10 @@ public async Task AnalyzeSymbol_RefsCallersAndFreshnessStaySnapshotConsistent_Un var writer = new DbWriter(_db.Connection); var fileAId = writer.UpsertFile(new FileRecord { - Path = "src/A.cs", Lang = "csharp", Size = 100, Lines = 20, + Path = "src/A.cs", + Lang = "csharp", + Size = 100, + Lines = 20, Modified = fileAModified, Checksum = "A", }); @@ -272,7 +290,10 @@ public async Task AnalyzeSymbol_RefsCallersAndFreshnessStaySnapshotConsistent_Un { var fileBId = w.UpsertFile(new FileRecord { - Path = "src/B.cs", Lang = "csharp", Size = 80, Lines = 10, + Path = "src/B.cs", + Lang = "csharp", + Size = 80, + Lines = 10, Modified = fileBModified, Checksum = "B", }); @@ -367,7 +388,10 @@ public async Task GetRepoMap_FreshnessAndEntrypointsStaySnapshotConsistent_Under { writer.UpsertFile(new FileRecord { - Path = $"src/seed{seedIndex}.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = $"src/seed{seedIndex}.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = baselineModified, Checksum = $"seed{seedIndex}", }); @@ -398,7 +422,10 @@ public async Task GetRepoMap_FreshnessAndEntrypointsStaySnapshotConsistent_Under { var fileId = w.UpsertFile(new FileRecord { - Path = toggledPath, Lang = "csharp", Size = 120, Lines = 12, + Path = toggledPath, + Lang = "csharp", + Size = 120, + Lines = 12, Modified = newerModified, Checksum = "newer", }); diff --git a/tests/CodeIndex.Tests/DatabaseTests.cs b/tests/CodeIndex.Tests/DatabaseTests.cs index 33d93a273f..836fba933e 100644 --- a/tests/CodeIndex.Tests/DatabaseTests.cs +++ b/tests/CodeIndex.Tests/DatabaseTests.cs @@ -1286,12 +1286,18 @@ public void UpsertFile_ReplacesOnConflict() // 同一パスは置換される(重複しない) var file1 = new FileRecord { - Path = "src/app.py", Lang = "python", Size = 100, Lines = 10, + Path = "src/app.py", + Lang = "python", + Size = 100, + Lines = 10, Modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), }; var file2 = new FileRecord { - Path = "src/app.py", Lang = "python", Size = 200, Lines = 20, + Path = "src/app.py", + Lang = "python", + Size = 200, + Lines = 20, Modified = new DateTime(2025, 2, 1, 0, 0, 0, DateTimeKind.Utc), }; @@ -1308,7 +1314,10 @@ public void GetUnchangedFileId_ReturnIdIfUnchanged() var modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); var file = new FileRecord { - Path = "src/lib.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/lib.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = modified, }; _writer.UpsertFile(file); @@ -1330,7 +1339,10 @@ public void GetUnchangedFileId_WithNullChecksumUsesModifiedAndSize() var modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); var file = new FileRecord { - Path = "src/size.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/size.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = modified, }; _writer.UpsertFile(file); @@ -1348,7 +1360,10 @@ public void GetUnchangedFileId_ReturnsNullWhenLanguageExtractorVersionIsStale() var modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); var file = new FileRecord { - Path = "src/lib.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/lib.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = modified, }; _writer.UpsertFile(file); @@ -1366,8 +1381,12 @@ public void GetUnchangedFileId_MatchesByChecksumWhenTimestampDiffers() var checksum = "abc123def456"; var file = new FileRecord { - Path = "src/checksum.py", Lang = "python", Size = 50, Lines = 5, - Modified = modified, Checksum = checksum, + Path = "src/checksum.py", + Lang = "python", + Size = 50, + Lines = 5, + Modified = modified, + Checksum = checksum, }; _writer.UpsertFile(file); @@ -1413,8 +1432,12 @@ public void GetUnchangedFileId_ReturnsNullWhenTimestampMatchesButChecksumDiffers var modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); var file = new FileRecord { - Path = "src/coarse-time.py", Lang = "python", Size = 50, Lines = 5, - Modified = modified, Checksum = "first_checksum", + Path = "src/coarse-time.py", + Lang = "python", + Size = 50, + Lines = 5, + Modified = modified, + Checksum = "first_checksum", }; _writer.UpsertFile(file); @@ -1436,18 +1459,30 @@ public void PurgeStaleFilesSharingChecksum_RemovesDeletedRenameRowsOnly() var modified = new DateTime(2026, 5, 18, 0, 0, 0, DateTimeKind.Utc); var currentId = _writer.UpsertFile(new FileRecord { - Path = "src/current.py", Lang = "python", Size = 14, Lines = 1, - Checksum = "same_checksum", Modified = modified, + Path = "src/current.py", + Lang = "python", + Size = 14, + Lines = 1, + Checksum = "same_checksum", + Modified = modified, }); var staleId = _writer.UpsertFile(new FileRecord { - Path = "src/renamed-away.py", Lang = "python", Size = 14, Lines = 1, - Checksum = "same_checksum", Modified = modified, + Path = "src/renamed-away.py", + Lang = "python", + Size = 14, + Lines = 1, + Checksum = "same_checksum", + Modified = modified, }); var duplicateId = _writer.UpsertFile(new FileRecord { - Path = "src/duplicate.py", Lang = "python", Size = 14, Lines = 1, - Checksum = "same_checksum", Modified = modified, + Path = "src/duplicate.py", + Lang = "python", + Size = 14, + Lines = 1, + Checksum = "same_checksum", + Modified = modified, }); _writer.InsertChunks([ new() { FileId = currentId, ChunkIndex = 0, StartLine = 1, EndLine = 1, Content = "current" }, @@ -1477,7 +1512,10 @@ public void InsertChunks_InsertsAndPopulatesFts() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/test.py", Lang = "python", Size = 100, Lines = 10, + Path = "src/test.py", + Lang = "python", + Size = 100, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1500,7 +1538,10 @@ public void InsertChunks_MultiRowValuesPopulatesFtsForEveryRow() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/multi.py", Lang = "python", Size = 300, Lines = 300, + Path = "src/multi.py", + Lang = "python", + Size = 300, + Lines = 300, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1527,7 +1568,10 @@ public void InsertSymbols_InsertsCorrectly() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/svc.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/svc.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1547,7 +1591,10 @@ public void InsertSymbols_ChunksLargeInputUnderSqlVariableLimit() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/symbols.py", Lang = "python", Size = 1000, Lines = 1000, + Path = "src/symbols.py", + Lang = "python", + Size = 1000, + Lines = 1000, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var symbols = Enumerable.Range(0, 120) @@ -1573,7 +1620,10 @@ public void InsertSymbols_BatchFailureSkipsOnlyBadRow() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/symbols_with_bad_row.py", Lang = "python", Size = 1000, Lines = 1000, + Path = "src/symbols_with_bad_row.py", + Lang = "python", + Size = 1000, + Lines = 1000, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var warnings = new List(); @@ -1612,7 +1662,10 @@ public void DeleteFileData_RemovesChunksAndSymbols() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/del.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/del.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1632,7 +1685,10 @@ public void InsertReferences_InsertsCorrectly() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/ref.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/ref.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1649,7 +1705,10 @@ public void InsertReferences_ChunksLargeInputAndDeduplicatesReferenceLines() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/refs.py", Lang = "python", Size = 1000, Lines = 1000, + Path = "src/refs.py", + Lang = "python", + Size = 1000, + Lines = 1000, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var references = Enumerable.Range(0, 120) @@ -1687,52 +1746,82 @@ public void RebuildTypeScriptAugmentationReferences_LinksMergedInterfacesOnly() var firstFileId = _writer.UpsertFile(new FileRecord { - Path = "src/a.ts", Lang = "typescript", Size = 80, Lines = 4, + Path = "src/a.ts", + Lang = "typescript", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var secondFileId = _writer.UpsertFile(new FileRecord { - Path = "src/b.ts", Lang = "typescript", Size = 80, Lines = 4, + Path = "src/b.ts", + Lang = "typescript", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var thirdFileId = _writer.UpsertFile(new FileRecord { - Path = "src/c.ts", Lang = "typescript", Size = 80, Lines = 4, + Path = "src/c.ts", + Lang = "typescript", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var moduleOneFileId = _writer.UpsertFile(new FileRecord { - Path = "src/module-a.ts", Lang = "typescript", Size = 80, Lines = 4, + Path = "src/module-a.ts", + Lang = "typescript", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var moduleTwoFileId = _writer.UpsertFile(new FileRecord { - Path = "src/module-b.ts", Lang = "typescript", Size = 80, Lines = 4, + Path = "src/module-b.ts", + Lang = "typescript", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var moduleMarkerFileId = _writer.UpsertFile(new FileRecord { - Path = "src/module-c.ts", Lang = "typescript", Size = 80, Lines = 2, + Path = "src/module-c.ts", + Lang = "typescript", + Size = 80, + Lines = 2, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var sideEffectImportFileId = _writer.UpsertFile(new FileRecord { - Path = "src/module-d.ts", Lang = "typescript", Size = 80, Lines = 2, + Path = "src/module-d.ts", + Lang = "typescript", + Size = 80, + Lines = 2, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var ambientGlobalFileId = _writer.UpsertFile(new FileRecord { - Path = "src/ambient-global.ts", Lang = "typescript", Size = 80, Lines = 1, + Path = "src/ambient-global.ts", + Lang = "typescript", + Size = 80, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var ambientModuleFirstFileId = _writer.UpsertFile(new FileRecord { - Path = "src/express-a.ts", Lang = "typescript", Size = 80, Lines = 1, + Path = "src/express-a.ts", + Lang = "typescript", + Size = 80, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var ambientModuleSecondFileId = _writer.UpsertFile(new FileRecord { - Path = "src/express-b.ts", Lang = "typescript", Size = 80, Lines = 1, + Path = "src/express-b.ts", + Lang = "typescript", + Size = 80, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1810,7 +1899,10 @@ public void InsertReferences_DeduplicatesReferenceLinesByFileAndLine() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/ref_lines.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/ref_lines.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1844,7 +1936,10 @@ public void InsertReferences_PreservesDistinctReferenceLineContextsForSameFileAn { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/concurrent_ref_lines.py", Lang = "python", Size = 80, Lines = 5, + Path = "src/concurrent_ref_lines.py", + Lang = "python", + Size = 80, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -1994,7 +2089,10 @@ public void InsertReferences_RollsBackChunkOnPartialFailureUnderOuterTransaction // 同じチャンク内で挿入済みの reference_lines が孤児として残ってはならない。 var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/partial.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/partial.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2051,7 +2149,10 @@ public void CleanExistingFileData_PreventsFtsOrphans() // Insert a file with chunks (populates FTS) / ファイルとチャンク(FTS含む)を挿入 var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/orphan.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/orphan.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new() { FileId = fileId, ChunkIndex = 0, StartLine = 1, EndLine = 5, Content = "def hello_orphan_test(): pass" }]); @@ -2067,7 +2168,10 @@ public void CleanExistingFileData_PreventsFtsOrphans() _writer.CleanExistingFileData("src/orphan.py"); var newId = _writer.UpsertFile(new FileRecord { - Path = "src/orphan.py", Lang = "python", Size = 60, Lines = 6, + Path = "src/orphan.py", + Lang = "python", + Size = 60, + Lines = 6, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc).AddMinutes(1), }); _writer.InsertChunks([new() { FileId = newId, ChunkIndex = 0, StartLine = 1, EndLine = 6, Content = "def world_replacement(): pass" }]); @@ -2105,12 +2209,18 @@ public void PurgeStaleFiles_RemovesDeletedFiles() _writer.UpsertFile(new FileRecord { - Path = "real.py", Lang = "python", Size = 5, Lines = 1, + Path = "real.py", + Lang = "python", + Size = 5, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.UpsertFile(new FileRecord { - Path = "ghost.py", Lang = "python", Size = 10, Lines = 2, + Path = "ghost.py", + Lang = "python", + Size = 10, + Lines = 2, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2182,7 +2292,10 @@ public void DropAll_RemovesAllTables() // データを挿入してから全削除 _writer.UpsertFile(new FileRecord { - Path = "src/x.py", Lang = "python", Size = 10, Lines = 1, + Path = "src/x.py", + Lang = "python", + Size = 10, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2203,7 +2316,10 @@ public void DeleteFileByPath_RemovesFileAndData() // ファイルとチャンク・シンボルを挿入し、パスで削除 var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/remove_me.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/remove_me.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2236,12 +2352,18 @@ public void DeleteFileByPath_DoesNotAffectOtherFiles() // 1ファイルの削除は他のファイルに影響しない _writer.UpsertFile(new FileRecord { - Path = "src/keep.py", Lang = "python", Size = 50, Lines = 5, + Path = "src/keep.py", + Lang = "python", + Size = 50, + Lines = 5, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.UpsertFile(new FileRecord { - Path = "src/delete.py", Lang = "python", Size = 30, Lines = 3, + Path = "src/delete.py", + Lang = "python", + Size = 30, + Lines = 3, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2256,7 +2378,10 @@ public void MarkFoldReady_StampsFoldReadyWhenAllRowsBackfilled() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/fold_ok.py", Lang = "python", Size = 30, Lines = 3, + Path = "src/fold_ok.py", + Lang = "python", + Size = 30, + Lines = 3, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertSymbols([ @@ -2280,7 +2405,10 @@ public void MarkFoldReady_LeavesFoldReadyUnsetWhenNullFoldedRowExists() // 修正後の MarkFoldReady は再検証で stamp を取りやめ、reader を NOCASE に保つ。 var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/fold_partial.py", Lang = "python", Size = 30, Lines = 3, + Path = "src/fold_partial.py", + Lang = "python", + Size = 30, + Lines = 3, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertSymbols([ @@ -2343,7 +2471,10 @@ public async Task TransactionScope_DisposeIsAtomicUnderConcurrentCalls() var scope = _writer.BeginTransaction(); _writer.UpsertFile(new FileRecord { - Path = "src/rolled_back.py", Lang = "python", Size = 10, Lines = 1, + Path = "src/rolled_back.py", + Lang = "python", + Size = 10, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2359,7 +2490,10 @@ public async Task TransactionScope_DisposeIsAtomicUnderConcurrentCalls() using var nextScope = _writer.BeginTransaction(); _writer.UpsertFile(new FileRecord { - Path = "src/next.py", Lang = "python", Size = 10, Lines = 1, + Path = "src/next.py", + Lang = "python", + Size = 10, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); nextScope.Commit(); @@ -2376,7 +2510,10 @@ public async Task TransactionScope_CommitDisposeRaceDoesNotSurfaceDoubleRollback var scope = _writer.BeginTransaction(); _writer.UpsertFile(new FileRecord { - Path = $"src/race_{i}.py", Lang = "python", Size = 10, Lines = 1, + Path = $"src/race_{i}.py", + Lang = "python", + Size = 10, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -2408,7 +2545,10 @@ public async Task TransactionScope_CommitDisposeRaceDoesNotSurfaceDoubleRollback using var nextScope = _writer.BeginTransaction(); _writer.UpsertFile(new FileRecord { - Path = "src/after_race.py", Lang = "python", Size = 10, Lines = 1, + Path = "src/after_race.py", + Lang = "python", + Size = 10, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); nextScope.Commit(); diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 31e3a9d96f..54fa06a07a 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -323,7 +323,10 @@ private void SeedData() const string authContent = "def authenticate(user, password):\n if user == 'admin':\n return True\n return False"; var pyId = _writer.UpsertFile(new FileRecord { - Path = "src/auth.py", Lang = "python", Size = 500, Lines = 30, + Path = "src/auth.py", + Lang = "python", + Size = 500, + Lines = 30, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -346,7 +349,10 @@ private void SeedData() const string apiContent = "export class ApiClient {\n async fetchData(url) {\n return fetch(url)\n }\n}"; var jsId = _writer.UpsertFile(new FileRecord { - Path = "src/api.js", Lang = "javascript", Size = 800, Lines = 50, + Path = "src/api.js", + Lang = "javascript", + Size = 800, + Lines = 50, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -1211,7 +1217,10 @@ public void Search_PrefersSourceFilesOverTests() { var testFileId = _writer.UpsertFile(new FileRecord { - Path = "tests/auth_test.py", Lang = "python", Size = 300, Lines = 10, + Path = "tests/auth_test.py", + Lang = "python", + Size = 300, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -1234,7 +1243,10 @@ public void Search_DeduplicatesFullyCoveredChunk() // 同じファイル内で低順位のマッチが完全包含される2チャンクを作成。 var overlapFileId = _writer.UpsertFile(new FileRecord { - Path = "src/overlap.py", Lang = "python", Size = 2000, Lines = 100, + Path = "src/overlap.py", + Lang = "python", + Size = 2000, + Lines = 100, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var duplicateContent = "# overlap_marker\ndef func_a():\n pass\n" + string.Concat(Enumerable.Repeat("# filler\n", 76)); @@ -1256,7 +1268,10 @@ public void Search_TiedChunksUseStableChunkIdOrder() { var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/tied_chunks.py", Lang = "python", Size = 3000, Lines = 260, + Path = "src/tied_chunks.py", + Lang = "python", + Size = 3000, + Lines = 260, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([ @@ -1288,7 +1303,10 @@ public void Search_PrefersDefinitionFileOverReferenceOnlySourceFile() { var refFileId = _writer.UpsertFile(new FileRecord { - Path = "src/session.py", Lang = "python", Size = 300, Lines = 10, + Path = "src/session.py", + Lang = "python", + Size = 300, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -2375,7 +2393,10 @@ public void SearchSymbols_ExactMatchesNameEqualityAcrossMultipleNames() // exact=false は substring なので `authenticate_v2` も引き当てるが、exact=true は名前一致のみ。 var extraFileId = _writer.UpsertFile(new FileRecord { - Path = "src/auth_v2.py", Lang = "python", Size = 80, Lines = 4, + Path = "src/auth_v2.py", + Lang = "python", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertSymbols([ @@ -2408,7 +2429,10 @@ public void SearchSymbols_ExactFoldsNonAsciiCasing() // `--exact` で同一視できることを確認する。 var extraFileId = _writer.UpsertFile(new FileRecord { - Path = "src/intl.py", Lang = "python", Size = 120, Lines = 6, + Path = "src/intl.py", + Lang = "python", + Size = 120, + Lines = 6, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertSymbols([ @@ -2492,7 +2516,10 @@ public void AllFoldedColumnsBackfilled_DetectsLegacyRowsWithNullFoldedValues() // writer 経由で入れた行は folded 付き。 var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2541,7 +2568,10 @@ public void GetStatus_WithFoldRowVerification_DegradesWhenReadyBitRowsAreIncompl var writer = new DbWriter(db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2580,7 +2610,10 @@ public void GetStatus_WithFoldRowVerification_IgnoresMissingReferenceTable() var writer = new DbWriter(db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2628,7 +2661,10 @@ public void AllFoldedColumnsBackfilled_DetectsEveryPartialFoldColumnState( var writer = new DbWriter(db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2694,7 +2730,10 @@ public void GetExactGraphSupportedDefinitionLanguage_DegradesOnLegacyDbMissingCo var writer = new DbWriter(db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/worker.cs", Lang = "csharp", Size = 40, Lines = 4, + Path = "src/worker.cs", + Lang = "csharp", + Size = 40, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2760,7 +2799,10 @@ public void SearchSymbols_ExactFallsBackToNocaseWhenFoldKeyVersionMismatches() var writer = new DbWriter(db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2804,7 +2846,10 @@ public void SearchSymbols_ExactFallsBackToNocaseWhenFoldFingerprintMismatches() var writer = new DbWriter(db.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2842,7 +2887,10 @@ public void SearchSymbols_ExactFallsBackToNocaseWhenFoldNotReady() var writer = new DbWriter(legacyDb.Connection); var fileId = writer.UpsertFile(new FileRecord { - Path = "src/a.py", Lang = "python", Size = 1, Lines = 1, + Path = "src/a.py", + Lang = "python", + Size = 1, + Lines = 1, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); writer.InsertSymbols([ @@ -2953,7 +3001,10 @@ public void SearchSymbols_ExcludeTests_RemovesLikelyTestPaths() { var testFileId = _writer.UpsertFile(new FileRecord { - Path = "tests/auth_test.py", Lang = "python", Size = 300, Lines = 10, + Path = "tests/auth_test.py", + Lang = "python", + Size = 300, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertSymbols([ @@ -3004,7 +3055,10 @@ public void Search_ExcludeTests_RemovesLikelyTestPaths() { var testFileId = _writer.UpsertFile(new FileRecord { - Path = "tests/auth_test.py", Lang = "python", Size = 300, Lines = 10, + Path = "tests/auth_test.py", + Lang = "python", + Size = 300, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -4623,7 +4677,10 @@ public void GetDefinitions_ExactMatchesNameEquality() { var extraFileId = _writer.UpsertFile(new FileRecord { - Path = "src/auth_v2.py", Lang = "python", Size = 80, Lines = 4, + Path = "src/auth_v2.py", + Lang = "python", + Size = 80, + Lines = 4, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -13366,7 +13423,10 @@ public void GetOutline_NullStartEndLine_FallsBackToLine() // start_line/end_lineがNULLのシンボルを持つファイルを挿入(#46) var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/nullcol.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = "src/nullcol.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); _writer.InsertChunks([new ChunkRecord @@ -16492,7 +16552,10 @@ public void GetUnusedSymbols_NullStartEndLine_DoesNotCrash() // リグレッション: #49 — 古いインデックスは symbols 行の start_line/end_line が NULL になりうる。 var fileId = _writer.UpsertFile(new FileRecord { - Path = "src/unused_null.cs", Lang = "csharp", Size = 100, Lines = 10, + Path = "src/unused_null.cs", + Lang = "csharp", + Size = 100, + Lines = 10, Modified = new DateTime(2025, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); using var cmd = _db.Connection.CreateCommand(); diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 058274e251..1f215ce763 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -648,16 +648,16 @@ public void ScanFiles_IndexesIssue189FilenameAndExtensionCoverage() Directory.CreateDirectory(tempDir); var files = new Dictionary(StringComparer.Ordinal) { - ["Gemfile"] = "source 'https://rubygems.org'\ngem 'rails', '~> 7.0'\n", - ["Rakefile"] = "task :default => [:test]\n", - ["Containerfile"] = "FROM alpine\nRUN echo hi\n", - ["Dockerfile.dev"] = "FROM alpine AS builder\nRUN echo dev\n", - ["GNUmakefile"] = "all:\n\techo hi\n", - ["common.mk"] = "OBJ = foo.o bar.o\n", - ["stub.pyi"] = "def foo() -> int: ...\n", - ["style.less"] = ".foo { color: red; }\n", - ["page.htm"] = "old-school\n", - ["Makefile.am"] = "SUBDIRS = lib\n", + ["Gemfile"] = "source 'https://rubygems.org'\ngem 'rails', '~> 7.0'\n", + ["Rakefile"] = "task :default => [:test]\n", + ["Containerfile"] = "FROM alpine\nRUN echo hi\n", + ["Dockerfile.dev"] = "FROM alpine AS builder\nRUN echo dev\n", + ["GNUmakefile"] = "all:\n\techo hi\n", + ["common.mk"] = "OBJ = foo.o bar.o\n", + ["stub.pyi"] = "def foo() -> int: ...\n", + ["style.less"] = ".foo { color: red; }\n", + ["page.htm"] = "old-school\n", + ["Makefile.am"] = "SUBDIRS = lib\n", }; foreach (var (name, content) in files) File.WriteAllText(Path.Combine(tempDir, name), content); @@ -2715,7 +2715,7 @@ public void ScanFiles_ExcludesUnknownExtensionEvenWhenShebangLooksSupported() } finally { - Directory.Delete(tempDir, true); + Directory.Delete(tempDir, true); } } diff --git a/tests/CodeIndex.Tests/McpServerTests.cs b/tests/CodeIndex.Tests/McpServerTests.cs index bd028c52d8..97c54fc529 100644 --- a/tests/CodeIndex.Tests/McpServerTests.cs +++ b/tests/CodeIndex.Tests/McpServerTests.cs @@ -9078,7 +9078,8 @@ public void SuggestImprovement_ValidInput_ReturnsSuccess() var uniqueDesc = $"Arrow functions are not detected as symbols {Guid.NewGuid():N}"; var json = new JsonObject { - ["jsonrpc"] = "2.0", ["id"] = 1, + ["jsonrpc"] = "2.0", + ["id"] = 1, ["method"] = "tools/call", ["params"] = new JsonObject { @@ -9103,7 +9104,8 @@ public void SuggestImprovement_CrashReport_ReturnsSuccess() var uniqueDesc = $"NullReferenceException when searching with empty query {Guid.NewGuid():N}"; var json = new JsonObject { - ["jsonrpc"] = "2.0", ["id"] = 1, + ["jsonrpc"] = "2.0", + ["id"] = 1, ["method"] = "tools/call", ["params"] = new JsonObject { @@ -9126,7 +9128,8 @@ public void SuggestImprovement_RecordsClientAttributionFromInitialize() var uniqueDesc = $"Attribution metadata regression {Guid.NewGuid():N}"; var json = new JsonObject { - ["jsonrpc"] = "2.0", ["id"] = 1, + ["jsonrpc"] = "2.0", + ["id"] = 1, ["method"] = "tools/call", ["params"] = new JsonObject { @@ -9160,7 +9163,8 @@ public void SuggestImprovement_DuplicateSubmission_ReturnsDuplicate() var uniqueDesc = $"Add support for Zig language {Guid.NewGuid():N}"; JsonNode MakeRequest(int id) => new JsonObject { - ["jsonrpc"] = "2.0", ["id"] = id, + ["jsonrpc"] = "2.0", + ["id"] = id, ["method"] = "tools/call", ["params"] = new JsonObject { @@ -9285,7 +9289,8 @@ public void SuggestImprovement_SourceCodeInDescription_ReturnsError() var desc = "public void Foo()\n{\n var x = 1;\n var y = 2;\n var z = x + y;\n Console.WriteLine(z);\n}"; var json = new JsonObject { - ["jsonrpc"] = "2.0", ["id"] = 1, + ["jsonrpc"] = "2.0", + ["id"] = 1, ["method"] = "tools/call", ["params"] = new JsonObject { @@ -9305,7 +9310,8 @@ public void SuggestImprovement_SourceCodeInContext_ReturnsError() var ctx = "function foo() {\n let x = 1;\n let y = 2;\n return x + y;\n}"; var json = new JsonObject { - ["jsonrpc"] = "2.0", ["id"] = 1, + ["jsonrpc"] = "2.0", + ["id"] = 1, ["method"] = "tools/call", ["params"] = new JsonObject { diff --git a/tests/CodeIndex.Tests/PreparedCommandCacheTests.cs b/tests/CodeIndex.Tests/PreparedCommandCacheTests.cs index 79be0d33f9..a0f49fa2fa 100644 --- a/tests/CodeIndex.Tests/PreparedCommandCacheTests.cs +++ b/tests/CodeIndex.Tests/PreparedCommandCacheTests.cs @@ -250,7 +250,11 @@ public void DbWriter_WithCache_NestedSavepointStillBindsToOuterTransaction() using var outerTxn = writer.BeginTransaction(); writer.UpsertFile(new FileRecord { - Path = "src/outer.py", Lang = "python", Size = 1, Lines = 1, Checksum = "o", + Path = "src/outer.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "o", Modified = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), }); @@ -260,7 +264,11 @@ public void DbWriter_WithCache_NestedSavepointStillBindsToOuterTransaction() // インナー savepoint 内で同じ cached command を再借用する。 writer.UpsertFile(new FileRecord { - Path = "src/inner.py", Lang = "python", Size = 1, Lines = 1, Checksum = "i", + Path = "src/inner.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "i", Modified = new DateTime(2025, 1, 2, 0, 0, 0, DateTimeKind.Utc), }); Assert.True(writer.HasFileAtPath("src/inner.py")); @@ -273,7 +281,11 @@ public void DbWriter_WithCache_NestedSavepointStillBindsToOuterTransaction() // 再借用は outer txn にバインドされる。 writer.UpsertFile(new FileRecord { - Path = "src/outer2.py", Lang = "python", Size = 1, Lines = 1, Checksum = "o2", + Path = "src/outer2.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "o2", Modified = new DateTime(2025, 1, 3, 0, 0, 0, DateTimeKind.Utc), }); outerTxn.Commit(); @@ -291,12 +303,20 @@ public void DbWriter_WithCache_GetUnchangedFileIdReusesCacheAcrossFiles() writer.UpsertFile(new FileRecord { - Path = "src/x.py", Lang = "python", Size = 1, Lines = 1, Checksum = "k1", + Path = "src/x.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "k1", Modified = modified, }); writer.UpsertFile(new FileRecord { - Path = "src/y.py", Lang = "python", Size = 1, Lines = 1, Checksum = "k2", + Path = "src/y.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "k2", Modified = modified, }); @@ -320,8 +340,12 @@ public void DbWriter_WithCache_GetUnchangedFileIdTouchUpdatesTimestamp() writer.UpsertFile(new FileRecord { - Path = "src/touched.py", Lang = "python", Size = 1, Lines = 1, - Checksum = "same_checksum", Modified = initial, + Path = "src/touched.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "same_checksum", + Modified = initial, }); // First call with a new timestamp + identical checksum triggers the touch. @@ -355,8 +379,12 @@ public void DbWriter_WithCache_GetUnchangedFileIdDoesNotTouchWhenChecksumDrifts_ writer.UpsertFile(new FileRecord { - Path = "src/drift.py", Lang = "python", Size = 1, Lines = 1, - Checksum = "old_checksum", Modified = initial, + Path = "src/drift.py", + Lang = "python", + Size = 1, + Lines = 1, + Checksum = "old_checksum", + Modified = initial, }); Assert.Null(writer.GetUnchangedFileId("src/drift.py", touched, "new_checksum")); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 7b8da58d61..1fd807502e 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -1449,11 +1449,11 @@ void run() {{ Assert.Equal("1", stdout.Trim()); Assert.Equal(string.Empty, stderr); } - finally - { - TestProjectHelper.DeleteDirectory(projectRoot); + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } } -} [Theory] [InlineData("js")] @@ -25936,7 +25936,7 @@ public enum Status { Ready } MarkGraphAndFoldReady(dbPath); var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunReferences( - ["Ready", "--db", dbPath, "--json", "--lang", "javascript", "--exact-name", "--path", "web/","--count"], + ["Ready", "--db", dbPath, "--json", "--lang", "javascript", "--exact-name", "--path", "web/", "--count"], _jsonOptions)); using var document = ParseJsonOutput(stdout); diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 92ea866498..0dc130d286 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -21479,11 +21479,11 @@ void Run(object value) Assert.DoesNotContain(references, r => r.SymbolName == "Point" && r.ReferenceKind == "call"); } - [Fact] - public void Extract_CsharpMultilinePositionalPatterns_CaptureTypeReferences() - { - // issue #969: multiline positional `case` / `is` heads must behave the same as - // the same-line forms and keep the real `type_reference` without phantom calls. + [Fact] + public void Extract_CsharpMultilinePositionalPatterns_CaptureTypeReferences() + { + // issue #969: multiline positional `case` / `is` heads must behave the same as + // the same-line forms and keep the real `type_reference` without phantom calls. // issue #969: 改行をまたぐ positional `case` / `is` head も同一行版と同様に // 本物の `type_reference` を残し、phantom な call を出してはならない。 const string content = """ @@ -21596,15 +21596,15 @@ class Demo var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); var pointRefs = references.Where(r => r.SymbolName == "Point" && r.ReferenceKind == "type_reference").ToList(); - Assert.Equal(2, pointRefs.Count); - Assert.All(pointRefs, r => Assert.Equal("Match", r.ContainerName)); - Assert.DoesNotContain(references, r => r.SymbolName == "Point" && r.ReferenceKind == "call"); - } - - [Fact] - public void Extract_CsharpCaseLogicalAndNegatedTypePatterns_CaptureTypeReferences() - { - // issues #668/#670: logical/negated type patterns must keep the left-hand type + Assert.Equal(2, pointRefs.Count); + Assert.All(pointRefs, r => Assert.Equal("Match", r.ContainerName)); + Assert.DoesNotContain(references, r => r.SymbolName == "Point" && r.ReferenceKind == "call"); + } + + [Fact] + public void Extract_CsharpCaseLogicalAndNegatedTypePatterns_CaptureTypeReferences() + { + // issues #668/#670: logical/negated type patterns must keep the left-hand type // dependency for both unqualified and qualified heads without reclassifying enum // member labels such as `Color.Red or Probe.Color.Blue` as type dependencies. // issues #668/#670: logical/negated な型パターンは unqualified / qualified の両方で diff --git a/tools/CodeIndex.Changelog/Program.cs b/tools/CodeIndex.Changelog/Program.cs index 6d23569c00..87d6ab4f0a 100644 --- a/tools/CodeIndex.Changelog/Program.cs +++ b/tools/CodeIndex.Changelog/Program.cs @@ -24,31 +24,31 @@ public static int Main(string[] args) switch (command) { case "check": - { - var summary = tool.CheckFragments(); - Console.Out.WriteLine(summary); - return 0; - } + { + var summary = tool.CheckFragments(); + Console.Out.WriteLine(summary); + return 0; + } case "prepare": - { - var options = ParseOptions(args[1..], requireDate: true); - var result = tool.Prepare(options.Version, options.ReleaseDate, writeChanges: true); - Console.Out.WriteLine(result.Summary); - return 0; - } + { + var options = ParseOptions(args[1..], requireDate: true); + var result = tool.Prepare(options.Version, options.ReleaseDate, writeChanges: true); + Console.Out.WriteLine(result.Summary); + return 0; + } case "render": - { - var options = ParseOptions(args[1..], requireDate: true); - var result = tool.Prepare(options.Version, options.ReleaseDate, writeChanges: false); - Console.Out.Write(result.RenderedChangelog ?? string.Empty); - return 0; - } + { + var options = ParseOptions(args[1..], requireDate: true); + var result = tool.Prepare(options.Version, options.ReleaseDate, writeChanges: false); + Console.Out.Write(result.RenderedChangelog ?? string.Empty); + return 0; + } case "release-notes": - { - var options = ParseOptions(args[1..], requireDate: false); - Console.Out.Write(tool.RenderReleaseNotes(options.Version)); - return 0; - } + { + var options = ParseOptions(args[1..], requireDate: false); + Console.Out.Write(tool.RenderReleaseNotes(options.Version)); + return 0; + } default: throw new ChangelogException($"Unknown command '{command}'."); }