From a9bac742e44ee24bdf40ea4ae92d3a6fdc5a9e2b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sat, 13 Jun 2026 23:48:54 +0900 Subject: [PATCH] Add Solidity symbol extraction --- DEVELOPER_GUIDE.md | 4 +- USER_GUIDE.md | 4 +- changelog.d/unreleased/3531.added.md | 25 +++ .../References/ReferenceExtractor.Core.cs | 3 + .../References/ReferenceExtractor.Solidity.cs | 204 ++++++++++++++++++ .../References/ReferenceExtractor.State.cs | 2 +- .../Indexer/SolidityLanguageSupport.cs | 88 ++++++++ .../Symbols/SymbolExtractor.Solidity.cs | 141 ++++++++++++ .../Indexer/Symbols/SymbolExtractor.cs | 5 +- .../QueryCommandRunnerTests.cs | 10 +- .../ReferenceExtractorSolidityTests.cs | 58 +++++ .../SymbolExtractorSolidityTests.cs | 82 +++++++ tests/CodeIndex.Tests/golden/status.json | 1 + 13 files changed, 620 insertions(+), 7 deletions(-) create mode 100644 changelog.d/unreleased/3531.added.md create mode 100644 src/CodeIndex/Indexer/References/ReferenceExtractor.Solidity.cs create mode 100644 src/CodeIndex/Indexer/SolidityLanguageSupport.cs create mode 100644 src/CodeIndex/Indexer/Symbols/SymbolExtractor.Solidity.cs create mode 100644 tests/CodeIndex.Tests/ReferenceExtractorSolidityTests.cs create mode 100644 tests/CodeIndex.Tests/SymbolExtractorSolidityTests.cs diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index aef6e23499..5f07fd95d2 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -951,7 +951,7 @@ For JavaScript / TypeScript, reference extraction also captures tagged template SQL also emits `namespace` symbols for `CREATE SCHEMA`, but the summary table above does not have a dedicated namespace column. SQL graph extraction emits `reference` edges for named source/target forms such as `FROM`, `JOIN`, `INSERT INTO`, `UPDATE`, `TRUNCATE TABLE`, `DELETE FROM`, `DELETE ... USING`, and `MERGE ... USING`; procedure and table-valued-function calls stay on the `call` path. -Additionally, 21 languages are detected and indexed as raw text without symbol extraction: cmake, clojure, crystal, dockerignore, d, editorconfig, erlang, gitignore, json, justfile, julia, markdown, nim, ocaml, solidity, svelte, tcl, toml, vue, xml, yaml. +Additionally, 20 languages are detected and indexed as raw text without symbol extraction: cmake, clojure, crystal, dockerignore, d, editorconfig, erlang, gitignore, json, justfile, julia, markdown, nim, ocaml, svelte, tcl, toml, vue, xml, yaml. VB.NET container patterns use `RegexOptions.IgnoreCase` plus `VisualBasicEnd`-based range tracking, so `Partial` spelling differences and multi-file type families still receive stable definition ranges and hotspot-family metadata. @@ -3160,7 +3160,7 @@ JavaScript / TypeScript では、reference extraction が `` gql`...` ``、`` st SQL は `CREATE SCHEMA` から `namespace` シンボルも出力するが、上の要約表には namespace 専用列はない。SQL graph extraction は `FROM`、`JOIN`、`INSERT INTO`、`UPDATE`、`TRUNCATE TABLE`、`DELETE FROM`、`DELETE ... USING`、`MERGE ... USING` のような source/target 形を `reference` edge として出力し、procedure call と table-valued function 使用は `call` 経路に残す。 -他に21言語がテキスト検索用に検出されるがシンボル抽出パターンは未対応: cmake, clojure, crystal, dockerignore, d, editorconfig, erlang, gitignore, json, justfile, julia, markdown, nim, ocaml, solidity, svelte, tcl, toml, vue, xml, yaml。 +他に20言語がテキスト検索用に検出されるがシンボル抽出パターンは未対応: cmake, clojure, crystal, dockerignore, d, editorconfig, erlang, gitignore, json, justfile, julia, markdown, nim, ocaml, svelte, tcl, toml, vue, xml, yaml。 正規表現ベースの抽出は意図的にシンプルです。AST精度よりも速度とポータビリティを優先しています。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index c15879e468..624cf0fcbf 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1749,7 +1749,7 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a | Nim | `.nim`, `.nims` | -- | | OCaml | `.ml`, `.mli` | -- | | Perl | `.pl`, `.pm`, `.t`, `.pod` | -- | -| Solidity | `.sol` | -- | +| Solidity | `.sol` | yes | | Tcl | `.tcl`, `.tk` | -- | | R | `.r`, `.R` | yes | | Haskell | `.hs`, `.lhs` | yes | @@ -4195,7 +4195,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 | Nim | `.nim`, `.nims` | -- | | OCaml | `.ml`, `.mli` | -- | | Perl | `.pl`, `.pm`, `.t`, `.pod` | -- | -| Solidity | `.sol` | -- | +| Solidity | `.sol` | yes | | Tcl | `.tcl`, `.tk` | -- | | R | `.r`, `.R` | yes | | Haskell | `.hs`, `.lhs` | yes | diff --git a/changelog.d/unreleased/3531.added.md b/changelog.d/unreleased/3531.added.md new file mode 100644 index 0000000000..94c0349ec4 --- /dev/null +++ b/changelog.d/unreleased/3531.added.md @@ -0,0 +1,25 @@ +--- +category: added +issues: + - 3531 +affected: + - src/CodeIndex/Indexer/SolidityLanguageSupport.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Solidity.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs + - src/CodeIndex/Indexer/References/ReferenceExtractor.Solidity.cs + - tests/CodeIndex.Tests/SymbolExtractorSolidityTests.cs + - tests/CodeIndex.Tests/ReferenceExtractorSolidityTests.cs + - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs + - USER_GUIDE.md + - DEVELOPER_GUIDE.md +--- + +## English + +- **Solidity files now expose symbols and graph references (#3531)** - `.sol` files index contracts, interfaces, libraries, constructors, functions, events, errors, structs, enums, modifiers, inheritance, library usage, modifier applications, event emissions, and interface-style calls for navigation and graph queries. + +## 日本語 + +- **Solidity ファイルでシンボルと graph 参照を抽出するようになりました (#3531)** - `.sol` ファイルで contract、interface、library、constructor、function、event、error、struct、enum、modifier、継承、library 使用、modifier 適用、event emit、interface 形式の call を index し、navigation と graph query に利用できるようにしました。 diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs index d02fddc743..6e6350edb5 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.Core.cs @@ -87,6 +87,9 @@ internal static List ExtractCore(ReferenceExtractionContext req // プロパティ自身に帰属させる (issue #233 参照)。 var containerCandidates = BuildReferenceContainerCandidates(symbols); var containerResolver = new InnermostContainerResolver(containerCandidates); + if (language == "solidity") + return ExtractSolidityReferences(fileId, lines, preparedLines, containerResolver); + var csharpXmlDocAttachmentScopeCandidates = BuildCSharpXmlDocAttachmentScopeCandidates(language, symbols); // Enclosing-type candidates for constructor-chain rewrites (class/struct/record; namespace excluded). // Ordered innermost-first via ascending body range. Java enums can declare constructors and diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.Solidity.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.Solidity.cs new file mode 100644 index 0000000000..baaf70a37a --- /dev/null +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.Solidity.cs @@ -0,0 +1,204 @@ +using System.Text.RegularExpressions; +using CodeIndex.Models; +using Regex = CodeIndex.Indexer.BoundedRegex; + +namespace CodeIndex.Indexer; + +public static partial class ReferenceExtractor +{ + private static readonly Regex SolidityInheritanceRegex = new( + @"^\s*(?:abstract\s+)?(?:contract|interface)\s+" + SolidityLanguageSupport.IdentifierPattern + @"\s+is\s+(?[^{;]+)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityBaseIdentifierRegex = new( + @"(?" + SolidityLanguageSupport.IdentifierPattern + @")(?:\s*\([^)]*\))?", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityUsingLibraryRegex = new( + @"^\s*using\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\s+for\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityEmitRegex = new( + @"\bemit\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\s*(?=\()", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityInterfaceCastCallRegex = new( + @"(?[A-Z][A-Za-z0-9_$]*)\s*\([^;\r\n]*?\)\s*\.\s*(?" + SolidityLanguageSupport.IdentifierPattern + @")\s*\(", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityCallableHeaderRegex = new( + @"^\s*(?:function\s+(?:" + SolidityLanguageSupport.IdentifierPattern + @")|constructor|fallback|receive)\s*\([^)]*\)(?[^{;]*)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityTailIdentifierRegex = new( + @"(?" + SolidityLanguageSupport.IdentifierPattern + @")(?:\s*\([^)]*\))?", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly HashSet SolidityModifierTailKeywords = new(StringComparer.Ordinal) + { + "public", "private", "internal", "external", "view", "pure", "payable", "virtual", "override", + "returns", "memory", "calldata", "storage", "immutable", "constant", + }; + + private static List ExtractSolidityReferences( + long fileId, + string[] rawLines, + string[] preparedLines, + InnermostContainerResolver containerResolver) + { + var matchLines = SolidityLanguageSupport.MaskCommentsAndStrings(preparedLines); + var references = new List(); + var seen = new HashSet(StringComparer.Ordinal); + + for (var i = 0; i < matchLines.Length; i++) + { + var lineNumber = i + 1; + var line = matchLines[i]; + var context = rawLines[i].Trim(); + + AddSolidityInheritanceReferences(references, seen, fileId, line, context, lineNumber, containerResolver); + AddSolidityLibraryReferences(references, seen, fileId, line, context, lineNumber, containerResolver); + AddSolidityModifierReferences(references, seen, fileId, line, context, lineNumber, containerResolver); + AddSolidityEventReferences(references, seen, fileId, line, context, lineNumber, containerResolver); + AddSolidityInterfaceCallReferences(references, seen, fileId, line, context, lineNumber, containerResolver); + } + + return references; + } + + private static void AddSolidityInheritanceReferences( + List references, + HashSet seen, + long fileId, + string line, + string context, + int lineNumber, + InnermostContainerResolver containerResolver) + { + var match = SolidityInheritanceRegex.Match(line); + if (!match.Success) + return; + + var bases = match.Groups["bases"]; + foreach (Match baseMatch in SolidityBaseIdentifierRegex.Matches(bases.Value)) + { + var name = baseMatch.Groups["name"].Value; + if (string.IsNullOrWhiteSpace(name)) + continue; + + AddSolidityReference( + references, + seen, + fileId, + name, + bases.Index + baseMatch.Groups["name"].Index, + "extends", + context, + lineNumber, + containerResolver); + } + } + + private static void AddSolidityLibraryReferences( + List references, + HashSet seen, + long fileId, + string line, + string context, + int lineNumber, + InnermostContainerResolver containerResolver) + { + var match = SolidityUsingLibraryRegex.Match(line); + if (!match.Success) + return; + + var name = match.Groups["name"]; + AddSolidityReference(references, seen, fileId, name.Value, name.Index, "use", context, lineNumber, containerResolver); + } + + private static void AddSolidityModifierReferences( + List references, + HashSet seen, + long fileId, + string line, + string context, + int lineNumber, + InnermostContainerResolver containerResolver) + { + var header = SolidityCallableHeaderRegex.Match(line); + if (!header.Success) + return; + + var tail = header.Groups["tail"]; + foreach (Match modifier in SolidityTailIdentifierRegex.Matches(tail.Value)) + { + var name = modifier.Groups["name"]; + if (SolidityModifierTailKeywords.Contains(name.Value)) + continue; + + AddSolidityReference( + references, + seen, + fileId, + name.Value, + tail.Index + name.Index, + "call", + context, + lineNumber, + containerResolver); + } + } + + private static void AddSolidityEventReferences( + List references, + HashSet seen, + long fileId, + string line, + string context, + int lineNumber, + InnermostContainerResolver containerResolver) + { + foreach (Match match in SolidityEmitRegex.Matches(line)) + { + var name = match.Groups["name"]; + AddSolidityReference(references, seen, fileId, name.Value, name.Index, "call", context, lineNumber, containerResolver); + } + } + + private static void AddSolidityInterfaceCallReferences( + List references, + HashSet seen, + long fileId, + string line, + string context, + int lineNumber, + InnermostContainerResolver containerResolver) + { + foreach (Match match in SolidityInterfaceCastCallRegex.Matches(line)) + { + var type = match.Groups["type"]; + AddSolidityReference(references, seen, fileId, type.Value, type.Index, "type_reference", context, lineNumber, containerResolver); + + var method = match.Groups["method"]; + AddSolidityReference(references, seen, fileId, method.Value, method.Index, "call", context, lineNumber, containerResolver); + } + } + + private static void AddSolidityReference( + List references, + HashSet seen, + long fileId, + string name, + int nameIndex, + string referenceKind, + string context, + int lineNumber, + InnermostContainerResolver containerResolver) + { + AddReference( + references, + seen, + fileId, + name, + nameIndex, + referenceKind, + context, + lineNumber, + containerResolver.Find(lineNumber), + "solidity"); + } +} diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs index 6e59f6cad3..9972e28f05 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.State.cs @@ -29,7 +29,7 @@ internal sealed class CSharpWhereConstraintState "java", "kotlin", "ruby", "perl", "c", "cpp", "php", "swift", "dart", "scala", "elixir", "lua", "commonlisp", "racket", "vb", "fsharp", "sql", "cobol", "batch", "assembly", - "r", "powershell", "shell", "haskell", + "r", "powershell", "shell", "haskell", "solidity", "gradle", "terraform", "protobuf", "dockerfile", "makefile", "cmake", "justfile", "msbuild", "zig", "css", "graphql", "html", "markdown", "fortran", "pascal", "objc", "smalltalk" ]; diff --git a/src/CodeIndex/Indexer/SolidityLanguageSupport.cs b/src/CodeIndex/Indexer/SolidityLanguageSupport.cs new file mode 100644 index 0000000000..0bb1949026 --- /dev/null +++ b/src/CodeIndex/Indexer/SolidityLanguageSupport.cs @@ -0,0 +1,88 @@ +using System.Text; + +namespace CodeIndex.Indexer; + +internal static class SolidityLanguageSupport +{ + internal const string IdentifierPattern = @"[A-Za-z_$][A-Za-z0-9_$]*"; + + internal static string[] MaskCommentsAndStrings(string[] lines) + { + var masked = new string[lines.Length]; + var inBlockComment = false; + + for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + var line = lines[lineIndex]; + var builder = new StringBuilder(line.Length); + var i = 0; + + while (i < line.Length) + { + if (inBlockComment) + { + if (i + 1 < line.Length && line[i] == '*' && line[i + 1] == '/') + { + builder.Append(" "); + i += 2; + inBlockComment = false; + } + else + { + builder.Append(' '); + i++; + } + + continue; + } + + if (i + 1 < line.Length && line[i] == '/' && line[i + 1] == '/') + { + builder.Append(' ', line.Length - i); + break; + } + + if (i + 1 < line.Length && line[i] == '/' && line[i + 1] == '*') + { + builder.Append(" "); + i += 2; + inBlockComment = true; + continue; + } + + if (line[i] is '"' or '\'') + { + var quote = line[i]; + builder.Append(' '); + i++; + + while (i < line.Length) + { + var current = line[i]; + builder.Append(' '); + i++; + + if (current == '\\' && i < line.Length) + { + builder.Append(' '); + i++; + continue; + } + + if (current == quote) + break; + } + + continue; + } + + builder.Append(line[i]); + i++; + } + + masked[lineIndex] = builder.ToString(); + } + + return masked; + } +} diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Solidity.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Solidity.cs new file mode 100644 index 0000000000..186e59ac15 --- /dev/null +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Solidity.cs @@ -0,0 +1,141 @@ +using System.Text.RegularExpressions; +using CodeIndex.Models; +using Regex = CodeIndex.Indexer.BoundedRegex; + +namespace CodeIndex.Indexer; + +public static partial class SymbolExtractor +{ + private static readonly Regex SolidityTypeDeclarationRegex = new( + @"^\s*(?:abstract\s+)?(?contract|interface|library)\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityFunctionDeclarationRegex = new( + @"^\s*function\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\s*\(", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityConstructorDeclarationRegex = new( + @"^\s*(?constructor)\s*\(", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityFallbackReceiveDeclarationRegex = new( + @"^\s*(?fallback|receive)\s*\(", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityEventDeclarationRegex = new( + @"^\s*event\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\s*\(", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityErrorDeclarationRegex = new( + @"^\s*error\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\s*\(", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityStructDeclarationRegex = new( + @"^\s*struct\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityEnumDeclarationRegex = new( + @"^\s*enum\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\b", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SolidityModifierDeclarationRegex = new( + @"^\s*modifier\s+(?" + SolidityLanguageSupport.IdentifierPattern + @")\s*(?:\(|\b)", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static List ExtractSoliditySymbols(long fileId, string[] lines) + { + var matchLines = SolidityLanguageSupport.MaskCommentsAndStrings(lines); + var symbols = new List(); + + for (var i = 0; i < matchLines.Length; i++) + { + var line = matchLines[i]; + var typeMatch = SolidityTypeDeclarationRegex.Match(line); + if (typeMatch.Success) + { + var keyword = typeMatch.Groups["keyword"].Value; + var kind = keyword == "interface" ? "interface" : "class"; + AddSoliditySymbol(symbols, fileId, kind, keyword, typeMatch, lines[i], matchLines, i, BodyStyle.Brace); + continue; + } + + var functionMatch = SolidityFunctionDeclarationRegex.Match(line); + if (functionMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "function", "function", functionMatch, lines[i], matchLines, i, BodyStyle.Brace); + continue; + } + + var constructorMatch = SolidityConstructorDeclarationRegex.Match(line); + if (constructorMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "function", "constructor", constructorMatch, lines[i], matchLines, i, BodyStyle.Brace); + continue; + } + + var fallbackReceiveMatch = SolidityFallbackReceiveDeclarationRegex.Match(line); + if (fallbackReceiveMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "function", fallbackReceiveMatch.Groups["name"].Value, fallbackReceiveMatch, lines[i], matchLines, i, BodyStyle.Brace); + continue; + } + + var eventMatch = SolidityEventDeclarationRegex.Match(line); + if (eventMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "event", "event", eventMatch, lines[i], matchLines, i, BodyStyle.None); + continue; + } + + var errorMatch = SolidityErrorDeclarationRegex.Match(line); + if (errorMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "type", "error", errorMatch, lines[i], matchLines, i, BodyStyle.None); + continue; + } + + var structMatch = SolidityStructDeclarationRegex.Match(line); + if (structMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "struct", "struct", structMatch, lines[i], matchLines, i, BodyStyle.Brace); + continue; + } + + var enumMatch = SolidityEnumDeclarationRegex.Match(line); + if (enumMatch.Success) + { + AddSoliditySymbol(symbols, fileId, "enum", "enum", enumMatch, lines[i], matchLines, i, BodyStyle.Brace); + continue; + } + + var modifierMatch = SolidityModifierDeclarationRegex.Match(line); + if (modifierMatch.Success) + AddSoliditySymbol(symbols, fileId, "function", "modifier", modifierMatch, lines[i], matchLines, i, BodyStyle.Brace); + } + + AssignContainers(symbols, lines, null); + PopulateDeclaredContainerQualifiedNames(symbols); + return symbols; + } + + private static void AddSoliditySymbol( + List symbols, + long fileId, + string kind, + string subKind, + Match match, + string rawLine, + string[] matchLines, + int lineIndex, + BodyStyle bodyStyle) + { + var name = match.Groups["name"]; + var (endLine, bodyStartLine, bodyEndLine) = ResolveRange(matchLines, lineIndex, bodyStyle, "solidity", match.Index); + symbols.Add(new SymbolRecord + { + FileId = fileId, + Kind = kind, + SubKind = subKind, + Name = name.Value, + Line = lineIndex + 1, + StartLine = lineIndex + 1, + StartColumn = name.Index, + EndLine = endLine, + BodyStartLine = bodyStartLine, + BodyEndLine = bodyEndLine, + Signature = rawLine.Trim(), + }); + } +} diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 2d79542bb4..aa8a05e959 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -2141,7 +2141,7 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult /// public static IReadOnlyCollection GetSupportedLanguages() => PatternCache.Keys - .Concat(new[] { "commonlisp", "racket", "vue", "svelte", "markdown", "json", "yaml", "razor", "blazor", "cshtml" }) + .Concat(new[] { "commonlisp", "racket", "vue", "svelte", "markdown", "json", "yaml", "razor", "blazor", "cshtml", "solidity" }) .Concat(ExtractorPluginRegistry.SymbolLanguages) .Distinct(StringComparer.Ordinal) .ToArray(); @@ -2328,6 +2328,9 @@ public static List Extract(long fileId, string? lang, string conte if (lang is "commonlisp" or "racket") return ExtractLispSymbols(fileId, lang, lines); + if (lang == "solidity") + return ExtractSoliditySymbols(fileId, lines); + if (lang == null || !PatternCache.TryGetValue(lang, out var patterns)) return []; diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index f78e7b3a4f..01369742b9 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -2299,7 +2299,7 @@ public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSuppor Assert.Contains("missing-references", entry.GetProperty("capability_gaps").EnumerateArray().Select(gap => gap.GetString())); } - foreach (var searchOnly in new[] { "crystal", "clojure", "d", "erlang", "julia", "nim", "ocaml", "solidity", "tcl" }) + foreach (var searchOnly in new[] { "crystal", "clojure", "d", "erlang", "julia", "nim", "ocaml", "tcl" }) { Assert.True(languages.ContainsKey(searchOnly), $"expected '{searchOnly}' to be listed"); var entry = languages[searchOnly]; @@ -2319,6 +2319,14 @@ public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSuppor Assert.True(languages["json"].GetProperty("symbol_extraction").GetBoolean(), "json must advertise symbol_extraction=true"); + Assert.True(languages.ContainsKey("solidity"), "expected 'solidity' to be listed"); + Assert.True(languages["solidity"].GetProperty("symbol_extraction").GetBoolean(), + "solidity must advertise symbol_extraction=true"); + Assert.True(languages["solidity"].GetProperty("reference_extraction").GetBoolean(), + "solidity must advertise reference_extraction=true"); + Assert.True(languages["solidity"].GetProperty("graph_queries").GetBoolean(), + "solidity must advertise graph_queries=true"); + Assert.True(languages.ContainsKey("perl"), "expected 'perl' to be listed"); Assert.True(languages["perl"].GetProperty("symbol_extraction").GetBoolean(), "perl must advertise symbol_extraction=true"); diff --git a/tests/CodeIndex.Tests/ReferenceExtractorSolidityTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorSolidityTests.cs new file mode 100644 index 0000000000..5aba9ea3b5 --- /dev/null +++ b/tests/CodeIndex.Tests/ReferenceExtractorSolidityTests.cs @@ -0,0 +1,58 @@ +using CodeIndex.Indexer; + +namespace CodeIndex.Tests; + +public partial class ReferenceExtractorTests +{ + [Fact] + public void Extract_Solidity_EmitsInheritanceLibraryModifierEventAndInterfaceReferences() + { + const string content = """ + contract Vault is Ownable, Pausable { + using SafeMath for uint256; + event Deposit(address account); + modifier onlyOwner() { _; } + constructor() onlyOwner {} + function deposit(IERC20 token, uint256 amount) external onlyOwner whenOpen { + emit Deposit(msg.sender); + IERC20(token).transfer(msg.sender, amount); + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "solidity", content); + var references = ReferenceExtractor.Extract(1, "solidity", content, symbols); + + Assert.Contains(references, reference => reference.SymbolName == "Ownable" && reference.ReferenceKind == "extends" && reference.ContainerName == "Vault"); + Assert.Contains(references, reference => reference.SymbolName == "Pausable" && reference.ReferenceKind == "extends" && reference.ContainerName == "Vault"); + Assert.Contains(references, reference => reference.SymbolName == "SafeMath" && reference.ReferenceKind == "use" && reference.ContainerName == "Vault"); + Assert.Contains(references, reference => reference.SymbolName == "onlyOwner" && reference.ReferenceKind == "call" && reference.ContainerName == "constructor"); + Assert.Contains(references, reference => reference.SymbolName == "onlyOwner" && reference.ReferenceKind == "call" && reference.ContainerName == "deposit"); + Assert.Contains(references, reference => reference.SymbolName == "whenOpen" && reference.ReferenceKind == "call" && reference.ContainerName == "deposit"); + Assert.Contains(references, reference => reference.SymbolName == "Deposit" && reference.ReferenceKind == "call" && reference.ContainerName == "deposit"); + Assert.Contains(references, reference => reference.SymbolName == "IERC20" && reference.ReferenceKind == "type_reference" && reference.ContainerName == "deposit"); + Assert.Contains(references, reference => reference.SymbolName == "transfer" && reference.ReferenceKind == "call" && reference.ContainerName == "deposit"); + } + + [Fact] + public void Extract_Solidity_IgnoresCommentsAndStrings() + { + const string content = """ + contract Vault is Ownable { + function deposit() external onlyOwner { + string memory text = "emit Phantom()"; + // emit CommentOnly(); + emit Deposit(); + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "solidity", content); + var references = ReferenceExtractor.Extract(1, "solidity", content, symbols); + + Assert.Contains(references, reference => reference.SymbolName == "Ownable" && reference.ReferenceKind == "extends"); + Assert.Contains(references, reference => reference.SymbolName == "onlyOwner" && reference.ReferenceKind == "call"); + Assert.Contains(references, reference => reference.SymbolName == "Deposit" && reference.ReferenceKind == "call"); + Assert.DoesNotContain(references, reference => reference.SymbolName is "Phantom" or "CommentOnly"); + } +} diff --git a/tests/CodeIndex.Tests/SymbolExtractorSolidityTests.cs b/tests/CodeIndex.Tests/SymbolExtractorSolidityTests.cs new file mode 100644 index 0000000000..2a67eb8791 --- /dev/null +++ b/tests/CodeIndex.Tests/SymbolExtractorSolidityTests.cs @@ -0,0 +1,82 @@ +using CodeIndex.Indexer; + +namespace CodeIndex.Tests; + +public partial class SymbolExtractorTests +{ + [Fact] + public void Extract_Solidity_DetectsContractsMembersAndRanges() + { + const string content = """ + pragma solidity ^0.8.20; + + abstract contract Vault is Ownable { + event Deposit(address indexed account, uint256 amount); + error Unauthorized(address account); + + struct Position { uint256 amount; } + enum State { Open, Closed } + + modifier onlyOwner() { _; } + + constructor(address owner) onlyOwner {} + function deposit(uint256 amount) external onlyOwner { emit Deposit(msg.sender, amount); } + fallback() external payable {} + receive() external payable {} + } + + interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); + } + + library SafeMath { + function add(uint256 a, uint256 b) internal pure returns (uint256) { return a + b; } + } + """; + + var symbols = SymbolExtractor.Extract(1, "solidity", content); + + var vault = Assert.Single(symbols.Where(symbol => symbol.Name == "Vault")); + Assert.Equal("class", vault.Kind); + Assert.Equal("contract", vault.SubKind); + Assert.Equal(3, vault.StartLine); + Assert.Equal(16, vault.EndLine); + Assert.Equal(3, vault.BodyStartLine); + Assert.Equal(16, vault.BodyEndLine); + + Assert.Contains(symbols, symbol => symbol.Name == "IERC20" && symbol.Kind == "interface" && symbol.SubKind == "interface"); + Assert.Contains(symbols, symbol => symbol.Name == "SafeMath" && symbol.Kind == "class" && symbol.SubKind == "library"); + Assert.Contains(symbols, symbol => symbol.Name == "Deposit" && symbol.Kind == "event" && symbol.SubKind == "event" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "Unauthorized" && symbol.Kind == "type" && symbol.SubKind == "error" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "Position" && symbol.Kind == "struct" && symbol.SubKind == "struct" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "State" && symbol.Kind == "enum" && symbol.SubKind == "enum" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "onlyOwner" && symbol.Kind == "function" && symbol.SubKind == "modifier" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "constructor" && symbol.Kind == "function" && symbol.SubKind == "constructor" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "deposit" && symbol.Kind == "function" && symbol.SubKind == "function" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "fallback" && symbol.Kind == "function" && symbol.SubKind == "fallback" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "receive" && symbol.Kind == "function" && symbol.SubKind == "receive" && symbol.ContainerName == "Vault"); + Assert.Contains(symbols, symbol => symbol.Name == "transfer" && symbol.Kind == "function" && symbol.ContainerName == "IERC20"); + Assert.Contains(symbols, symbol => symbol.Name == "add" && symbol.Kind == "function" && symbol.ContainerName == "SafeMath"); + } + + [Fact] + public void Extract_Solidity_IgnoresCommentsAndStrings() + { + const string content = """ + // contract Fake {} + contract Real { + string constant Text = "function Nope() public {}"; + /* + event Phantom(address indexed account); + */ + function run() external {} + } + """; + + var symbols = SymbolExtractor.Extract(1, "solidity", content); + + Assert.Contains(symbols, symbol => symbol.Name == "Real" && symbol.Kind == "class"); + Assert.Contains(symbols, symbol => symbol.Name == "run" && symbol.Kind == "function"); + Assert.DoesNotContain(symbols, symbol => symbol.Name is "Fake" or "Nope" or "Phantom"); + } +} diff --git a/tests/CodeIndex.Tests/golden/status.json b/tests/CodeIndex.Tests/golden/status.json index ee9cef41b2..cfcc0cebbc 100644 --- a/tests/CodeIndex.Tests/golden/status.json +++ b/tests/CodeIndex.Tests/golden/status.json @@ -72,6 +72,7 @@ "scala", "shell", "smalltalk", + "solidity", "sql", "svelte", "swift",