From 46dac9353765fcec5ee0223e77701a607c074327 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 21 Jun 2026 01:10:23 +0900 Subject: [PATCH] Add solution and manifest structural extraction (#3662) --- USER_GUIDE.md | 10 +- changelog.d/unreleased/3662.added.md | 26 +++ src/CodeIndex/Database/DbReader.cs | 8 +- .../References/ReferenceExtractor.Core.cs | 3 + .../References/ReferenceExtractor.State.cs | 1 + .../ReferenceExtractor.StructuralMetadata.cs | 32 +++ src/CodeIndex/Indexer/Scanning/FileIndexer.cs | 2 + src/CodeIndex/Indexer/SolutionFileParser.cs | 78 ++++++++ .../SymbolExtractor.StructuralMetadata.cs | 187 ++++++++++++++++++ .../Indexer/Symbols/SymbolExtractor.cs | 14 +- src/CodeIndex/Models/SymbolKindCatalog.cs | 3 + tests/CodeIndex.Tests/DbReaderTests.cs | 19 ++ tests/CodeIndex.Tests/FileIndexerTests.cs | 42 ++++ .../ReferenceExtractorTests.cs | 29 +++ tests/CodeIndex.Tests/SymbolExtractorTests.cs | 69 +++++++ tests/CodeIndex.Tests/golden/status.json | 1 + 16 files changed, 518 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/3662.added.md create mode 100644 src/CodeIndex/Indexer/References/ReferenceExtractor.StructuralMetadata.cs create mode 100644 src/CodeIndex/Indexer/SolutionFileParser.cs create mode 100644 src/CodeIndex/Indexer/Symbols/SymbolExtractor.StructuralMetadata.cs diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 361bd8540a..fdfd6a5da9 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -31,7 +31,7 @@ cdidx mcp # Start MCP server for AI tools cdidx lsp --db .cdidx/codeindex.db # Start read-only LSP server for editors ``` -80 languages supported. 24 registered MCP tools. Incremental updates. Zero config. +82 languages supported. 24 registered MCP tools. Incremental updates. Zero config. | Topic | Link | |---|---| @@ -1856,6 +1856,8 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a | Zig | `.zig` | yes | | XAML | `.xaml`, `.axaml` | yes | | MSBuild | `.csproj`, `.fsproj`, `.vbproj`, `.props`, `.targets` | yes | +| Solution | `.sln` | yes | +| Application manifest | `.manifest` | yes | | Shell | `.sh`, `.bash`, `.zsh`, `.fish` | partial | | PowerShell | `.ps1`, `.psm1`, `.psd1` | yes | | Batch | `.bat`, `.cmd` | yes | @@ -1894,6 +1896,7 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a - JavaScript/TypeScript imports: static imports, dynamic imports, CommonJS `require` / `require.resolve`, `import.meta.resolve`, `new URL(..., import.meta.url)`, `importScripts`, service-worker registrations, worklet loads, and worker constructors add `import` symbols when the specifier is static. `tsconfig.json` / `jsconfig.json` `compilerOptions.baseUrl` and `paths` aliases are resolved to indexed project paths when the target file exists. - Node module layouts: `.cjs` / `.mjs` are JavaScript; `.cts` / `.mts`, including `.d.cts` / `.d.mts`, are TypeScript. - Dependency manifests and lockfiles: use `--lang dependency_manifest` or `--lang dependency_lock` for dependency/security audits. These buckets are searchable text and do not claim symbol or graph extraction. +- Solution and application manifests: `.sln` files expose project entries as symbols and project path references; `.manifest` files expose assembly identity, requested execution level, supported OS, and long-path settings as symbols. - Extensionless scripts: files with recognized shebangs are indexed for shell (`sh`, `bash`, `zsh`, `fish`, `dash`, `ksh`, `ash`), Python, Ruby, Node.js, PHP, Lua, and PowerShell. ### Language extraction matrix @@ -1919,6 +1922,7 @@ commands and when to fall back to `search`. | GLSL / HLSL / Metal / WGSL | entry points, structs, type aliases, resource bindings, constant buffers, samplers, textures, uniforms/inputs/outputs | none yet | Shader entry points and resource declarations are searchable as symbols; use `search` for data-flow, binding compatibility, and call/reference questions. | | Verilog / SystemVerilog / VHDL | modules, packages, interfaces, classes, functions/tasks/processes, types, signals/parameters | none yet | HDL declarations are available to `symbols`, `definition`, `outline`, and symbol-aware `search`; use plain `search` for netlist/reference questions. | | Shell / PowerShell / Batch / Makefile / CMake / Justfile / MSBuild / Gradle | functions, labels, targets, recipes, tasks, imports where applicable | command-style calls, target dependencies, and control-flow targets | Runtime command construction is not resolved. | +| Solution / application manifest | solution projects and manifest identity/settings | solution project references; application manifests are symbol-only | `.sln` project paths are graph edges for repository structure; use `symbols --lang app_manifest` for Windows manifest metadata. | | SQL / Terraform / Dockerfile | statements/resources/stages/labels | table/resource/stage references, Dockerfile stage dependencies, Terraform dotted refs | SQL hotspot grouping defaults to statements; Dockerfile `COPY --from=` follows named stages. | | Markdown / HTML / CSS / Sass / Stylus / XAML / GraphQL / Protobuf | headings, anchors, selectors, UI elements, schema types/messages where supported | links/assets/components, local anchors, CSS/Sass/Stylus imports, variables, mixins/functions, XAML resources/bindings/handlers, schema references where supported | Use `search` for prose and generated markup. | | Dependency manifests / lockfiles | none | none | Use `--lang dependency_manifest` or `--lang dependency_lock` for dependency/security audits. | @@ -4400,6 +4404,8 @@ indexing はファイル単位の SQLite transaction を commit します。長 | Zig | `.zig` | yes | | XAML | `.xaml`, `.axaml` | yes | | MSBuild | `.csproj`, `.fsproj`, `.vbproj`, `.props`, `.targets` | yes | +| ソリューション | `.sln` | yes | +| アプリケーションマニフェスト | `.manifest` | yes | | Shell | `.sh`, `.bash`, `.zsh`, `.fish` | partial | | PowerShell | `.ps1`, `.psm1`, `.psd1` | yes | | Batch | `.bat`, `.cmd` | yes | @@ -4438,6 +4444,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 - JavaScript/TypeScript import: static import、dynamic import、CommonJS `require` / `require.resolve`、`import.meta.resolve`、`new URL(..., import.meta.url)`、`importScripts`、Service Worker registration、worklet load、worker constructor は、specifier が静的なら `import` シンボルを追加します。`tsconfig.json` / `jsconfig.json` の `compilerOptions.baseUrl` と `paths` alias は、対象ファイルが存在する場合に indexed project path へ解決します。 - Node モジュール構成: `.cjs` / `.mjs` は JavaScript、`.cts` / `.mts`(`.d.cts` / `.d.mts` を含む)は TypeScript として扱います。 - Dependency manifest / lockfile: dependency / security audit では `--lang dependency_manifest` または `--lang dependency_lock` を使います。この bucket は検索可能な text として扱われ、symbol / graph 抽出は主張しません。 +- ソリューションとアプリケーションマニフェスト: `.sln` は project entry をシンボルとして公開し、project path を参照として記録します。`.manifest` は assembly identity、requested execution level、supported OS、long-path 設定をシンボルとして公開します。 - 拡張子なしスクリプト: 先頭行の shebang が shell (`sh`, `bash`, `zsh`, `fish`, `dash`, `ksh`, `ash`)、Python、Ruby、Node.js、PHP、Lua、PowerShell として認識できれば index 対象です。 ### 言語別 extraction matrix @@ -4460,6 +4467,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 | GLSL / HLSL / Metal / WGSL | entry point、struct、type alias、resource binding、constant buffer、sampler、texture、uniform/input/output | まだなし | Shader entry point と resource 宣言はシンボルとして検索できます。data-flow、binding compatibility、call/reference の調査には `search` を使ってください。 | | Verilog / SystemVerilog / VHDL | module、package、interface、class、function/task/process、type、signal/parameter | まだなし | HDL 宣言は `symbols`、`definition`、`outline`、symbol-aware `search` で使えます。netlist / reference の調査には通常の `search` を使ってください。 | | Shell / PowerShell / Batch / Makefile / CMake / Justfile / MSBuild / Gradle | function、label、target、recipe、task、対応言語の import | command-style call、target dependency、control-flow target | runtime で組み立てられる command は解決しません。 | +| ソリューション / アプリケーションマニフェスト | solution project、manifest identity / setting | `.sln` の project reference。manifest は symbol-only | `.sln` の project path はリポジトリ構造の graph edge です。Windows manifest metadata は `symbols --lang app_manifest` で確認できます。 | | SQL / Terraform / Dockerfile | statement/resource/stage/label | table/resource/stage reference、Dockerfile stage dependency、Terraform dotted refs | SQL hotspot grouping は既定で statement、Dockerfile `COPY --from=` は named stage を追跡します。 | | Markdown / HTML / CSS / Sass / Stylus / XAML / GraphQL / Protobuf | heading、anchor、selector、UI element、対応 schema type/message | link/asset/component、local anchor、CSS/Sass/Stylus の import・variable・mixin/function、XAML resource / binding / handler、対応 schema reference | prose や generated markup には `search` を使ってください。 | | Dependency manifest / lockfile | なし | なし | dependency / security audit には `--lang dependency_manifest` または `--lang dependency_lock` を使います。 | diff --git a/changelog.d/unreleased/3662.added.md b/changelog.d/unreleased/3662.added.md new file mode 100644 index 0000000000..65b878af6c --- /dev/null +++ b/changelog.d/unreleased/3662.added.md @@ -0,0 +1,26 @@ +--- +category: added +issues: + - 3662 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs + - src/CodeIndex/Indexer/SolutionFileParser.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.StructuralMetadata.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.StructuralMetadata.cs + - src/CodeIndex/Models/SymbolKindCatalog.cs + - tests/CodeIndex.Tests/FileIndexerTests.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs + - USER_GUIDE.md +--- + +## English + +- **Solution and application manifest files now have structural extraction (#3662)** — `.sln` files expose project symbols and project-reference edges, while `.manifest` files expose key assembly identity and application compatibility settings as symbols. + +## 日本語 + +- **Solution と application manifest ファイルが構造化抽出に対応しました (#3662)** — `.sln` は project シンボルと project-reference edge を公開し、`.manifest` は主要な assembly identity と application compatibility 設定をシンボルとして公開します。 diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 55e81707e7..81cba72c8c 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -172,13 +172,15 @@ ELSE 0 // are dependency edges from a lambda body back to an enclosing local and participate in impact. // C# generic type arguments are retained only when they are attached to an explicit invocation, // so impact can follow `Process(x)` without promoting ordinary type annotations to - // call-graph edges (#2062). + // call-graph edges (#2062). Solution `project_reference` edges are retained because solution + // files are advertised as graph-supported structural metadata (#3662). // call-graph (callers/callees/hotspots) に参加する reference kind。`attribute` / `annotation` // のようなメタデータ kind は非呼び出しエッジなのでここから除外する (issue #293)。 // Razor の `razor_event_binding`、React の `consumes_hook`、C++ の `friend` は依存関係 graph query に含める。 // C# closure の `capture` は lambda 本体から外側 local への依存であり、impact に参加する。 - // C# generic invocation type arguments are included when tied to an actual call (#2062). - internal const string CallGraphReferenceKindsSql = "('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook', 'capture')"; + // C# generic invocation type arguments are included when tied to an actual call (#2062)。 + // solution の `project_reference` は structural metadata として graph 対応を告知しているため含める (#3662)。 + internal const string CallGraphReferenceKindsSql = "('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook', 'capture', 'project_reference')"; private const string SyntheticTopLevelCallerName = ""; private const string SyntheticTopLevelCallerKind = "function"; diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs index 59c02fd00d..d93d9ee2c3 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs @@ -42,6 +42,9 @@ internal static List ExtractCore(ReferenceExtractionContext req var typeScriptTypeAliases = language == "typescript" ? TypeScriptReferenceExtractor.BuildTypeAliasTargets(preparedLines) : null; + if (language == "solution") + return ExtractSolutionReferences(fileId, lines); + var swiftTypeAliases = language == "swift" ? SwiftReferenceExtractor.BuildTypeAliasTargets(preparedLines) : null; diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs index 7ca43204e5..6747524099 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs @@ -31,6 +31,7 @@ internal sealed class CSharpWhereConstraintState "assembly", "r", "powershell", "shell", "haskell", "solidity", "gradle", "terraform", "protobuf", "dockerfile", "makefile", "cmake", "justfile", "msbuild", + "solution", "zig", "css", "sass", "stylus", "xml", "graphql", "html", "markdown", "fortran", "pascal", "objc", "smalltalk" ]; diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.StructuralMetadata.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.StructuralMetadata.cs new file mode 100644 index 0000000000..4abd093d53 --- /dev/null +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.StructuralMetadata.cs @@ -0,0 +1,32 @@ +using CodeIndex.Models; + +namespace CodeIndex.Indexer; + +public static partial class ReferenceExtractor +{ + private static List ExtractSolutionReferences(long fileId, string[] lines) + { + var references = CreateReferenceList(null); + var seen = new HashSet(StringComparer.Ordinal); + foreach (var project in SolutionFileParser.ExtractProjects(lines)) + { + AddReference( + references, + seen, + fileId, + project.NormalizedProjectPath, + project.PathIndex, + "project_reference", + project.Context, + project.LineNumber, + new SymbolRecord + { + Kind = "project", + Name = project.Name, + }, + "solution"); + } + + return references; + } +} diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index 5c46d486d2..fda6d7bda1 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -202,6 +202,8 @@ internal readonly record struct ProjectMarkerFingerprintResult(string? Fingerpri [".toml"] = "toml", [".xaml"] = "xml", // WPF/MAUI/Avalonia XAML / XAML テンプレート [".axaml"] = "xml", // Avalonia XAML / Avalonia XAML + [".sln"] = "solution", // Visual Studio solution / Visual Studio ソリューション + [".manifest"] = "app_manifest", // Windows application manifest / Windows アプリケーションマニフェスト [".csproj"] = "msbuild",// C# project file / C# プロジェクトファイル [".fsproj"] = "msbuild",// F# project file / F# プロジェクトファイル [".vbproj"] = "msbuild",// VB.NET project file / VB.NET プロジェクトファイル diff --git a/src/CodeIndex/Indexer/SolutionFileParser.cs b/src/CodeIndex/Indexer/SolutionFileParser.cs new file mode 100644 index 0000000000..bb9144cbc7 --- /dev/null +++ b/src/CodeIndex/Indexer/SolutionFileParser.cs @@ -0,0 +1,78 @@ +using System.Text.RegularExpressions; +using Regex = CodeIndex.Indexer.BoundedRegex; + +namespace CodeIndex.Indexer; + +internal readonly record struct SolutionProjectEntry( + string Name, + string ProjectPath, + string NormalizedProjectPath, + int LineNumber, + int NameIndex, + int PathIndex, + string Context); + +internal static class SolutionFileParser +{ + private static readonly Regex ProjectLineRegex = new( + @"^\s*Project\(""(?[^""]+)""\)\s*=\s*""(?[^""]+)""\s*,\s*""(?[^""]+)""\s*,\s*""(?[^""]+)""", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly HashSet SolutionFolderTypeGuids = new(StringComparer.OrdinalIgnoreCase) + { + "2150E333-8FDC-42A3-9474-1A3956D46DE8", + "66A26720-8FB5-11D2-AA7E-00C04F688DDE", + }; + + internal static List ExtractProjects(string[] lines) + { + var entries = new List(); + for (var i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var match = ProjectLineRegex.Match(line); + if (!match.Success) + continue; + + var typeGuid = NormalizeGuid(match.Groups["typeGuid"].Value); + if (SolutionFolderTypeGuids.Contains(typeGuid)) + continue; + + var name = match.Groups["name"].Value.Trim(); + var projectPath = match.Groups["path"].Value.Trim(); + if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(projectPath)) + continue; + + var normalizedPath = NormalizeProjectPath(projectPath); + if (!IsProjectPath(normalizedPath)) + continue; + + entries.Add(new SolutionProjectEntry( + name, + projectPath, + normalizedPath, + i + 1, + match.Groups["name"].Index, + match.Groups["path"].Index, + line.Trim())); + } + + return entries; + } + + private static string NormalizeProjectPath(string projectPath) + => projectPath.Replace('\\', '/').Trim(); + + private static string NormalizeGuid(string guid) + => guid.Trim().Trim('{', '}'); + + private static bool IsProjectPath(string projectPath) + { + if (string.IsNullOrWhiteSpace(projectPath)) + return false; + + var fileName = Path.GetFileName(projectPath); + return !string.IsNullOrWhiteSpace(fileName) + && !string.IsNullOrWhiteSpace(Path.GetExtension(fileName)); + } +} diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.StructuralMetadata.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.StructuralMetadata.cs new file mode 100644 index 0000000000..2856e2f16f --- /dev/null +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.StructuralMetadata.cs @@ -0,0 +1,187 @@ +using System.Xml; +using CodeIndex.Models; + +namespace CodeIndex.Indexer; + +public static partial class SymbolExtractor +{ + private static readonly string[] ManifestAssemblyIdentityAttributes = + [ + "version", + "processorArchitecture", + "type", + "publicKeyToken", + ]; + + private static List ExtractSolutionSymbols(long fileId, string[] lines) + { + var symbols = new List(); + foreach (var project in SolutionFileParser.ExtractProjects(lines)) + { + symbols.Add(new SymbolRecord + { + FileId = fileId, + Kind = "project", + SubKind = GetSolutionProjectSubKind(project.NormalizedProjectPath), + Name = project.Name, + Line = project.LineNumber, + StartLine = project.LineNumber, + StartColumn = project.NameIndex, + EndLine = project.LineNumber, + Signature = project.Context, + }); + } + + return symbols; + } + + private static List ExtractAppManifestSymbols(long fileId, string content, string[] lines) + { + var symbols = new List(); + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Ignore, + IgnoreComments = true, + IgnoreProcessingInstructions = true, + XmlResolver = null, + }; + + try + { + using var reader = XmlReader.Create(new StringReader(content), settings); + while (reader.Read()) + { + if (reader.NodeType != XmlNodeType.Element) + continue; + + var elementName = reader.LocalName; + var lineNumber = reader is IXmlLineInfo lineInfo && lineInfo.HasLineInfo() + ? lineInfo.LineNumber + : 1; + + if (string.Equals(elementName, "assemblyIdentity", StringComparison.OrdinalIgnoreCase)) + { + AddManifestAssemblyIdentitySymbols(fileId, lines, symbols, reader, lineNumber); + } + else if (string.Equals(elementName, "requestedExecutionLevel", StringComparison.OrdinalIgnoreCase)) + { + AddManifestAttributeSymbol( + fileId, + lines, + symbols, + "property", + "requestedExecutionLevel.level", + reader.GetAttribute("level"), + lineNumber, + parentName: "requestedExecutionLevel"); + AddManifestAttributeSymbol( + fileId, + lines, + symbols, + "property", + "requestedExecutionLevel.uiAccess", + reader.GetAttribute("uiAccess"), + lineNumber, + parentName: "requestedExecutionLevel"); + } + else if (string.Equals(elementName, "supportedOS", StringComparison.OrdinalIgnoreCase)) + { + var id = reader.GetAttribute("Id"); + var name = string.IsNullOrWhiteSpace(id) ? "supportedOS" : $"supportedOS.{id}"; + AddManifestAttributeSymbol(fileId, lines, symbols, "property", name, id, lineNumber, parentName: "compatibility"); + } + else if (string.Equals(elementName, "longPathAware", StringComparison.OrdinalIgnoreCase)) + { + symbols.Add(CreateManifestSymbol( + fileId, + "property", + "longPathAware", + lineNumber, + lines, + parentName: "application")); + } + } + } + catch (XmlException) + { + return symbols; + } + + return symbols; + } + + private static void AddManifestAssemblyIdentitySymbols( + long fileId, + string[] lines, + List symbols, + XmlReader reader, + int lineNumber) + { + var assemblyName = reader.GetAttribute("name"); + if (!string.IsNullOrWhiteSpace(assemblyName)) + { + symbols.Add(CreateManifestSymbol(fileId, "assembly", assemblyName, lineNumber, lines, parentName: null)); + } + + foreach (var attributeName in ManifestAssemblyIdentityAttributes) + { + AddManifestAttributeSymbol( + fileId, + lines, + symbols, + "property", + $"assemblyIdentity.{attributeName}", + reader.GetAttribute(attributeName), + lineNumber, + parentName: "assemblyIdentity"); + } + } + + private static void AddManifestAttributeSymbol( + long fileId, + string[] lines, + List symbols, + string kind, + string name, + string? value, + int lineNumber, + string? parentName) + { + if (string.IsNullOrWhiteSpace(value)) + return; + + symbols.Add(CreateManifestSymbol(fileId, kind, name, lineNumber, lines, parentName)); + } + + private static SymbolRecord CreateManifestSymbol( + long fileId, + string kind, + string name, + int lineNumber, + string[] lines, + string? parentName) + { + var line = GetLineOrEmpty(lines, lineNumber).Trim(); + return new SymbolRecord + { + FileId = fileId, + Kind = kind, + Name = name, + Line = lineNumber, + StartLine = lineNumber, + EndLine = lineNumber, + Signature = string.IsNullOrEmpty(line) ? null : line, + ContainerKind = parentName == null ? null : "namespace", + ContainerName = parentName, + ContainerQualifiedName = parentName, + }; + } + + private static string GetSolutionProjectSubKind(string projectPath) + { + var extension = Path.GetExtension(projectPath); + return string.IsNullOrWhiteSpace(extension) + ? "project" + : extension.TrimStart('.').ToLowerInvariant(); + } +} diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 5decd153c5..54b3dbb0ea 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -61,7 +61,7 @@ public static int GetContractVersion(string? lang) "clojure" or "erlang" or "ocaml" or "raku" => FunctionalLanguageContractVersion, "crystal" or "groovy" or "julia" or "tcl" => DynamicLanguageContractVersion, "ada" or "d" or "nim" => SystemsLanguageContractVersion, - "cmake" or "graphql" or "html" or "json" or "justfile" or "markdown" or "msbuild" or "yaml" => ExpandedLanguageContractVersion, + "app_manifest" or "cmake" or "graphql" or "html" or "json" or "justfile" or "markdown" or "msbuild" or "solution" or "yaml" => ExpandedLanguageContractVersion, _ => DefaultContractVersion, }; } @@ -2306,7 +2306,7 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult /// public static IReadOnlyCollection GetSupportedLanguages() => PatternCache.Keys - .Concat(new[] { "commonlisp", "racket", "vue", "svelte", "markdown", "json", "yaml", "xml", "razor", "blazor", "cshtml", "solidity", "cuda" }) + .Concat(new[] { "app_manifest", "commonlisp", "racket", "vue", "svelte", "markdown", "json", "yaml", "xml", "razor", "blazor", "cshtml", "solidity", "solution", "cuda" }) .Concat(ExtractorPluginRegistry.SymbolLanguages) .Distinct(StringComparer.Ordinal) .ToArray(); @@ -2463,6 +2463,16 @@ public static List Extract(long fileId, string? lang, string conte return ExtractMsBuildSymbols(fileId, content, content.Split('\n')); } + if (lang == "solution") + { + return ExtractSolutionSymbols(fileId, content.Split('\n')); + } + + if (lang == "app_manifest") + { + return ExtractAppManifestSymbols(fileId, content, content.Split('\n')); + } + if (lang == "markdown") { var markdownLines = content.Split('\n'); diff --git a/src/CodeIndex/Models/SymbolKindCatalog.cs b/src/CodeIndex/Models/SymbolKindCatalog.cs index 23a43afada..74996b683b 100644 --- a/src/CodeIndex/Models/SymbolKindCatalog.cs +++ b/src/CodeIndex/Models/SymbolKindCatalog.cs @@ -11,6 +11,7 @@ public static class SymbolKindCatalog "accessor", "add", "annotation", + "assembly", "async_function", "async_generator", "attribute", @@ -48,6 +49,7 @@ public static class SymbolKindCatalog "property", "procedure", "program", + "project", "protocol", "protocol_impl", "reference", @@ -100,6 +102,7 @@ public static class SymbolKindCatalog "join_condition_reference", "lifetime_reference", "metadata", + "project_reference", "reference", "stage", "razor_event_binding", diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 0ace55d68e..5a5061b247 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -372,6 +372,25 @@ void Process(T item) {} Assert.Equal("generic_type_argument", caller.ReferenceKind); } + [Fact] + public void GetCallers_SolutionProjectReference_ParticipatesInGraph_Issue3662() + { + InsertManualReference( + "CodeIndex.sln", + "solution", + "project", + "App", + "src/App/App.csproj", + "project_reference"); + + var caller = Assert.Single(_reader.GetCallers("src/App/App.csproj", lang: "solution", exact: true)); + + Assert.Equal("CodeIndex.sln", caller.Path); + Assert.Equal("App", caller.CallerName); + Assert.Equal("src/App/App.csproj", caller.CalleeName); + Assert.Equal("project_reference", caller.ReferenceKind); + } + [Fact] public void CreateSearchReferencesCommand_RanksWithoutLoweringReferenceNames() { diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 7f958649d8..8df9f697ef 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -568,6 +568,8 @@ public void ScanFilesDetailed_HardlinkedFiles_SkipsDuplicatePathWithWarning() [InlineData("shader.hlsl", "hlsl")] [InlineData("shader.wgsl", "wgsl")] [InlineData("shader.metal", "metal")] + [InlineData("CodeIndex.sln", "solution")] + [InlineData("app.manifest", "app_manifest")] [InlineData("cpu.s", "assembly")] [InlineData("cpu.S", "assembly")] [InlineData("cpu.asm", "assembly")] @@ -1361,6 +1363,8 @@ public void GetLanguageExtensions_ExposesPrefixAndFileNameVariants() Assert.Equal("dependency_lock", map["go.sum"]); Assert.Equal("dependency_lock", map["uv.lock"]); Assert.Equal("dependency_lock", map["packages.lock.json"]); + Assert.Equal("solution", map[".sln"]); + Assert.Equal("app_manifest", map[".manifest"]); Assert.Equal("assembly", map[".s"]); Assert.Equal("assembly", map[".S"]); @@ -3464,6 +3468,44 @@ public void ScanFilesDetailed_SeparatesUnknownExtensionsFromOtherNonIndexableFil } } + [Fact] + public void ScanFilesDetailed_TreatsSolutionAndManifestAsKnownStructuralFiles_Issue3662() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText(Path.Combine(tempDir, "App.sln"), """ + Microsoft Visual Studio Solution File, Format Version 12.00 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{11111111-1111-1111-1111-111111111111}" + EndProject + """); + File.WriteAllText(Path.Combine(tempDir, "app.manifest"), """ + + + + + """); + File.WriteAllText(Path.Combine(tempDir, "data.mystery"), "unknown extension\n"); + + var indexer = new FileIndexer(tempDir); + var scanResult = indexer.ScanFilesDetailed(); + var files = scanResult.Files + .Select(path => Path.GetRelativePath(tempDir, path).Replace('\\', '/')) + .OrderBy(path => path, StringComparer.Ordinal) + .ToArray(); + + Assert.Equal(["App.sln", "app.manifest"], files); + Assert.Equal(["data.mystery"], scanResult.UnknownExtensionFiles); + Assert.DoesNotContain("App.sln", scanResult.UnknownExtensionFiles); + Assert.DoesNotContain("app.manifest", scanResult.UnknownExtensionFiles); + } + finally + { + Directory.Delete(tempDir, true); + } + } + [Fact] public void DetectLanguage_ExtensionlessShebangs_HonorsUnicodeBomEncodings() { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 93f78b4278..8083139c4e 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -169,6 +169,35 @@ public void Extract_CustomReferencePlugin_HandlesUnsupportedLanguage() } } + [Fact] + public void Extract_Solution_IndexesProjectPathReferences_Issue3662() + { + const string content = """ + Microsoft Visual Studio Solution File, Format Version 12.00 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{11111111-1111-1111-1111-111111111111}" + EndProject + Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "PythonApp", "tools\PythonApp\PythonApp.pyproj", "{33333333-3333-3333-3333-333333333333}" + EndProject + """; + + var symbols = SymbolExtractor.Extract(1, "solution", content); + var references = ReferenceExtractor.Extract(1, "solution", content, symbols); + Assert.Equal(2, references.Count); + + var reference = Assert.Single(references, item => item.SymbolName == "src/App/App.csproj"); + Assert.Equal("project_reference", reference.ReferenceKind); + Assert.Equal("project", reference.ContainerKind); + Assert.Equal("App", reference.ContainerName); + Assert.Equal(2, reference.Line); + Assert.True(reference.Column > 0); + + var pythonReference = Assert.Single(references, item => item.SymbolName == "tools/PythonApp/PythonApp.pyproj"); + Assert.Equal("project_reference", pythonReference.ReferenceKind); + Assert.Equal("project", pythonReference.ContainerKind); + Assert.Equal("PythonApp", pythonReference.ContainerName); + Assert.Equal(4, pythonReference.Line); + } + [Fact] public void Extract_CMake_BuildAutomationReferences() { diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 5f5621333c..d038b7bc34 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -153,6 +153,75 @@ public void Extract_CustomSymbolPlugin_HandlesUnsupportedLanguage() } } + [Fact] + public void Extract_Solution_IndexesProjectEntries_Issue3662() + { + const string content = """ + Microsoft Visual Studio Solution File, Format Version 12.00 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{11111111-1111-1111-1111-111111111111}" + EndProject + Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "PythonApp", "tools\PythonApp\PythonApp.pyproj", "{33333333-3333-3333-3333-333333333333}" + EndProject + Project("{66A26720-8FB5-11D2-AA7E-00C04F688DDE}") = "Solution Items", "Solution Items", "{22222222-2222-2222-2222-222222222222}" + EndProject + """; + + var symbols = SymbolExtractor.Extract(1, "solution", content); + Assert.Equal(2, symbols.Count); + + var project = Assert.Single(symbols, symbol => symbol.Name == "App"); + Assert.Equal("project", project.Kind); + Assert.Equal("csproj", project.SubKind); + Assert.Equal(2, project.Line); + Assert.Contains(@"src\App\App.csproj", project.Signature, StringComparison.Ordinal); + + var pythonProject = Assert.Single(symbols, symbol => symbol.Name == "PythonApp"); + Assert.Equal("project", pythonProject.Kind); + Assert.Equal("pyproj", pythonProject.SubKind); + Assert.Equal(4, pythonProject.Line); + Assert.Contains(@"tools\PythonApp\PythonApp.pyproj", pythonProject.Signature, StringComparison.Ordinal); + } + + [Fact] + public void Extract_AppManifest_IndexesRelevantEntries_Issue3662() + { + const string content = """ + + + + + + + + + + + + + + + + + + true + + + + """; + + var symbols = SymbolExtractor.Extract(1, "app_manifest", content); + + Assert.Contains(symbols, symbol => + symbol.Kind == "assembly" + && symbol.Name == "CodeIndex.App" + && symbol.Line == 3); + Assert.Contains(symbols, symbol => symbol.Name == "assemblyIdentity.version"); + Assert.Contains(symbols, symbol => symbol.Name == "requestedExecutionLevel.level"); + Assert.Contains(symbols, symbol => symbol.Name == "requestedExecutionLevel.uiAccess"); + Assert.Contains(symbols, symbol => symbol.Name == "supportedOS.{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"); + Assert.Contains(symbols, symbol => symbol.Name == "longPathAware"); + } + [Fact] public void Extract_Cython_DetectsNativeDeclarations_Issue3530() { diff --git a/tests/CodeIndex.Tests/golden/status.json b/tests/CodeIndex.Tests/golden/status.json index bfb25f7c46..f2d1e7f82c 100644 --- a/tests/CodeIndex.Tests/golden/status.json +++ b/tests/CodeIndex.Tests/golden/status.json @@ -74,6 +74,7 @@ "shell", "smalltalk", "solidity", + "solution", "sql", "stylus", "svelte",