diff --git a/changelog.d/unreleased/3656.internal.md b/changelog.d/unreleased/3656.internal.md new file mode 100644 index 0000000000..0d9f75a77a --- /dev/null +++ b/changelog.d/unreleased/3656.internal.md @@ -0,0 +1,15 @@ +--- +category: internal +issues: + - 3656 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.cs +--- + +## English + +- **Removed the stale dry-run probe helper (#3656)** — deleted the unused old probe helper while leaving the active full-scan checkpoint constants wired to the current implementation. + +## 日本語 + +- **古い dry-run probe helper を削除しました (#3656)** — 未使用の旧 probe helper を削除し、現行実装で使われている full-scan checkpoint 定数は維持しました。 diff --git a/changelog.d/unreleased/3669.fixed.md b/changelog.d/unreleased/3669.fixed.md new file mode 100644 index 0000000000..c4b1658a9c --- /dev/null +++ b/changelog.d/unreleased/3669.fixed.md @@ -0,0 +1,19 @@ +--- +category: fixed +issues: + - 3669 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs +--- + +## English + +- **Improved unused audit scoping (#3669)** — `cdidx unused --audit-scope source` now focuses cleanup output on private/internal source candidates while excluding documentation-shaped paths; `--audit-scope all` keeps the caller's explicit filters. + +## 日本語 + +- **unused の audit scope を改善しました (#3669)** — `cdidx unused --audit-scope source` は documentation 形状の path を除外しつつ private/internal の source cleanup 候補に絞るようになりました。`--audit-scope all` は呼び出し側の明示 filter のみを使います。 diff --git a/changelog.d/unreleased/3671.internal.md b/changelog.d/unreleased/3671.internal.md new file mode 100644 index 0000000000..608f8ddcef --- /dev/null +++ b/changelog.d/unreleased/3671.internal.md @@ -0,0 +1,19 @@ +--- +category: internal +issues: + - 3671 +affected: + - src/CodeIndex/Cli/IndexLock.cs + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Database/DbReader.CSharpResolution.cs + - src/CodeIndex/Database/DbReader.References.cs + - src/CodeIndex/Database/DbSymbolReader.cs +--- + +## English + +- **Removed stale private CLI/database helpers (#3671)** — deleted unused helper methods after confirming they were not called by current CLI or reader paths. + +## 日本語 + +- **CLI / database の古い private helper を削除しました (#3671)** — 現行 CLI / reader 経路から呼ばれていないことを確認した未使用 helper を削除しました。 diff --git a/changelog.d/unreleased/3672.internal.md b/changelog.d/unreleased/3672.internal.md new file mode 100644 index 0000000000..b104e83710 --- /dev/null +++ b/changelog.d/unreleased/3672.internal.md @@ -0,0 +1,18 @@ +--- +category: internal +issues: + - 3672 +affected: + - src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs + - src/CodeIndex/Indexer/References/Languages/LuaReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs +--- + +## English + +- **Removed stale reference extractor helpers (#3672)** — deleted unreferenced helper methods and fields after confirming they are not called by current extraction paths. + +## 日本語 + +- **古い reference extractor helper を削除しました (#3672)** — 現行の抽出経路から呼ばれていない未使用 helper method と field を削除しました。 diff --git a/changelog.d/unreleased/3679.fixed.md b/changelog.d/unreleased/3679.fixed.md new file mode 100644 index 0000000000..fbac2e0c46 --- /dev/null +++ b/changelog.d/unreleased/3679.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 3679 +affected: + - src/CodeIndex/Cli/QueryCommandRunner.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs +--- + +## English + +- **Improved compact unused output (#3679)** — compact unused JSON now includes representative symbol samples by bucket while continuing to omit the full `symbols` section. + +## 日本語 + +- **compact unused 出力を改善しました (#3679)** — full `symbols` セクションは省略したまま、bucket ごとの代表 symbol サンプルを compact unused JSON に含めるようにしました。 diff --git a/changelog.d/unreleased/3691.fixed.md b/changelog.d/unreleased/3691.fixed.md new file mode 100644 index 0000000000..8f7d19c89f --- /dev/null +++ b/changelog.d/unreleased/3691.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 3691 +affected: + - src/CodeIndex/Cli/ConsoleUi.cs + - src/CodeIndex/Cli/CliFlagSchema.cs + - src/CodeIndex/Cli/QueryCommandRunner.cs + - src/CodeIndex/Database/DbSymbolReader.cs + - src/CodeIndex/Models/QueryResults.cs + - tests/CodeIndex.Tests/ConsoleUiTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs +--- + +## English + +- **Added unused result tags and pagination (#3691)** — unused JSON results now include reason tags, and `cdidx unused` can page result sets with `next_cursor` / `--cursor unused:`. + +## 日本語 + +- **unused 結果に tag と pagination を追加しました (#3691)** — unused JSON 結果に reason tag を含め、`next_cursor` / `--cursor unused:` で `cdidx unused` の結果をページングできるようにしました。 diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs index 10b5b098a1..3569a97bc9 100644 --- a/src/CodeIndex/Cli/CliFlagSchema.cs +++ b/src/CodeIndex/Cli/CliFlagSchema.cs @@ -128,6 +128,7 @@ internal static class CliFlagSchema private static readonly string[] SymbolSortCommands = ["symbols"]; private static readonly string[] ByBucketCommands = ["unused"]; private static readonly string[] UnusedFilterCommands = ["unused"]; + private static readonly string[] CursorCommands = ["search", "unused"]; private static readonly string[] AllResultCommands = ["goto", "find"]; private static readonly string[] SinceCommands = ["search", "definition", "symbols", "files", "suggestions"]; @@ -285,7 +286,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--include-query", ValuePlaceholder = "", Description = "Search recipe: include one child query; repeat or comma-separate values", Commands = Set("search") }, new() { Name = "--exclude-query", ValuePlaceholder = "", Description = "Search recipe: exclude one child query; repeat or comma-separate values", Commands = Set("search") }, new() { Name = "--list-recipes", Description = "Search: list built-in audit recipes", Commands = Set("search") }, - new() { Name = "--audit-scope", ValuePlaceholder = "", Description = "Search recipes: use production source defaults or include all indexed paths", Commands = Set("search") }, + new() { Name = "--audit-scope", ValuePlaceholder = "", Description = "Search recipes/Unused: use production source defaults or include all indexed paths", Commands = Set("search", "unused") }, new() { Name = "--show-excluded", Description = "Search recipes: include effective scope and exclusion diagnostics in recipe output", Commands = Set("search") }, new() { Name = "--named-query", ValuePlaceholder = "=", Description = "Search: add one named ad hoc batch query", Commands = Set("search") }, new() { Name = "--open-issues", ValuePlaceholder = "", Description = "Preflight issue drafts against open issue JSON or GitHub open issues", Commands = Set("search", "suggestions") }, @@ -294,7 +295,7 @@ private static IReadOnlyList BuildAll() new() { Name = "--duplicate-threshold", ValuePlaceholder = "", Description = "Issue-drafts: explicit duplicate-preflight minimum score from 0 to 1", Commands = Set("search", "suggestions") }, new() { Name = "--issue-title", ValuePlaceholder = "", Description = "Search issue-drafts: override the title for an ad hoc search draft", Commands = Set("search") }, new() { Name = "--issue-label", ValuePlaceholder = "<label>", Description = "Search issue-drafts: add a label hint; repeat or comma-separate values", Commands = Set("search") }, - new() { Name = "--cursor", ValuePlaceholder = "<cursor>", Description = "Search recipe: pagination cursor returned as next_cursor", Commands = Set("search") }, + new() { Name = "--cursor", ValuePlaceholder = "<cursor>", Description = "Search recipe or unused pagination cursor returned as next_cursor", Commands = Set(CursorCommands) }, new() { Name = "--status", ValuePlaceholder = "<status>", Description = "Suggestions: filter by suggestion status", Commands = Set("suggestions") }, new() { Name = "--category", ValuePlaceholder = "<category>", Description = "Suggestions: filter by category", Commands = Set("suggestions") }, new() { Name = "--agent", ValuePlaceholder = "<agent>", Description = "Suggestions: filter by agent", Commands = Set("suggestions") }, diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs index 23ad1a161f..fe4ff3c2bf 100644 --- a/src/CodeIndex/Cli/ConsoleUi.cs +++ b/src/CodeIndex/Cli/ConsoleUi.cs @@ -112,7 +112,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines = ("validate", "cdidx validate [--db <path>] [--json[=array]] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--severity <info|warning|error>] [--path <glob>]"), ("impact", "cdidx impact <query>|--query <query>|-- <query> [--db <path>] [--json] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--body] [--snippet-lines <n>] [--max-line-width <n>] [--max-hops <n>] [--count] [--with-paths]"), ("deps", "cdidx deps [--db <path>] [--json] [--format <dot|graphml|json-graph|edgelist>] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--reverse] [--cycles]"), - ("unused", "cdidx unused [--db <path>] [--json] [--compact] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--bucket <bucket>] [--min-confidence <medium|low>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--by-bucket]"), + ("unused", "cdidx unused [--db <path>] [--json] [--compact] [--verbose] [--limit <n>|--top <n>] [--cursor <unused:offset>] [--audit-scope <source|all>] [--kind <kind>] [--bucket <bucket>] [--min-confidence <medium|low>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--by-bucket]"), ("hotspots", "cdidx hotspots [--db <path>] [--json] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--group-by <symbol|file|statement>] [--group-by-name]"), ("suggestions", "cdidx suggestions <list|show|export> [id] [--db <path>] [--json] [--status <all|submitted|unsubmitted>] [--language <lang>] [--category <category>] [--since <datetime>] [--agent <name>] [--limit <n>] [--offset <n>] [--format <json|markdown|issue-drafts>] [--open-issues <path|github|github:owner/name>] [--repo <owner/name>] [--duplicate-confidence <low|medium|high>|--duplicate-threshold <score>]"), ("export", "cdidx export <archive> [--db <path>] [--json]"), @@ -1018,7 +1018,7 @@ private static void PrintFlagReference(Action<string> WriteHelpLine) WriteHelpLine(" --named-query <name>=<query> search only: add a named ad hoc batch query; repeat to run related searches with grouped compact results"); Console.WriteLine(" --exclude-path <glob> Exclude glob-style path patterns (* and ?) (repeatable)"); Console.WriteLine(" --exclude-tests Exclude likely test files"); - WriteHelpLine(" --audit-scope <source|all> search recipes only: source uses recipe default production-code paths/excludes; all searches docs, tests, changelog, and recipe definitions unless other filters exclude them"); + WriteHelpLine(" --audit-scope <source|all> search recipes/unused: source uses production-code cleanup defaults; all disables source-scope defaults"); Console.WriteLine(" --exclude-comments search only: suppress comment-only matches"); Console.WriteLine(" --exclude-strings search only: suppress string, regex, and help-text matches"); Console.WriteLine(" --exclude-fixtures search only: suppress fixture-only matches in tests"); diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 558fd14b3a..267afedb3c 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -874,45 +874,6 @@ private static bool IsPathEqualOrParent(string candidateParent, string candidate return PathCasing.IsPathEqualOrParent(normalizedParent, normalizedChild); } - private static bool TryProbeDryRunFile(FileIndexer indexer, string absolutePath, out string lang, out string? error) - { - lang = string.Empty; - error = null; - - var indexability = indexer.GetFileIndexabilityForIndexing(absolutePath); - if (indexability == FileIndexer.FileProbeStatus.ProbeFailed) - { - error = "Could not probe file for indexability/language."; - return false; - } - - if (indexability != FileIndexer.FileProbeStatus.Supported) - return false; - - var detection = indexer.TryDetectLanguageForIndexing(absolutePath); - if (detection.Status == FileIndexer.FileProbeStatus.ProbeFailed) - { - error = "Could not probe file for indexability/language."; - return false; - } - - if (detection.Status != FileIndexer.FileProbeStatus.Supported) - return false; - - try - { - var (record, _, _, warning) = indexer.BuildRecordWithRawBytes(absolutePath); - lang = record.Lang ?? "unknown"; - error = warning; - return true; - } - catch (Exception ex) - { - error = ex.Message; - return false; - } - } - // Issue #1509: stamp the Git HEAD commit, branch, and UTC timestamp into // codeindex_meta so cross-session staleness ("the DB was indexed at commit X but // you're now at Y, N commits ahead") is detectable by `status` / consumers. Only diff --git a/src/CodeIndex/Cli/IndexLock.cs b/src/CodeIndex/Cli/IndexLock.cs index e9e9d54173..81844f1e85 100644 --- a/src/CodeIndex/Cli/IndexLock.cs +++ b/src/CodeIndex/Cli/IndexLock.cs @@ -209,13 +209,6 @@ private static string SerializeInfo(IndexLockInfo info) return new IndexLockInfo(pid.Value, started.Value); } - private static string EscapeValue(string? value) - { - if (string.IsNullOrEmpty(value)) - return string.Empty; - return value.Replace("\\", "\\\\").Replace("\n", "\\n").Replace("\r", "\\r"); - } - private static string UnescapeValue(string value) { if (string.IsNullOrEmpty(value)) diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs index 88b217d8b6..37288e68c4 100644 --- a/src/CodeIndex/Cli/QueryCommandRunner.cs +++ b/src/CodeIndex/Cli/QueryCommandRunner.cs @@ -436,6 +436,14 @@ public static int RunSearch( "Use `--recipe risky-code/raw-diagnostic-echo --format compact --cursor <next_cursor>` to fetch the next page for one child query."); return CommandExitCodes.UsageError; } + if (options.UnusedCursorOffset.HasValue) + { + WriteUsageError( + "--cursor for search must be a search pagination cursor returned by recipe search.", + GetUsageLineOrThrow("search"), + "Use `--cursor <next_cursor>` only with `--recipe`; `unused:<offset>` cursors are for `cdidx unused`."); + return CommandExitCodes.UsageError; + } if (options.AuditScopeExplicit && options.RecipeName == null) { WriteUsageError( @@ -2383,6 +2391,9 @@ private static string FormatSearchSnippetFocusMode(SearchSnippetFocusMode mode) private static string FormatSearchCursor(SearchResult result) => string.Create(CultureInfo.InvariantCulture, $"{result.Score:R}:{result.ChunkId}:{result.NextOffset}"); + private static string FormatUnusedCursor(int offset) + => string.Create(CultureInfo.InvariantCulture, $"unused:{offset}"); + private static bool TryParseSearchCursor(string value, out SearchCursor cursor) { cursor = default; @@ -2401,6 +2412,16 @@ private static bool TryParseSearchCursor(string value, out SearchCursor cursor) return true; } + private static bool TryParseUnusedCursor(string value, out int offset) + { + offset = 0; + const string prefix = "unused:"; + if (!value.StartsWith(prefix, StringComparison.Ordinal)) + return false; + return int.TryParse(value[prefix.Length..], NumberStyles.Integer, CultureInfo.InvariantCulture, out offset) + && offset >= 0; + } + private static string QuoteReplayShellArg(string arg) { if (arg.Length > 0 && arg.All(c => char.IsLetterOrDigit(c) || c is '_' or '-' or '.' or '/' or ':' or '=')) @@ -7799,6 +7820,23 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) return CommandExitCodes.InvalidArgument; if (TryWriteUnexpectedPositionals("unused", options)) return CommandExitCodes.UsageError; + if (options.SearchCursor.HasValue) + { + WriteUsageError( + "--cursor for unused must use the `unused:<offset>` cursor returned by a previous unused response.", + GetUsageLineOrThrow("unused"), + "Use the `next_cursor` value from `cdidx unused --json`."); + return CommandExitCodes.UsageError; + } + if (options.UnusedCursorOffset.HasValue && options.CountOnly) + { + WriteUsageError( + "--cursor cannot be used with `unused --count`.", + GetUsageLineOrThrow("unused"), + "Remove `--count` to page unused results."); + return CommandExitCodes.UsageError; + } + var unusedScope = BuildUnusedAuditScopeFilters(options); return WithDb(options, jsonOptions, reader => { @@ -7808,20 +7846,20 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) bool? graphSupported = options.Lang != null ? ReferenceExtractor.SupportsLanguage(options.Lang) : null; var graphSupportReason = ReferenceExtractor.BuildGraphSupportReason(options.Lang, graphSupported); - var baseSqlGraphSignal = reader.GetSqlGraphContractSignal(options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests); + var baseSqlGraphSignal = reader.GetSqlGraphContractSignal(options.Lang, unusedScope.PathPatterns, unusedScope.ExcludePaths, unusedScope.ExcludeTests); var zeroResultSqlGraphSignal = NarrowSqlGraphContractSignal( baseSqlGraphSignal, - reader.ScopeMayIncludeSqlSymbols(options.Kind, options.Lang, options.PathPatterns, options.ExcludePaths, options.ExcludeTests)); + reader.ScopeMayIncludeSqlSymbols(options.Kind, options.Lang, unusedScope.PathPatterns, unusedScope.ExcludePaths, unusedScope.ExcludeTests)); if (options.CountOnly) { var countSummary = reader.CountUnusedSymbols( options.Kind, options.Lang, - options.PathPatterns, - options.ExcludePaths, - options.ExcludeTests, - visibilityFilters: options.VisibilityFilters, - excludeVisibilityFilters: options.ExcludeVisibilityFilters, + unusedScope.PathPatterns, + unusedScope.ExcludePaths, + unusedScope.ExcludeTests, + visibilityFilters: unusedScope.VisibilityFilters, + excludeVisibilityFilters: unusedScope.ExcludeVisibilityFilters, bucketFilter: options.UnusedBucket, minConfidence: options.MinUnusedConfidence); var effectiveSqlGraphSignal = countSummary.Count == 0 @@ -7844,7 +7882,7 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) ["degraded"] = !reader._hasReferencesTable }; AddSqlGraphContractJsonFields(payload, effectiveSqlGraphSignal); - payload["query_context"] = BuildQueryContextJson(options, jsonOptions); + payload["query_context"] = BuildUnusedQueryContextJson(options, unusedScope, jsonOptions); if (options.Compact) { payload["compact"] = true; @@ -7861,17 +7899,26 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) return CommandExitCodes.Success; } - var results = reader.GetUnusedSymbols( - options.Limit, + var pageOffset = options.UnusedCursorOffset ?? 0; + var fetchLimit = GetUnusedFetchLimit(options.Limit, pageOffset); + var fetchedResults = reader.GetUnusedSymbols( + fetchLimit, options.Kind, options.Lang, - options.PathPatterns, - options.ExcludePaths, - options.ExcludeTests, - visibilityFilters: options.VisibilityFilters, - excludeVisibilityFilters: options.ExcludeVisibilityFilters, + unusedScope.PathPatterns, + unusedScope.ExcludePaths, + unusedScope.ExcludeTests, + visibilityFilters: unusedScope.VisibilityFilters, + excludeVisibilityFilters: unusedScope.ExcludeVisibilityFilters, bucketFilter: options.UnusedBucket, minConfidence: options.MinUnusedConfidence); + var results = fetchedResults + .Skip(pageOffset) + .Take(options.Limit) + .ToList(); + var nextCursor = fetchedResults.Count > pageOffset + options.Limit + ? FormatUnusedCursor(pageOffset + options.Limit) + : null; var sqlGraphSignal = results.Count == 0 ? zeroResultSqlGraphSignal : NarrowSqlGraphContractSignalByLanguages( @@ -7889,7 +7936,9 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) sqlGraphSignal, reader._hasReferencesTable, jsonOptions, - options)); + options, + unusedScope, + nextCursor: nextCursor)); } else { @@ -7905,7 +7954,7 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) if (options.Json) { - Console.WriteLine(BuildUnusedJsonPayload(results, graphSupported, graphSupportReason, sqlGraphSignal, reader._hasReferencesTable, jsonOptions, options, byBucket: byBucket)); + Console.WriteLine(BuildUnusedJsonPayload(results, graphSupported, graphSupportReason, sqlGraphSignal, reader._hasReferencesTable, jsonOptions, options, unusedScope, byBucket: byBucket, nextCursor: nextCursor)); } else { @@ -7930,6 +7979,8 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) .Where(bucketCounts.ContainsKey) .Select(bucket => $"{GetUnusedBucketHeading(bucket)}: {bucketCounts[bucket]}"); CommandErrorWriter.WriteStderr($"({results.Count} returned potentially unused symbols; returned buckets: {string.Join(", ", summaryBuckets)})"); + if (nextCursor != null) + CommandErrorWriter.WriteStderr($"next_cursor={nextCursor}"); WriteSqlGraphContractWarningIfNeeded(json: false, sqlGraphSignal, reader, options); } return CommandExitCodes.Success; @@ -7944,6 +7995,64 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions) "reflection_or_config_suspect", ]; + private static readonly string[] UnusedSourceAuditExcludePaths = + [ + "*.md", + "docs/*", + "doc/*", + "CHANGELOG.md", + "changelog.d/*", + "README.md", + "USER_GUIDE.md", + "DEVELOPER_GUIDE.md", + "TESTING_GUIDE.md", + "AGENT_GUIDE.md", + ".codex/*", + ".github/*", + ]; + + private static readonly string[] UnusedSourceAuditVisibilityFilters = + [ + "private", + "internal", + ]; + + private sealed record UnusedAuditScopeFilters( + IReadOnlyList<string> PathPatterns, + IReadOnlyList<string> ExcludePaths, + bool ExcludeTests, + IReadOnlyList<string> VisibilityFilters, + IReadOnlyList<string> ExcludeVisibilityFilters, + bool AppliedSourceDefaults); + + private static UnusedAuditScopeFilters BuildUnusedAuditScopeFilters(QueryCommandOptions options) + { + if (!options.AuditScopeExplicit + || !string.Equals(options.AuditScope, SearchAuditRecipes.DefaultAuditScope, StringComparison.OrdinalIgnoreCase)) + { + return new( + options.PathPatterns, + options.ExcludePaths, + options.ExcludeTests, + options.VisibilityFilters, + options.ExcludeVisibilityFilters, + AppliedSourceDefaults: false); + } + + var excludePaths = new List<string>(options.ExcludePaths); + AddDistinct(excludePaths, UnusedSourceAuditExcludePaths); + var visibilityFilters = options.VisibilityFilters.Count > 0 + ? options.VisibilityFilters + : [.. UnusedSourceAuditVisibilityFilters]; + return new( + options.PathPatterns, + excludePaths, + ExcludeTests: true, + visibilityFilters, + options.ExcludeVisibilityFilters, + AppliedSourceDefaults: true); + } + internal static Dictionary<string, int> BuildUnusedBucketCounts(IEnumerable<UnusedSymbolResult> results) { var grouped = results @@ -7974,6 +8083,42 @@ internal static JsonObject BuildUnusedSummaryJson(IEnumerable<UnusedSymbolResult }; } + private static int GetUnusedFetchLimit(int pageLimit, int pageOffset) + { + var requested = (long)Math.Max(pageLimit, 1) + Math.Max(pageOffset, 0) + 1; + return requested > int.MaxValue ? int.MaxValue : (int)requested; + } + + internal static JsonObject BuildUnusedRepresentativeSymbolsJson(IEnumerable<UnusedSymbolResult> results) + { + var grouped = results + .GroupBy(result => result.UnusedBucket, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.Take(3).ToList(), StringComparer.Ordinal); + var representatives = new JsonObject(); + foreach (var bucket in OrderedUnusedBuckets) + { + if (!grouped.TryGetValue(bucket, out var bucketResults) || bucketResults.Count == 0) + continue; + + var samples = new JsonArray(); + foreach (var result in bucketResults) + { + samples.Add(new JsonObject + { + ["name"] = result.Name, + ["kind"] = result.Kind, + ["path"] = result.Path, + ["line"] = result.Line, + ["confidence"] = result.UnusedConfidence, + }); + } + + representatives[bucket] = samples; + } + + return representatives; + } + internal static JsonObject BuildUnusedBucketTaxonomyJson() { var taxonomy = new JsonObject(); @@ -8011,7 +8156,7 @@ internal static JsonObject BuildUnusedBucketTaxonomyJson() _ => "Unknown unused-symbol bucket.", }; - private static string BuildUnusedJsonPayload(IEnumerable<UnusedSymbolResult> results, bool? graphSupported, string? graphSupportReason, SqlGraphContractSignal sqlGraphSignal, bool hasReferencesTable, JsonSerializerOptions jsonOptions, QueryCommandOptions? queryOptions = null, bool byBucket = false) + private static string BuildUnusedJsonPayload(IEnumerable<UnusedSymbolResult> results, bool? graphSupported, string? graphSupportReason, SqlGraphContractSignal sqlGraphSignal, bool hasReferencesTable, JsonSerializerOptions jsonOptions, QueryCommandOptions? queryOptions = null, UnusedAuditScopeFilters? unusedScope = null, bool byBucket = false, string? nextCursor = null) { var resultList = results as List<UnusedSymbolResult> ?? results.ToList(); var payload = new JsonObject @@ -8023,9 +8168,12 @@ private static string BuildUnusedJsonPayload(IEnumerable<UnusedSymbolResult> res ["summary"] = BuildUnusedSummaryJson(resultList, jsonOptions), ["bucket_taxonomy"] = BuildUnusedBucketTaxonomyJson(), }; + if (nextCursor != null) + payload["next_cursor"] = nextCursor; if (queryOptions?.Compact == true) { payload["compact"] = true; + payload["representative_symbols"] = BuildUnusedRepresentativeSymbolsJson(resultList); payload["omitted_sections"] = new JsonArray(JsonValue.Create("symbols")); } else @@ -8044,10 +8192,28 @@ private static string BuildUnusedJsonPayload(IEnumerable<UnusedSymbolResult> res AddSqlGraphContractJsonFields(payload, sqlGraphSignal); if (queryOptions != null) - payload["query_context"] = BuildQueryContextJson(queryOptions, jsonOptions); + payload["query_context"] = unusedScope != null + ? BuildUnusedQueryContextJson(queryOptions, unusedScope, jsonOptions) + : BuildQueryContextJson(queryOptions, jsonOptions); return payload.ToJsonString(jsonOptions); } + private static JsonObject BuildUnusedQueryContextJson(QueryCommandOptions options, UnusedAuditScopeFilters unusedScope, JsonSerializerOptions jsonOptions) + { + var query = BuildQueryContextJson(options, jsonOptions); + if (!unusedScope.AppliedSourceDefaults) + return query; + + var context = CliJsonSerializerContextFactory.Create(jsonOptions); + if (!options.ExcludeTests && unusedScope.ExcludeTests) + query["effective_exclude_tests"] = true; + if (!options.ExcludePaths.SequenceEqual(unusedScope.ExcludePaths, StringComparer.Ordinal)) + query["effective_exclude_path"] = JsonSerializer.SerializeToNode(unusedScope.ExcludePaths.ToList(), context.ListString); + if (options.VisibilityFilters.Count == 0 && unusedScope.VisibilityFilters.Count > 0) + query["effective_visibility"] = JsonSerializer.SerializeToNode(unusedScope.VisibilityFilters.ToList(), context.ListString); + return query; + } + private static JsonObject BuildUnusedResultsByBucketJson(IEnumerable<UnusedSymbolResult> results, JsonSerializerOptions jsonOptions) { var grouped = results @@ -8513,6 +8679,7 @@ public static QueryCommandOptions ParseArgs( string? issueTitle = null; var issueLabels = new List<string>(); SearchCursor? searchCursor = null; + int? unusedCursorOffset = null; var namedSearchQueries = new List<SearchNamedQuery>(); bool languagesIndexedOnly = false; var languageCapabilities = new List<string>(); @@ -8956,8 +9123,10 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) WarnIfDuplicateSingleValueOption("--cursor", cursorValue!); if (TryParseSearchCursor(cursorValue!, out var parsedCursor)) searchCursor = parsedCursor; + else if (TryParseUnusedCursor(cursorValue!, out var parsedUnusedCursorOffset)) + unusedCursorOffset = parsedUnusedCursorOffset; else - AddParseError("Error: --cursor must be a search pagination cursor returned as `next_cursor` by a previous recipe search response."); + AddParseError("Error: --cursor must be a search pagination cursor or an unused pagination cursor returned as `next_cursor`."); } else { @@ -9842,6 +10011,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue) IssueTitle = issueTitle, IssueLabels = issueLabels, SearchCursor = searchCursor, + UnusedCursorOffset = unusedCursorOffset, NamedSearchQueries = namedSearchQueries, LanguagesIndexedOnly = languagesIndexedOnly, LanguageCapabilities = languageCapabilities, @@ -11673,6 +11843,17 @@ private static JsonObject BuildQueryContextJson(QueryCommandOptions options, Jso query["bucket"] = options.UnusedBucket; if (options.MinUnusedConfidence != null) query["min_confidence"] = options.MinUnusedConfidence; + if (options.AuditScopeExplicit) + query["audit_scope"] = options.AuditScope; + if (options.VisibilityFilters.Count > 0) + query["visibility"] = JsonSerializer.SerializeToNode(options.VisibilityFilters, CliJsonSerializerContextFactory.Create(jsonOptions).ListString); + if (options.ExcludeVisibilityFilters.Count > 0) + query["exclude_visibility"] = JsonSerializer.SerializeToNode(options.ExcludeVisibilityFilters, CliJsonSerializerContextFactory.Create(jsonOptions).ListString); + if (options.UnusedCursorOffset.HasValue) + { + query["cursor"] = FormatUnusedCursor(options.UnusedCursorOffset.Value); + query["offset"] = options.UnusedCursorOffset.Value; + } if (options.RankMode != ReferenceRankMode.Weighted) query["rank_by"] = FormatReferenceRankMode(options.RankMode); if (options.SymbolSortMode != SymbolSortMode.Name) @@ -12342,16 +12523,6 @@ private static bool IsFoldOnlyReadinessDegraded(StatusResult status) && status.CSharpSymbolNameReady && status.CSharpMetadataTargetReady; - private static bool IsCSharpMetadataTargetOnlyReadinessDegraded(StatusResult status) - => !status.CSharpMetadataTargetReady - && status.GraphTableAvailable - && status.IssuesTableAvailable - && status.SqlGraphContractReady - && status.HotspotFamilyReady - && status.CSharpSymbolNameReady - && status.FoldReady - && !status.IndexNewerThanReader; - private static string BuildFoldNotReadyExplanation(string? foldReadyReason) => DegradationReasonCodes.BuildFoldNotReadyExplanation(foldReadyReason); @@ -13550,6 +13721,7 @@ public sealed class QueryCommandOptions public string? IssueTitle { get; init; } public List<string> IssueLabels { get; init; } = []; public SearchCursor? SearchCursor { get; init; } + public int? UnusedCursorOffset { get; init; } public List<SearchNamedQuery> NamedSearchQueries { get; init; } = []; public bool LanguagesIndexedOnly { get; init; } public List<string> LanguageCapabilities { get; init; } = []; diff --git a/src/CodeIndex/Database/DbReader.CSharpResolution.cs b/src/CodeIndex/Database/DbReader.CSharpResolution.cs index a601bd2b88..6bd7f4ec26 100644 --- a/src/CodeIndex/Database/DbReader.CSharpResolution.cs +++ b/src/CodeIndex/Database/DbReader.CSharpResolution.cs @@ -899,14 +899,6 @@ private static int FindCSharpBaseListColonIndex(string signature) return -1; } - private static string TakeFirstCSharpBaseListEntry(string baseList) - { - foreach (var entry in EnumerateCSharpBaseListEntries(baseList)) - return entry; - - return baseList; - } - private static IEnumerable<string> EnumerateCSharpBaseListEntries(string baseList) { var angleDepth = 0; diff --git a/src/CodeIndex/Database/DbReader.References.cs b/src/CodeIndex/Database/DbReader.References.cs index 31b0abf09d..808a4cabe7 100644 --- a/src/CodeIndex/Database/DbReader.References.cs +++ b/src/CodeIndex/Database/DbReader.References.cs @@ -452,19 +452,6 @@ private bool ShouldSuppressCSharpQualifiedConstantPatternReference(string path, return string.IsNullOrWhiteSpace(normalized) ? null : normalized; } - private static string? NormalizeRustMacroQuery(string? query) - { - if (query == null) - return null; - - var trimmed = query.TrimEnd(); - if (!trimmed.EndsWith("!", StringComparison.Ordinal)) - return trimmed; - - var normalized = trimmed[..^1].TrimEnd(); - return string.IsNullOrWhiteSpace(normalized) ? null : normalized; - } - private static bool IsBareVerbatimQueryToken(string? value) { var trimmed = value?.Trim(); diff --git a/src/CodeIndex/Database/DbSymbolReader.cs b/src/CodeIndex/Database/DbSymbolReader.cs index d3852b09f9..f20b2fc781 100644 --- a/src/CodeIndex/Database/DbSymbolReader.cs +++ b/src/CodeIndex/Database/DbSymbolReader.cs @@ -1765,53 +1765,6 @@ FROM symbols s return value == null || value == DBNull.Value ? null : (string?)value; } - private bool HasExactDefinitionMatch( - string query, - string? lang, - IReadOnlyList<string>? pathPatterns, - IReadOnlyList<string>? excludePathPatterns, - bool excludeTests, - string extraConditionSql, - SqliteCommand? command = null) - { - var normalizedQuery = NormalizeCSharpVerbatimQuery(query, lang) ?? query; - using var ownedCommand = command == null ? _conn.CreateCommand() : null; - var cmd = command ?? ownedCommand!; - var allowLeafFallback = !SqlNameResolver.HasQualifier(normalizedQuery); - var nameCondition = _foldReady - ? allowLeafFallback - ? "(s.name_folded = @queryFolded OR (f.lang = 'sql' AND ((sql_segment_count(s.name) = @querySegmentCount AND sql_normalize_name_folded(s.name) = @queryNormalizedFolded) OR sql_leaf_name_folded(s.name) = @queryLeafFolded)))" - : "(s.name_folded = @queryFolded OR (f.lang = 'sql' AND sql_segment_count(s.name) = @querySegmentCount AND sql_normalize_name_folded(s.name) = @queryNormalizedFolded))" - : allowLeafFallback - ? "(s.name = @queryRaw COLLATE NOCASE OR (f.lang = 'sql' AND ((sql_segment_count(s.name) = @querySegmentCount AND sql_normalize_name(s.name) = @queryNormalized COLLATE NOCASE) OR sql_leaf_name(s.name) = @queryLeaf COLLATE NOCASE)))" - : "(s.name = @queryRaw COLLATE NOCASE OR (f.lang = 'sql' AND sql_segment_count(s.name) = @querySegmentCount AND sql_normalize_name(s.name) = @queryNormalized COLLATE NOCASE))"; - - var sql = @" - SELECT 1 - FROM symbols s - JOIN files f ON s.file_id = f.id - WHERE " + nameCondition; - if (lang != null) - sql += " AND f.lang = @lang"; - sql += " AND " + extraConditionSql; - AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests); - sql += " LIMIT 1"; - - cmd.CommandText = sql; - cmd.Parameters.AddWithValue("@queryRaw", normalizedQuery); - cmd.Parameters.AddWithValue("@queryFolded", NameFold.Fold(normalizedQuery) ?? normalizedQuery); - cmd.Parameters.AddWithValue("@queryNormalized", SqlNameResolver.NormalizeQualifiedName(normalizedQuery)); - cmd.Parameters.AddWithValue("@queryNormalizedFolded", NameFold.Fold(SqlNameResolver.NormalizeQualifiedName(normalizedQuery)) ?? SqlNameResolver.NormalizeQualifiedName(normalizedQuery)); - cmd.Parameters.AddWithValue("@queryLeaf", SqlNameResolver.GetLeafName(normalizedQuery)); - cmd.Parameters.AddWithValue("@queryLeafFolded", NameFold.Fold(SqlNameResolver.GetLeafName(normalizedQuery)) ?? SqlNameResolver.GetLeafName(normalizedQuery)); - cmd.Parameters.AddWithValue("@querySegmentCount", SqlNameResolver.GetSegmentCount(normalizedQuery)); - if (lang != null) - cmd.Parameters.AddWithValue("@lang", lang); - AddPathFilterParameters(cmd, pathPatterns, excludePathPatterns); - - return cmd.ExecuteScalar() != null; - } - public bool HasFilteredCSharpEnumSymbols(string? kind, string? lang, IReadOnlyList<string>? pathPatterns, IReadOnlyList<string>? excludePathPatterns, bool excludeTests) { if (lang != null && !string.Equals(lang, "csharp", StringComparison.Ordinal)) @@ -3649,6 +3602,7 @@ private UnusedSymbolResult CreateUnusedSymbolResult(UnusedCandidateSymbol candid UnusedBucket = classification.Bucket, UnusedConfidence = classification.Confidence, UnusedReason = classification.Reason, + UnusedReasonTags = BuildUnusedReasonTags(candidate.IsPublicOrExported, isReflectionOrConfigSuspect, candidate.Visibility), }; } @@ -3802,6 +3756,7 @@ FROM unused_candidates UnusedBucket = classification.Bucket, UnusedConfidence = classification.Confidence, UnusedReason = classification.Reason, + UnusedReasonTags = BuildUnusedReasonTags(isPublicOrExported, isReflectionOrConfigSuspect, visibility), }); } @@ -5037,6 +4992,20 @@ private static (string Bucket, string Confidence, string Reason) ClassifyUnusedS "non-public symbol with no indexed references"); } + private static List<string> BuildUnusedReasonTags(bool isPublicOrExported, bool isReflectionOrConfigSuspect, string? visibility) + { + var tags = new List<string> { "no_indexed_references" }; + if (isReflectionOrConfigSuspect) + tags.Add("reflection_or_config_suspect"); + if (isPublicOrExported) + tags.Add("public_or_exported"); + else if (IsPrivateLikeVisibility(visibility)) + tags.Add("private_or_file_local"); + else + tags.Add("non_public"); + return tags; + } + private static bool IsPrivateLikeVisibility(string? visibility) { return string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase) diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs index de16872e94..98b155acaf 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.Support.cs @@ -10,8 +10,6 @@ public static partial class ReferenceExtractor private readonly record struct CSharpLineColumn(int Line, int Column); private readonly record struct CSharpRecursivePatternValueNameRecord(string Name, int Offset, bool IsCasePattern, int ArrowIndex = -1); private sealed record CSharpNamespaceScope(string QualifiedName, int ScopeStartLine, int ScopeEndLine); - private sealed record CSharpUsingNamespaceScope(string TargetQualifiedName, int Line, int ScopeStartLine, int ScopeEndLine); - private sealed record CSharpContainingTypeScope(string QualifiedName, int ScopeStartLine, int ScopeEndLine); internal sealed record CSharpUsingAliasRecord(string AliasName, string TargetQualifiedName, int Line, int ScopeStartLine, int ScopeEndLine, bool TargetsType); internal sealed record CSharpUsingNamespaceRecord(string TargetQualifiedName, int Line, int ScopeStartLine, int ScopeEndLine); internal sealed record CSharpUsingStaticRecord(string TargetQualifiedName, int Line, int ScopeStartLine, int ScopeEndLine); @@ -230,52 +228,6 @@ private static List<CSharpUsingStaticRecord> BuildCSharpUsingStatics(string lang return imports; } - private static List<CSharpUsingNamespaceScope> BuildCSharpUsingNamespaceScopes(string language, IReadOnlyList<SymbolRecord> symbols) - { - var scopes = new List<CSharpUsingNamespaceScope>(); - if (language != "csharp") - return scopes; - - var namespaceScopes = symbols - .Where(symbol => symbol.Kind == "namespace") - .Select(symbol => ( - StartLine: symbol.BodyStartLine ?? symbol.StartLine, - EndLine: symbol.BodyEndLine ?? symbol.EndLine)) - .Where(scope => scope.StartLine > 0 && scope.EndLine >= scope.StartLine) - .ToList(); - - foreach (var symbol in symbols) - { - if (symbol.Kind != "import" || string.IsNullOrWhiteSpace(symbol.Signature)) - continue; - - if (!TryParseCSharpUsingNamespaceImport(symbol.Signature!, out var target, out _)) - continue; - - var scopeStartLine = 1; - var scopeEndLine = int.MaxValue; - var scopeWidth = int.MaxValue; - foreach (var (startLine, endLine) in namespaceScopes) - { - if (symbol.Line < startLine || symbol.Line > endLine) - continue; - - var width = endLine - startLine; - if (width > scopeWidth) - continue; - - scopeStartLine = startLine; - scopeEndLine = endLine; - scopeWidth = width; - } - - scopes.Add(new CSharpUsingNamespaceScope(target!, symbol.Line, scopeStartLine, scopeEndLine)); - } - - scopes.Sort(static (left, right) => left.Line.CompareTo(right.Line)); - return scopes; - } - private static List<CSharpNamespaceScope> BuildCSharpNamespaceScopes(string language, IReadOnlyList<SymbolRecord> symbols, int totalLineCount) { var scopes = new List<CSharpNamespaceScope>(); @@ -305,56 +257,6 @@ private static List<CSharpNamespaceScope> BuildCSharpNamespaceScopes(string lang return scopes; } - private static bool TryParseCSharpUsingNamespaceImport(string signature, out string? target, out bool isGlobal) - { - target = null; - isGlobal = false; - if (string.IsNullOrWhiteSpace(signature) || signature.IndexOf('=') >= 0) - return false; - - var match = CSharpUsingNamespaceRegex.Match(signature); - if (!match.Success) - return false; - - target = TryNormalizeCSharpQualifiedName(match.Groups["target"].Value); - if (string.IsNullOrWhiteSpace(target)) - return false; - - isGlobal = signature.TrimStart().StartsWith("global using ", StringComparison.Ordinal); - return true; - } - - private static List<CSharpContainingTypeScope> BuildCSharpContainingTypeScopes(string language, IReadOnlyList<SymbolRecord> symbols) - { - var scopes = new List<CSharpContainingTypeScope>(); - if (language != "csharp") - return scopes; - - foreach (var symbol in symbols) - { - if (symbol.Kind is not ("class" or "struct" or "interface") - || string.IsNullOrWhiteSpace(symbol.Name)) - { - continue; - } - - var startLine = symbol.BodyStartLine ?? symbol.StartLine; - var endLine = symbol.BodyEndLine ?? symbol.EndLine; - if (startLine <= 0 || endLine < startLine) - continue; - - var qualifiedName = CombineQualifiedName(symbol.ContainerQualifiedName, NormalizeCSharpIdentifier(symbol.Name)); - if (string.IsNullOrWhiteSpace(qualifiedName)) - qualifiedName = NormalizeCSharpIdentifier(symbol.Name); - if (string.IsNullOrWhiteSpace(qualifiedName)) - continue; - - scopes.Add(new CSharpContainingTypeScope(qualifiedName!, startLine, endLine)); - } - - return scopes; - } - private static Dictionary<string, HashSet<string>> BuildCSharpTopLevelTypeNamespacesByName(string language, IReadOnlyList<SymbolRecord> symbols) { var lookup = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal); @@ -1770,28 +1672,6 @@ private static bool TryGetCSharpXmlDocCommentSpan( return true; } - private static bool HasActiveCSharpUsingStaticTarget( - string targetQualifiedName, - int lineNumber, - IReadOnlyList<CSharpUsingStaticRecord> usingStatics) - { - if (string.IsNullOrWhiteSpace(targetQualifiedName)) - return false; - - for (var i = usingStatics.Count - 1; i >= 0; i--) - { - var import = usingStatics[i]; - if (import.Line > lineNumber) - continue; - if (lineNumber < import.ScopeStartLine || lineNumber > import.ScopeEndLine) - continue; - if (string.Equals(import.TargetQualifiedName, targetQualifiedName, StringComparison.Ordinal)) - return true; - } - - return false; - } - private static bool HasCSharpValueReceiverConflict( string qualifier, string resolvedQualifier, diff --git a/src/CodeIndex/Indexer/References/Languages/LuaReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/LuaReferenceExtractor.cs index 19a4fc098e..8fbce6a850 100644 --- a/src/CodeIndex/Indexer/References/Languages/LuaReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/LuaReferenceExtractor.cs @@ -6,9 +6,6 @@ namespace CodeIndex.Indexer; internal static class LuaReferenceExtractor { - private static readonly Regex LuaRequireRegex = new( - @"\brequire\s*\(?\s*[""'](?<name>[^""']+)[""']", - RegexOptions.Compiled | RegexOptions.CultureInvariant); private static readonly Regex LuaCommandCallRegex = new( @"^\s*(?<name>[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)\s+(?=[""'{A-Za-z_])", RegexOptions.Compiled | RegexOptions.CultureInvariant); diff --git a/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs index 23f88eee09..2881bd0ee9 100644 --- a/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/XamlReferenceExtractor.cs @@ -15,10 +15,6 @@ internal static class XamlReferenceExtractor @"\bx:TypeArguments\s*=\s*[""'](?<value>[^""']+)[""']", RegexOptions.Compiled | RegexOptions.CultureInvariant); - private static readonly Regex XamlResourceReferenceRegex = new( - @"\{(?:StaticResource|StaticResourceExtension|DynamicResource|DynamicResourceExtension)\b(?<content>(?:[^{}]|{[^{}]*})*)\}", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - private static readonly Regex XamlReferenceRegex = new( @"\{x:Reference(?:Extension)?\b(?<content>(?:[^{}]|{[^{}]*})*)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant); diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs index 5c637d1bba..b45698a18b 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs @@ -2422,11 +2422,6 @@ private static string TakeFirstBaseEntry(string baseList) return segment.Length > 0 ? segment : null; } - private static string ReplaceRegexMatchesWithSpaces(Regex regex, string input) - { - return regex.Replace(input, static match => match.Length == 0 ? string.Empty : new string(' ', match.Length)); - } - private static string PrepareLine(string lang, string line) { var result = line; diff --git a/src/CodeIndex/Models/QueryResults.cs b/src/CodeIndex/Models/QueryResults.cs index 339cd13133..37931e4bea 100644 --- a/src/CodeIndex/Models/QueryResults.cs +++ b/src/CodeIndex/Models/QueryResults.cs @@ -223,6 +223,7 @@ public class UnusedSymbolResult : SymbolResult public string UnusedBucket { get; set; } = string.Empty; public string UnusedConfidence { get; set; } = string.Empty; public string UnusedReason { get; set; } = string.Empty; + public List<string> UnusedReasonTags { get; set; } = []; } public class GroupedHotspotResult diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs index 6fcf4fc24b..e3b7a833d3 100644 --- a/tests/CodeIndex.Tests/ConsoleUiTests.cs +++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs @@ -171,7 +171,8 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags() Assert.Contains("cdidx validate [--db <path>] [--json[=array]] [--format <text|json|count|compact|csv|tsv|lsp|qf|sarif>] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--severity <info|warning|error>] [--path <glob>]", output); Assert.Contains("Note: if a query itself starts with '-', pass it with --query <query> or -- <query>", output); Assert.DoesNotContain("cdidx validate [--db <path>] [--json] [--limit <n>] [--lang <lang>]", output); - Assert.Contains("cdidx unused [--db <path>] [--json] [--compact] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--bucket <bucket>] [--min-confidence <medium|low>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--by-bucket]", output); + Assert.Contains("cdidx unused [--db <path>] [--json] [--compact]", output); + Assert.Contains("[--cursor <unused:offset>] [--audit-scope <source|all>]", output); Assert.Contains("cdidx hotspots [--db <path>] [--json] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--group-by <symbol|file|statement>] [--group-by-name]", output); Assert.Contains("--json Output as JSON (search/files stream ndjson by", output); Assert.Contains("search/files/validate accept --json=array for one array)", output); @@ -288,7 +289,8 @@ public void PrintUsage_QueryLinesMatchImplementedOptions() Assert.Contains("cdidx symbols [query|--query <query>|-- <query>] [--name <name>] [--db <path>] [--json] [--format <text|json|count>] [--verbose] [--limit <n>|--top <n>] [--sort <hotspot|references|size|complexity|path>] [--lang <lang>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--exact|--exact-name] [--count] [--since <datetime>]", output); Assert.Contains("cdidx files [query|--query <query>|-- <query>] [--db <path>] [--json[=ndjson|array]] [--verbose] [--limit <n>|--top <n>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--since <datetime>] [--bytes]", output); Assert.Contains("cdidx hotspots [--db <path>] [--json] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count]", output); - Assert.Contains("cdidx unused [--db <path>] [--json] [--compact] [--verbose] [--limit <n>|--top <n>] [--kind <kind>] [--bucket <bucket>] [--min-confidence <medium|low>] [--visibility <v[,v]>] [--exclude-visibility <v[,v]>] [--lang <lang>] [--path <glob>] [--exclude-path <glob>] [--exclude-tests] [--count] [--by-bucket]", output); + Assert.Contains("cdidx unused", output); + Assert.Contains("[--cursor <unused:offset>] [--audit-scope <source|all>]", output); Assert.Contains("cdidx license", output); Assert.Contains("cdidx completions <shell>", output); Assert.Contains("cdidx --completions <shell>", output); diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs index 9909e52852..02b20c3813 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerSymbolTests.cs @@ -978,10 +978,14 @@ public void RunUnused_WithJsonIncludesConfidenceBuckets() Assert.Equal("Hidden", symbols[0].GetProperty("name").GetString()); Assert.Equal("likely_unused_private", symbols[0].GetProperty("unused_bucket").GetString()); Assert.Equal("medium", symbols[0].GetProperty("unused_confidence").GetString()); + Assert.Contains(symbols[0].GetProperty("unused_reason_tags").EnumerateArray(), tag => tag.GetString() == "no_indexed_references"); + Assert.Contains(symbols[0].GetProperty("unused_reason_tags").EnumerateArray(), tag => tag.GetString() == "private_or_file_local"); Assert.Equal("PathResolver", symbols[2].GetProperty("name").GetString()); Assert.Equal("public_or_exported_no_refs", symbols[2].GetProperty("unused_bucket").GetString()); + Assert.Contains(symbols[2].GetProperty("unused_reason_tags").EnumerateArray(), tag => tag.GetString() == "public_or_exported"); Assert.Equal("ConnectionString", symbols[3].GetProperty("name").GetString()); Assert.Equal("reflection_or_config_suspect", symbols[3].GetProperty("unused_bucket").GetString()); + Assert.Contains(symbols[3].GetProperty("unused_reason_tags").EnumerateArray(), tag => tag.GetString() == "reflection_or_config_suspect"); Assert.Equal("ApplyConfiguration", symbols[7].GetProperty("name").GetString()); Assert.Equal("public_or_exported_no_refs", symbols[7].GetProperty("unused_bucket").GetString()); Assert.Equal("UseIOptions", symbols[8].GetProperty("name").GetString()); @@ -993,6 +997,85 @@ public void RunUnused_WithJsonIncludesConfidenceBuckets() } } + [Fact] + public void RunUnused_JsonPaginatesWithUnusedCursor_Issue3691() + { + var (projectRoot, dbPath) = CreateUnusedFixtureDb(); + try + { + var (firstExitCode, firstStdout, firstStderr) = CaptureConsole(() => QueryCommandRunner.RunUnused( + ["--db", dbPath, "--json", "--lang", "csharp", "--limit", "2"], + _jsonOptions)); + using var firstDocument = ParseJsonOutput(firstStdout); + var firstJson = firstDocument.RootElement; + var firstSymbols = firstJson.GetProperty("symbols"); + + Assert.Equal(CommandExitCodes.Success, firstExitCode); + Assert.Equal(string.Empty, firstStderr); + Assert.Equal(2, firstJson.GetProperty("count").GetInt32()); + Assert.Equal("unused:2", firstJson.GetProperty("next_cursor").GetString()); + Assert.Equal("Hidden", firstSymbols[0].GetProperty("name").GetString()); + Assert.Equal("InternalOnly", firstSymbols[1].GetProperty("name").GetString()); + + var (secondExitCode, secondStdout, secondStderr) = CaptureConsole(() => QueryCommandRunner.RunUnused( + ["--db", dbPath, "--json", "--lang", "csharp", "--limit", "2", "--cursor", "unused:2"], + _jsonOptions)); + using var secondDocument = ParseJsonOutput(secondStdout); + var secondJson = secondDocument.RootElement; + var secondSymbols = secondJson.GetProperty("symbols"); + var secondQuery = secondJson.GetProperty("query_context"); + + Assert.Equal(CommandExitCodes.Success, secondExitCode); + Assert.Equal(string.Empty, secondStderr); + Assert.Equal(2, secondJson.GetProperty("count").GetInt32()); + Assert.Equal("unused:4", secondJson.GetProperty("next_cursor").GetString()); + Assert.Equal("unused:2", secondQuery.GetProperty("cursor").GetString()); + Assert.Equal(2, secondQuery.GetProperty("offset").GetInt32()); + Assert.Equal("PathResolver", secondSymbols[0].GetProperty("name").GetString()); + Assert.Equal("ConnectionString", secondSymbols[1].GetProperty("name").GetString()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void RunUnused_AuditScopeSourcePrioritizesPrivateSourceCandidates_Issue3669() + { + var (projectRoot, dbPath) = CreateUnusedFixtureDb(); + try + { + var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunUnused( + ["--db", dbPath, "--json", "--audit-scope", "source", "--limit", "10"], + _jsonOptions)); + + using var document = ParseJsonOutput(stdout); + var json = document.RootElement; + var symbols = json.GetProperty("symbols"); + var query = json.GetProperty("query_context"); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal(string.Empty, stderr); + Assert.Equal(2, json.GetProperty("count").GetInt32()); + Assert.Collection( + symbols.EnumerateArray(), + symbol => Assert.Equal("Hidden", symbol.GetProperty("name").GetString()), + symbol => Assert.Equal("InternalOnly", symbol.GetProperty("name").GetString())); + Assert.Equal("source", query.GetProperty("audit_scope").GetString()); + Assert.True(query.GetProperty("effective_exclude_tests").GetBoolean()); + Assert.Contains(query.GetProperty("effective_exclude_path").EnumerateArray(), path => path.GetString() == "*.md"); + Assert.Collection( + query.GetProperty("effective_visibility").EnumerateArray(), + visibility => Assert.Equal("private", visibility.GetString()), + visibility => Assert.Equal("internal", visibility.GetString())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void RunUnused_WithJsonByBucketGroupsReturnedSymbolsByTaxonomyBucket() { @@ -1039,6 +1122,18 @@ public void RunUnused_CompactJsonOmitsSymbolBodiesAndShowsFilters_Issue3395() Assert.True(json.GetProperty("compact").GetBoolean()); Assert.False(json.TryGetProperty("symbols", out _)); Assert.Contains(json.GetProperty("omitted_sections").EnumerateArray(), section => section.GetString() == "symbols"); + Assert.Equal( + "Hidden", + json.GetProperty("representative_symbols") + .GetProperty("likely_unused_private")[0] + .GetProperty("name") + .GetString()); + Assert.Equal( + "function", + json.GetProperty("representative_symbols") + .GetProperty("likely_unused_private")[0] + .GetProperty("kind") + .GetString()); Assert.Equal(1, json.GetProperty("returned_bucket_counts").GetProperty("likely_unused_private").GetInt32()); Assert.Equal(1, json.GetProperty("summary").GetProperty("by_confidence").GetProperty("medium").GetInt32()); Assert.Equal("medium", json.GetProperty("bucket_taxonomy").GetProperty("likely_unused_private").GetProperty("confidence").GetString());