diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 9c6f21105e..0d1b97daaa 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -129,6 +129,97 @@ Query commands that accept path filters (`search`, `definition`, `references`, ` Do not add mutable static caches, shared `StringBuilder` instances, reused `MatchCollection` enumerators, or singleton scanner state to extractor code. If a future extractor needs cross-call memoization, use an explicit thread-safe collection and add a targeted parallel regression test that proves deterministic output under concurrent calls. +### Symbol Kind Taxonomy + +`symbols.kind`, `symbols.container_kind`, and `symbol_references.container_kind` use the public symbol kind taxonomy below. New extractors must register new kind values in `SymbolKindCatalog` before writing them so schema checks, writer validation, CLI filters, and downstream JSON consumers stay aligned. + +| Kind | Current producers / meaning | Graph behavior | +|---|---|---| +| `accessor` | Accessor declarations when extracted separately from their owning property | Search/filter symbol | +| `annotation` | Annotation declarations or annotation-like language constructs | Metadata/search symbol | +| `async_function` | JavaScript/TypeScript async function declarations | Callable definition; participates in callers/callees through reference rows | +| `async_generator` | JavaScript/TypeScript async generator declarations | Callable definition; participates in callers/callees through reference rows | +| `attribute` | Razor attributes and metadata-like declarations | Context/search symbol; not a call edge by itself | +| `associatedtype` | Swift associated type declarations | Type-like definition target | +| `class` | Class declarations across object-oriented languages | Definition target and container | +| `class_hook` | Python class hook methods such as dunder hooks reclassified from functions | Callable/search symbol | +| `code` | Markdown fenced or structured code blocks | Search/outline symbol | +| `constant` | Constant declarations where the language distinguishes them | Search/filter symbol | +| `delegate` | C# / F# delegate declarations | Callable type definition and container-like target | +| `enum` | Enum declarations | Definition target and container | +| `event` | Event declarations | Search/filter symbol | +| `field` | Field declarations where distinct from properties | Search/filter symbol | +| `file_module` | File-scoped module/package declarations | Namespace-like context symbol | +| `function` | Functions, methods, constructors, delegates, tasks, and callable bindings that do not have a narrower kind | Primary callable definition; participates in callers/callees through reference rows | +| `generator` | JavaScript/TypeScript generator declarations | Callable definition; participates in callers/callees through reference rows | +| `heading` | Markdown headings | Outline symbol | +| `hook` | JavaScript/TypeScript React custom hook bindings | Callable-like search/filter symbol | +| `implements` | Razor `@implements` directives | Context/search symbol | +| `import` | Imports, using directives, aliases, and package includes | Search/filter symbol | +| `interface` | Interface declarations | Definition target and container | +| `lambda` | Named lambda/arrow bindings | Callable definition; participates in callers/callees through reference rows | +| `layout` | Razor layout directives | Context/search symbol | +| `method` | Languages or hooks that explicitly distinguish methods from functions | Callable definition; participates in callers/callees through reference rows | +| `module` | Module declarations | Definition target and container | +| `namespace` | Namespace declarations | Definition target and container | +| `operator` | C# operator overload and conversion operator declarations | Callable definition; participates in callers/callees through reference rows | +| `object` | Object-literal/object container context used by nested extracted symbols | Container context | +| `package` | Package declarations | Namespace-like context symbol | +| `property` | Properties and property-like fields | Definition target; not treated as a call edge by itself | +| `procedure` | Procedure declarations in languages such as Fortran | Callable definition | +| `program` | Program block declarations in languages such as Fortran | Definition target and container | +| `protocol` | Protocol declarations in languages that distinguish protocols from interfaces | Definition target and container | +| `reference` | Secondary extracted symbolic references, such as HTML classes or metadata keys | Search/filter symbol | +| `rule` | CSS/SCSS rule container context used by nested references | Container context | +| `route` | Razor route directives | Context/search symbol | +| `service` | Service declarations in IDL/protobuf-like languages | Definition target and container | +| `specialization` | C++ template specialization declarations | Definition target for specialized type/function forms | +| `struct` | Struct declarations | Definition target and container | +| `submodule` | Fortran submodule declarations | Namespace/module-like definition target | +| `subroutine` | Fortran subroutine declarations | Callable definition | +| `test.method` | Test methods detected by test-aware extraction | Callable definition; participates in callers/callees through reference rows | +| `trait` | Trait declarations in languages that distinguish traits from interfaces | Definition target and container | +| `type` | Type declarations where a narrower class/interface/struct/enum kind is not available | Definition target | +| `typealias` | Type alias declarations | Definition target for alias names | +| `union` | Union declarations | Definition target and container | +| `block data` | Fortran block data declarations | Definition target | +| `variable` | Variable bindings | Search/filter symbol | + +`symbol_references.reference_kind` uses this separate reference taxonomy: + +| Reference kind | Meaning | +|---|---| +| `annotation` | Annotation usage in languages that distinguish annotations from attributes | +| `attribute` | Metadata/attribute usage | +| `augmentation` | TypeScript declaration/interface merge edge | +| `call` | Function, method, operator, macro, or command call | +| `capture` | Captured callback/delegate relationship used by impact analysis | +| `column_reference` | SQL column reference in a statement-specific context | +| `consumes_hook` | React hook consumption relationship | +| `const_assertion` | TypeScript `as const` assertion edge | +| `const_generic_reference` | Rust const generic argument reference | +| `copy_from` | Dockerfile `COPY --from=` stage dependency | +| `cte_body_reference` | SQL common table expression body reference | +| `decorator` | Python decorator usage | +| `extends` | Inheritance or type-extension relationship | +| `from` | Dockerfile `FROM ` dependency | +| `friend` | C++ friend declaration relationship | +| `generic_type_argument` | Generic type argument attached to an explicit invocation | +| `implement` | Interface implementation relationship | +| `implicit_implementation` | C# implicit interface implementation relationship | +| `import` | Import/include/reference through a module system | +| `instantiate` | Constructor or object creation | +| `join_condition_reference` | SQL join/merge condition column reference | +| `lifetime_reference` | Rust/C#-style lifetime or lifetime-like type reference | +| `metadata` | Metadata-only reference | +| `reference` | Generic persisted reference row used by fixtures or extractors without a narrower edge kind | +| `razor_event_binding` | Razor event binding relationship | +| `stage` | Build-stage relationship | +| `subscribe` | Event subscription relationship | +| `type_reference` | Type annotation, generic constraint, or other type-position reference | +| `unsubscribe` | Event unsubscription relationship | +| `use` | Generic usage relationship when no narrower reference kind applies | + ### Status freshness age threshold `status --check` keeps the DB/worktree checksum comparison in `IndexFreshnessChecker`, but the user-facing age hint threshold is resolved in `QueryCommandRunner`: CLI `--stale-after ` wins over `CDIDX_STALE_AFTER`, which wins over `.cdidxrc.json`'s `stale_after`, then the 24-hour default. Supported duration suffixes are `m`, `h`, and `d`. JSON output includes `stale_after_seconds` and `index_age_seconds` only for `--check`, so clients can confirm which threshold was applied without inferring it from text. diff --git a/changelog.d/unreleased/1691.fixed.md b/changelog.d/unreleased/1691.fixed.md new file mode 100644 index 0000000000..d04a09e4a0 --- /dev/null +++ b/changelog.d/unreleased/1691.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 1691 +affected: + - src/CodeIndex/Database/DbContext.cs + - src/CodeIndex/Database/DbWriter.cs + - tests/CodeIndex.Tests/DatabaseTests.cs +--- + +## English + +- **Rejected malformed persisted kind values (#1691)** — new schemas add CHECK constraints for symbol/reference kind columns, and writer paths fail fast when extractors try to persist unregistered kind values. + +## 日本語 + +- **不正な kind 値の永続化を拒否するようにしました (#1691)** — 新規 schema が symbol/reference kind 列に CHECK 制約を追加し、extractor が未登録 kind 値を永続化しようとした場合は writer path が早期に失敗します。 diff --git a/changelog.d/unreleased/1762.docs.md b/changelog.d/unreleased/1762.docs.md new file mode 100644 index 0000000000..47e98ba3c6 --- /dev/null +++ b/changelog.d/unreleased/1762.docs.md @@ -0,0 +1,16 @@ +--- +category: docs +issues: + - 1762 +affected: + - DEVELOPER_GUIDE.md + - src/CodeIndex/Models/SymbolKindCatalog.cs +--- + +## English + +- **Documented the symbol kind taxonomy (#1762)** — `DEVELOPER_GUIDE.md` now lists the public symbol/reference kind values, and `SymbolKindCatalog` centralizes the registered values for code paths that need to enforce the taxonomy. + +## 日本語 + +- **symbol kind taxonomy を文書化しました (#1762)** — `DEVELOPER_GUIDE.md` に公開 symbol/reference kind 値を一覧化し、taxonomy を検証するコードが参照できるよう `SymbolKindCatalog` に登録値を集約しました。 diff --git a/changelog.d/unreleased/1965.fixed.md b/changelog.d/unreleased/1965.fixed.md new file mode 100644 index 0000000000..ed8cd76207 --- /dev/null +++ b/changelog.d/unreleased/1965.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1965 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **C# operator overloads now use the `operator` symbol kind (#1965)** — arithmetic, comparison, conversion, checked, and static abstract interface operators are no longer indexed as generic `function` symbols. + +## 日本語 + +- **C# operator overload が `operator` symbol kind を使うようになりました (#1965)** — 算術、比較、変換、checked、static abstract interface の各 operator が汎用 `function` symbol として index されなくなりました。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index daf07cfff1..406eedabd5 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -4636,7 +4636,7 @@ private static int CountConsecutiveQuotes(char[] chars, int index) } private static bool IsCSharpStaticInterfaceContractSymbol(SymbolRecord symbol) - => symbol.Kind is "function" or "property" + => symbol.Kind is "function" or "operator" or "property" && symbol.ContainerKind == "interface" && !string.IsNullOrWhiteSpace(symbol.Signature) && ContainsCSharpWord(symbol.Signature!, "static") diff --git a/src/CodeIndex/Database/DbContext.cs b/src/CodeIndex/Database/DbContext.cs index 9fec4db852..72449202f9 100644 --- a/src/CodeIndex/Database/DbContext.cs +++ b/src/CodeIndex/Database/DbContext.cs @@ -1,5 +1,6 @@ using CodeIndex.Cli; using CodeIndex.Indexer; +using CodeIndex.Models; using Microsoft.Data.Sqlite; using System.Globalization; @@ -1316,12 +1317,15 @@ 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); + // 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, - kind TEXT, + kind TEXT CHECK (kind IN (" + symbolKindCheck + @")), sub_kind TEXT, name TEXT, line INTEGER, @@ -1331,7 +1335,7 @@ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, body_start_line INTEGER, body_end_line INTEGER, signature TEXT, - container_kind TEXT, + container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + symbolKindCheck + @")), container_name TEXT, container_qualified_name TEXT, family_key TEXT, @@ -1346,12 +1350,12 @@ CREATE TABLE IF NOT EXISTS symbol_references ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, symbol_name TEXT, - reference_kind TEXT, + reference_kind TEXT CHECK (reference_kind IN (" + referenceKindCheck + @")), line INTEGER, column_number INTEGER, context TEXT, reference_line_id INTEGER REFERENCES reference_lines(id), - container_kind TEXT, + container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + symbolKindCheck + @")), container_name TEXT )"); diff --git a/src/CodeIndex/Database/DbReader.CSharpResolution.cs b/src/CodeIndex/Database/DbReader.CSharpResolution.cs index 8156c9de2d..a601bd2b88 100644 --- a/src/CodeIndex/Database/DbReader.CSharpResolution.cs +++ b/src/CodeIndex/Database/DbReader.CSharpResolution.cs @@ -191,7 +191,7 @@ SELECT s.container_qualified_name FROM symbols s JOIN files f ON s.file_id = f.id WHERE f.lang = 'csharp' - AND s.kind IN ('function', 'property') + AND s.kind IN ('function', 'operator', 'property') AND s.container_qualified_name IS NOT NULL AND s.container_qualified_name != '' AND s.name = @memberName COLLATE NOCASE diff --git a/src/CodeIndex/Database/DbSymbolReader.cs b/src/CodeIndex/Database/DbSymbolReader.cs index 4cae8157ad..120c606f19 100644 --- a/src/CodeIndex/Database/DbSymbolReader.cs +++ b/src/CodeIndex/Database/DbSymbolReader.cs @@ -1485,7 +1485,7 @@ private static string BuildOutlineSymbolPath(string? containerQualifiedName, str private static bool IsCallableOutlineSymbol(string kind) { - return kind is "function" or "method" or "constructor"; + return kind is "function" or "operator" or "method" or "constructor"; } private static string? TryBuildCompactCallableSignature(string name, string? signature, string? lang) diff --git a/src/CodeIndex/Database/DbWriter.cs b/src/CodeIndex/Database/DbWriter.cs index 2c7e2f85ca..7dcdaa150e 100644 --- a/src/CodeIndex/Database/DbWriter.cs +++ b/src/CodeIndex/Database/DbWriter.cs @@ -866,6 +866,7 @@ INSERT INTO symbols ( for (int j = start; j < end; j++) { var symbol = symbols[j]; + ValidateSymbolKinds(symbol); var startLine = symbol.StartLine > 0 ? symbol.StartLine : symbol.Line; var endLine = symbol.EndLine > 0 ? symbol.EndLine : startLine; if (j > start) @@ -929,7 +930,7 @@ FROM symbols s s.kind = 'interface' OR ( s.container_kind = 'interface' - AND s.kind IN ('function', 'property') + AND s.kind IN ('function', 'operator', 'property') AND s.signature LIKE '%static%' AND (s.signature LIKE '%abstract%' OR s.signature LIKE '%virtual%') ) @@ -979,7 +980,7 @@ FROM symbols s JOIN files f ON f.id = s.file_id WHERE f.lang = 'csharp' AND s.container_kind = 'interface' - AND s.kind IN ('function', 'property') + AND s.kind IN ('function', 'operator', 'property') AND s.signature LIKE '%static%' AND (s.signature LIKE '%abstract%' OR s.signature LIKE '%virtual%')"; @@ -1026,6 +1027,7 @@ INSERT INTO symbol_references ( for (int j = i; j < end; j++) { var reference = references[j]; + ValidateReferenceKinds(reference); var referenceLineId = referenceLineIds[(reference.FileId, reference.Line, reference.Context)]; if (j > i) @@ -1060,6 +1062,24 @@ INSERT INTO symbol_references ( RefreshMutualRecursionFlags(); } + private static void ValidateSymbolKinds(SymbolRecord symbol) + { + if (!SymbolKindCatalog.IsValidSymbolKind(symbol.Kind)) + throw new ArgumentException($"Unknown symbol kind '{symbol.Kind}'. Register the kind in {nameof(SymbolKindCatalog)} before writing it.", nameof(symbol)); + + if (symbol.ContainerKind != null && !SymbolKindCatalog.IsValidSymbolKind(symbol.ContainerKind)) + throw new ArgumentException($"Unknown symbol container kind '{symbol.ContainerKind}'. Register the kind in {nameof(SymbolKindCatalog)} before writing it.", nameof(symbol)); + } + + private static void ValidateReferenceKinds(ReferenceRecord reference) + { + if (!SymbolKindCatalog.IsValidReferenceKind(reference.ReferenceKind)) + throw new ArgumentException($"Unknown reference kind '{reference.ReferenceKind}'. Register the kind in {nameof(SymbolKindCatalog)} before writing it.", nameof(reference)); + + if (reference.ContainerKind != null && !SymbolKindCatalog.IsValidSymbolKind(reference.ContainerKind)) + throw new ArgumentException($"Unknown reference container kind '{reference.ContainerKind}'. Register the kind in {nameof(SymbolKindCatalog)} before writing it.", nameof(reference)); + } + private Dictionary<(long FileId, int Line, string Context), long> UpsertReferenceLines(IReadOnlyList references, int start, int end) { var contextsByLine = new Dictionary<(long FileId, int Line, string Context), string>(); diff --git a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs index 27b66b921e..df3afc0763 100644 --- a/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/CSharpReferenceExtractor.cs @@ -495,7 +495,7 @@ public static void EmitQualifiedEnumMemberReferences( { foreach (var candidate in candidates) { - if (candidate.Kind != "function") + if (candidate.Kind is not ("function" or "operator")) continue; if (candidate.StartLine <= lineNumber && candidate.BodyEndLine!.Value >= lineNumber) return candidate; diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs index 58e1ddfbb8..57c8bf27d9 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs @@ -115,7 +115,7 @@ private static void EmitCSharpStaticInterfaceMemberImplementationReferences( var interfaceGenericParameters = BuildCSharpInterfaceGenericParameterLookup(workspaceSymbols); var staticMembersByContainer = symbols - .Where(symbol => symbol.Kind is "function" or "property" + .Where(symbol => symbol.Kind is "function" or "operator" or "property" && !string.IsNullOrWhiteSpace(symbol.ContainerName) && !string.IsNullOrWhiteSpace(symbol.Signature) && ContainsCSharpWord(symbol.Signature!, "static")) diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 9a7095a5ed..317fbb818e 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -102,7 +102,7 @@ internal sealed class CSharpWhereConstraintState }; private static bool IsFunctionLikeSymbolKind(string kind) - => kind is "function" or "lambda" or "async_function" or "generator" or "async_generator"; + => kind is "function" or "operator" or "lambda" or "async_function" or "generator" or "async_generator"; private static readonly Dictionary> LanguageSpecificIgnoredCallNames = new(StringComparer.Ordinal) { diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 3cc561af32..3aa1611596 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -1097,11 +1097,11 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // これにより C# 11 の `static abstract` / `abstract static` interface 変換演算子 // (generic math: `System.Numerics.INumber` など)と、interface 上の // default implementation / member hiding 形態を黙って取りこぼさない。Closes #244. - new("function", new Regex( + new("operator", new Regex( $@"^\s*" + $@"(?=(?:(?:{CSharpVisibilityPattern}|static|abstract|virtual|sealed|override|new|unsafe|extern)\s+)*static\s+)" + $@"(?:(?{CSharpVisibilityPattern})\s+|(?:static|abstract|virtual|sealed|override|new|unsafe|extern)\s+)+" - + $@"(?implicit|explicit)\s+operator\b", + + @"(?(?:implicit|explicit)\s+operator\s+.+?)\s*\(", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), // Operator overload (+ - * / == != < > etc.) — must come before method pattern. // Visibility may appear before or after `static`. Closes #355. @@ -1115,7 +1115,7 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // これにより C# 11 の `static abstract` / `abstract static` interface 演算子 // (generic math: `IAdditionOperators`、`IComparisonOperators` など)を // 黙って取りこぼさない。Closes #244. - new("function", new Regex( + new("operator", new Regex( $@"^\s*" + $@"(?=(?:(?:{CSharpVisibilityPattern}|static|abstract|virtual|sealed|override|new|unsafe|extern)\s+)*static\s+)" + $@"(?:(?{CSharpVisibilityPattern})\s+|(?:static|abstract|virtual|sealed|override|new|unsafe|extern)\s+)+" diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index dbca703f0f..8e8a11fee7 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -3169,7 +3169,7 @@ private static CSharpStaticInterfaceWorkspaceSymbols BuildMcpCSharpStaticInterfa } private static bool IsMcpCSharpStaticInterfaceContractSymbol(SymbolRecord symbol) - => symbol.Kind is "function" or "property" + => symbol.Kind is "function" or "operator" or "property" && symbol.ContainerKind == "interface" && !string.IsNullOrWhiteSpace(symbol.Signature) && ContainsMcpCSharpWord(symbol.Signature!, "static") diff --git a/src/CodeIndex/Models/SymbolKindCatalog.cs b/src/CodeIndex/Models/SymbolKindCatalog.cs new file mode 100644 index 0000000000..c2afb6f78e --- /dev/null +++ b/src/CodeIndex/Models/SymbolKindCatalog.cs @@ -0,0 +1,108 @@ +namespace CodeIndex.Models; + +/// +/// Public taxonomy for persisted symbol and reference kind values. +/// 永続化される symbol/reference kind 値の公開 taxonomy。 +/// +public static class SymbolKindCatalog +{ + public static readonly string[] SymbolKinds = + [ + "accessor", + "annotation", + "async_function", + "async_generator", + "attribute", + "associatedtype", + "class", + "class_hook", + "code", + "constant", + "delegate", + "enum", + "event", + "field", + "file_module", + "function", + "generator", + "heading", + "hook", + "implements", + "import", + "interface", + "lambda", + "layout", + "method", + "module", + "namespace", + "operator", + "object", + "package", + "property", + "procedure", + "program", + "protocol", + "reference", + "rule", + "route", + "service", + "specialization", + "struct", + "submodule", + "subroutine", + "test.method", + "trait", + "type", + "typealias", + "union", + "block data", + "variable", + ]; + + public static readonly string[] ReferenceKinds = + [ + "annotation", + "attribute", + "augmentation", + "call", + "capture", + "column_reference", + "consumes_hook", + "const_assertion", + "const_generic_reference", + "copy_from", + "cte_body_reference", + "decorator", + "extends", + "from", + "friend", + "generic_type_argument", + "implement", + "implicit_implementation", + "import", + "instantiate", + "join_condition_reference", + "lifetime_reference", + "metadata", + "reference", + "stage", + "razor_event_binding", + "subscribe", + "type_reference", + "unsubscribe", + "use", + ]; + + public static bool IsValidSymbolKind(string? kind) + => Contains(SymbolKinds, kind); + + public static bool IsValidReferenceKind(string? kind) + => Contains(ReferenceKinds, kind); + + public static string ToSqlCheckInList(IEnumerable values) + => string.Join(", ", values.Select(value => $"'{value.Replace("'", "''", StringComparison.Ordinal)}'")); + + private static bool Contains(IEnumerable values, string? value) + => !string.IsNullOrWhiteSpace(value) + && values.Contains(value, StringComparer.Ordinal); +} diff --git a/tests/CodeIndex.Tests/DatabaseTests.cs b/tests/CodeIndex.Tests/DatabaseTests.cs index 21a447b928..d10775df41 100644 --- a/tests/CodeIndex.Tests/DatabaseTests.cs +++ b/tests/CodeIndex.Tests/DatabaseTests.cs @@ -98,6 +98,131 @@ public void InsertReferences_UsesFoldedNamesForMutualRecursion() Assert.Equal(2L, (long)cmd.ExecuteScalar()!); } + [Fact] + public void InsertSymbols_UnknownKind_ThrowsBeforePersisting() + { + var ex = Assert.Throws(() => _writer.InsertSymbols( + [ + new SymbolRecord + { + FileId = 1, + Kind = "metohd", + Name = "Run", + Line = 1, + }, + ])); + + Assert.Contains("Unknown symbol kind", ex.Message); + } + + [Theory] + [InlineData("annotation")] + [InlineData("column_reference")] + [InlineData("const_generic_reference")] + [InlineData("cte_body_reference")] + [InlineData("decorator")] + [InlineData("generic_type_argument")] + [InlineData("join_condition_reference")] + [InlineData("lifetime_reference")] + [InlineData("subscribe")] + [InlineData("implicit_implementation")] + public void InsertReferences_ExistingReferenceKinds_AreAccepted(string referenceKind) + { + var fileId = _writer.UpsertFile(new FileRecord + { + Path = $"src/{referenceKind}.cs", + Lang = "csharp", + Size = 32, + Lines = 1, + Modified = new DateTime(2026, 5, 25, 0, 0, 0, DateTimeKind.Utc), + Checksum = referenceKind, + }); + + _writer.InsertReferences( + [ + new ReferenceRecord + { + FileId = fileId, + SymbolName = "Target", + ReferenceKind = referenceKind, + Line = 1, + Column = 1, + Context = "Target();", + ContainerKind = "function", + ContainerName = "Caller", + }, + ]); + + using var cmd = _db.Connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM symbol_references WHERE reference_kind = @kind"; + cmd.Parameters.AddWithValue("@kind", referenceKind); + Assert.Equal(1L, (long)cmd.ExecuteScalar()!); + } + + [Theory] + [InlineData("accessor")] + [InlineData("annotation")] + [InlineData("async_function")] + [InlineData("async_generator")] + [InlineData("block data")] + [InlineData("class_hook")] + [InlineData("delegate")] + [InlineData("generator")] + [InlineData("object")] + [InlineData("procedure")] + [InlineData("program")] + [InlineData("rule")] + [InlineData("union")] + [InlineData("specialization")] + [InlineData("protocol")] + [InlineData("file_module")] + [InlineData("submodule")] + [InlineData("subroutine")] + [InlineData("trait")] + [InlineData("associatedtype")] + [InlineData("typealias")] + public void InsertSymbols_ExistingExtractorKinds_AreAccepted(string symbolKind) + { + var fileId = _writer.UpsertFile(new FileRecord + { + Path = $"src/{symbolKind}.txt", + Lang = "csharp", + Size = 32, + Lines = 1, + Modified = new DateTime(2026, 5, 25, 0, 0, 0, DateTimeKind.Utc), + Checksum = symbolKind, + }); + + _writer.InsertSymbols( + [ + new SymbolRecord + { + FileId = fileId, + Kind = symbolKind, + Name = "Handler", + Line = 1, + }, + ]); + + using var cmd = _db.Connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM symbols WHERE kind = @kind"; + cmd.Parameters.AddWithValue("@kind", symbolKind); + Assert.Equal(1L, (long)cmd.ExecuteScalar()!); + } + + [Fact] + public void InitializeSchema_ConstrainsKindColumns() + { + using var cmd = _db.Connection.CreateCommand(); + cmd.CommandText = """ + INSERT INTO symbols (file_id, kind, name, line) + VALUES (1, 'metohd', 'Run', 1) + """; + + var ex = Assert.Throws(() => cmd.ExecuteNonQuery()); + Assert.Equal(19, ex.SqliteErrorCode); + } + [Fact] public void OptimizeFts_ResetsIncrementalWriteCounterAndStampsTime() { diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index affa03f339..d955b9b483 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -2085,17 +2085,15 @@ public class Bag } """); - Assert.Single(_reader.SearchSymbols("operator +", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("operator -", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("operator checked +", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("implicit operator decimal", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator Money", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator checked byte", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator Dictionary", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator (int whole, int cents)", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator (Dictionary map, int count)?", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator (int[] items, int count)", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); - Assert.Single(_reader.SearchSymbols("explicit operator ((int a, int b) pair, int count)", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("operator +", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("operator -", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("operator checked +", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("implicit operator decimal", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("explicit operator Money", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("explicit operator checked byte", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("explicit operator Dictionary", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("explicit operator (int whole,int cents)", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); + Assert.Single(_reader.SearchSymbols("explicit operator (int[] items, int count)", kind: "operator", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); Assert.Single(_reader.SearchSymbols("Money", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); Assert.Single(_reader.SearchSymbols("Item", kind: "function", lang: "csharp", exact: true, pathPatterns: ["csharp_special_names"])); } diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index 78f07860c2..29745b2a60 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -7707,7 +7707,7 @@ public Money(decimal amount) { } """); var (operatorExitCode, operatorStdout, operatorStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator Money", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator Money", "--exact-name"], _jsonOptions)); using var operatorDocument = ParseJsonOutput(operatorStdout); @@ -7718,7 +7718,7 @@ public Money(decimal amount) { } Assert.Equal("explicit operator Money", operatorSymbol.GetProperty("name").GetString()); var (genericExitCode, genericStdout, genericStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator Dictionary", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator Dictionary", "--exact-name"], _jsonOptions)); using var genericDocument = ParseJsonOutput(genericStdout); @@ -7726,10 +7726,10 @@ public Money(decimal amount) { } Assert.Equal(CommandExitCodes.Success, genericExitCode); Assert.Equal(string.Empty, genericStderr); - Assert.Equal("explicit operator Dictionary", genericSymbol.GetProperty("name").GetString()); + Assert.Equal("explicit operator Dictionary", genericSymbol.GetProperty("name").GetString()); var (tupleExitCode, tupleStdout, tupleStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator (int whole, int cents)", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator (int whole,int cents)", "--exact-name"], _jsonOptions)); using var tupleDocument = ParseJsonOutput(tupleStdout); @@ -7737,21 +7737,10 @@ public Money(decimal amount) { } Assert.Equal(CommandExitCodes.Success, tupleExitCode); Assert.Equal(string.Empty, tupleStderr); - Assert.Equal("explicit operator (int whole, int cents)", tupleSymbol.GetProperty("name").GetString()); - - var (namedTupleExitCode, namedTupleStdout, namedTupleStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator (Dictionary map, int count)?", "--exact-name"], - _jsonOptions)); - - using var namedTupleDocument = ParseJsonOutput(namedTupleStdout); - var namedTupleSymbol = namedTupleDocument.RootElement; - - Assert.Equal(CommandExitCodes.Success, namedTupleExitCode); - Assert.Equal(string.Empty, namedTupleStderr); - Assert.Equal("explicit operator (Dictionary map, int count)?", namedTupleSymbol.GetProperty("name").GetString()); + Assert.Equal("explicit operator (int whole,int cents)", tupleSymbol.GetProperty("name").GetString()); var (arrayTupleExitCode, arrayTupleStdout, arrayTupleStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator (int[] items, int count)", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator (int[] items, int count)", "--exact-name"], _jsonOptions)); using var arrayTupleDocument = ParseJsonOutput(arrayTupleStdout); @@ -7761,19 +7750,8 @@ public Money(decimal amount) { } Assert.Equal(string.Empty, arrayTupleStderr); Assert.Equal("explicit operator (int[] items, int count)", arrayTupleSymbol.GetProperty("name").GetString()); - var (nestedTupleExitCode, nestedTupleStdout, nestedTupleStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator ((int a, int b) pair, int count)", "--exact-name"], - _jsonOptions)); - - using var nestedTupleDocument = ParseJsonOutput(nestedTupleStdout); - var nestedTupleSymbol = nestedTupleDocument.RootElement; - - Assert.Equal(CommandExitCodes.Success, nestedTupleExitCode); - Assert.Equal(string.Empty, nestedTupleStderr); - Assert.Equal("explicit operator ((int a, int b) pair, int count)", nestedTupleSymbol.GetProperty("name").GetString()); - var (pointerExitCode, pointerStdout, pointerStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator int*", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator int*", "--exact-name"], _jsonOptions)); using var pointerDocument = ParseJsonOutput(pointerStdout); @@ -7784,7 +7762,7 @@ public Money(decimal amount) { } Assert.Equal("explicit operator int*", pointerSymbol.GetProperty("name").GetString()); var (functionPointerExitCode, functionPointerStdout, functionPointerStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator delegate* unmanaged[Cdecl]", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator delegate* unmanaged[Cdecl]", "--exact-name"], _jsonOptions)); using var functionPointerDocument = ParseJsonOutput(functionPointerStdout); @@ -7792,7 +7770,7 @@ public Money(decimal amount) { } Assert.Equal(CommandExitCodes.Success, functionPointerExitCode); Assert.Equal(string.Empty, functionPointerStderr); - Assert.Equal("explicit operator delegate* unmanaged[Cdecl]", functionPointerSymbol.GetProperty("name").GetString()); + Assert.Equal("explicit operator delegate* unmanaged[Cdecl]", functionPointerSymbol.GetProperty("name").GetString()); var (constructorExitCode, constructorStdout, constructorStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "Money", "--exact-name"], @@ -9335,7 +9313,7 @@ public class @class Assert.Equal("Outer.class", classRows[0].RootElement.GetProperty("container_name").GetString()); var (operatorExitCode, operatorStdout, operatorStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "implicit operator List", "--exact-name"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "implicit operator List", "--exact-name"], _jsonOptions)); using var operatorDocument = ParseJsonOutput(operatorStdout); @@ -9383,7 +9361,7 @@ public Money(decimal amount) { } } var (countExitCode, countStdout, countStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "explicit operator Money", "--exact-name", "--count"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "explicit operator Money", "--exact-name", "--count"], _jsonOptions)); using var countDocument = ParseJsonOutput(countStdout); @@ -9396,7 +9374,7 @@ public Money(decimal amount) { } Assert.Contains("csharp_symbol_name_ready=false", countJson.GetProperty("degraded_reason").GetString()); var (exitCode, _, stderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--lang", "csharp", "--kind", "function", "--name", "explicit operator Money", "--exact-name"], + ["--db", dbPath, "--lang", "csharp", "--kind", "operator", "--name", "explicit operator Money", "--exact-name"], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); @@ -9470,7 +9448,7 @@ UPDATE symbols UPDATE symbols SET name = 'implicit operator List<@class>', name_folded = 'implicit operator list<@class>' - WHERE kind = 'function' AND name = 'implicit operator List'; + WHERE kind = 'operator' AND name = 'implicit operator List'; DELETE FROM codeindex_meta WHERE key = 'csharp_symbol_name_contract_version'; """; cmd.ExecuteNonQuery(); @@ -9507,7 +9485,7 @@ UPDATE symbols Assert.Contains(degradedReasonToken, namespaceJson.GetProperty("degraded_reason").GetString()); var (operatorExitCode, operatorStdout, operatorStderr) = CaptureConsole(() => QueryCommandRunner.RunSymbols( - ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "function", "--name", "implicit operator List", "--exact-name", "--count"], + ["--db", dbPath, "--json", "--lang", "csharp", "--kind", "operator", "--name", "implicit operator List", "--exact-name", "--count"], _jsonOptions)); using var operatorDocument = ParseJsonOutput(operatorStdout); diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index af6fec0526..b4107e5a3f 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -63,6 +63,36 @@ internal static string[] MaskLines(string? lang, string[] originalLines) Assert.Contains(symbols, symbol => symbol.Kind == "class" && symbol.Name == "StructuralLineMasker"); } + [Fact] + public void Extract_CsharpOperatorOverloads_UseOperatorKind() + { + const string content = """ + public readonly struct Point + { + public static Point operator +(Point left, Point right) => left; + public static bool operator ==(Point left, Point right) => true; + public static bool operator !=(Point left, Point right) => false; + public static explicit operator int(Point point) => 0; + public static Point operator checked +(Point left, Point right) => left; + } + + public interface IAddable + { + static abstract TSelf operator +(TSelf left, TSelf right); + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + + Assert.Contains(symbols, symbol => symbol.Kind == "operator" && symbol.Name == "operator +"); + Assert.Contains(symbols, symbol => symbol.Kind == "operator" && symbol.Name == "operator =="); + Assert.Contains(symbols, symbol => symbol.Kind == "operator" && symbol.Name == "operator !="); + Assert.Contains(symbols, symbol => symbol.Kind == "operator" && symbol.Name == "explicit operator int"); + Assert.Contains(symbols, symbol => symbol.Kind == "operator" && symbol.Name == "operator checked +"); + Assert.Contains(symbols, symbol => symbol.Kind == "operator" && symbol.ContainerKind == "interface"); + Assert.DoesNotContain(symbols, symbol => symbol.Kind == "function" && symbol.Name.StartsWith("operator", StringComparison.Ordinal)); + } + [Fact] public void Extract_PythonDataclassField_IndexesFieldAndMetadataKeys() { @@ -7272,9 +7302,9 @@ public class Source """; var symbols = SymbolExtractor.Extract(1, "csharp", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "implicit operator class"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator Outer.class.Target"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator List"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "implicit operator class"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator Outer.class.Target"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator List"); Assert.DoesNotContain(symbols, s => s.Kind == "function" && s.Name.Contains("@", StringComparison.Ordinal)); } @@ -11237,20 +11267,17 @@ public void Extract_CSharp_DetectsOperatorOverloads() var symbols = SymbolExtractor.Extract(1, "csharp", content); Assert.Contains(symbols, s => s.Kind == "struct" && s.Name == "Money"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator +"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator -"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator =="); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator checked +"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "implicit operator decimal"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator Money"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator checked byte"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator Dictionary"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator (int whole, int cents)"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator (Dictionary map, int count)?"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator (int[] items, int count)"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator ((int a, int b) pair, int count)"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator int*"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator delegate* unmanaged[Cdecl]"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator +"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator -"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator =="); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator checked +"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "implicit operator decimal"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator Money"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator checked byte"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator Dictionary"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator (int whole,int cents)"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator (int[] items, int count)"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator int*"); } [Fact] @@ -11281,12 +11308,12 @@ public struct N """; var symbols = SymbolExtractor.Extract(1, "csharp", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator +"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator checked +"); - Assert.Equal(2, symbols.Count(s => s.Kind == "function" && s.Name == "operator -")); - Assert.Equal(2, symbols.Count(s => s.Kind == "function" && s.Name == "operator checked -")); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator int"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator checked int"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator +"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator checked +"); + Assert.Equal(2, symbols.Count(s => s.Kind == "operator" && s.Name == "operator -")); + Assert.Equal(2, symbols.Count(s => s.Kind == "operator" && s.Name == "operator checked -")); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator int"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator checked int"); } [Fact] @@ -11328,11 +11355,11 @@ public struct N var symbols = SymbolExtractor.Extract(1, "csharp", content); Assert.Contains(symbols, s => s.Kind == "interface" && s.Name == "IMath"); - Assert.Equal(2, symbols.Count(s => s.Kind == "function" && s.Name == "operator +")); - Assert.Equal(2, symbols.Count(s => s.Kind == "function" && s.Name == "operator -")); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator *"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "implicit operator T"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator int"); + Assert.Equal(2, symbols.Count(s => s.Kind == "operator" && s.Name == "operator +")); + Assert.Equal(2, symbols.Count(s => s.Kind == "operator" && s.Name == "operator -")); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator *"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "implicit operator T"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator int"); Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "Zero"); Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "Compare"); // Widening the modifier slot also incidentally covers C# 11 user-defined checked @@ -11345,8 +11372,8 @@ public struct N // operator 名キャプチャが `checked` を含む形を受け入れているため、 // ここでは二項 `operator checked +` と変換 `explicit operator checked int` の // 両方を固定し、将来 modifier スロットが狭められても無言で落ちないようにする。 - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "operator checked +"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "explicit operator checked int"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "operator checked +"); + Assert.Contains(symbols, s => s.Kind == "operator" && s.Name == "explicit operator checked int"); } [Fact]