From 1da8284e82c7a4021c1aeee018328f434bdc7d53 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:03:28 +0900 Subject: [PATCH 1/5] Fix Rust cfg_attr derive references (#2052) --- changelog.d/unreleased/2052.fixed.md | 17 ++ .../Languages/RustReferenceExtractor.cs | 249 +++++++++++++++++- .../Indexer/References/ReferenceExtractor.cs | 9 + .../ReferenceExtractorTests.cs | 24 ++ 4 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/2052.fixed.md diff --git a/changelog.d/unreleased/2052.fixed.md b/changelog.d/unreleased/2052.fixed.md new file mode 100644 index 0000000000..fd8a60a10a --- /dev/null +++ b/changelog.d/unreleased/2052.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 2052 +affected: + - src/CodeIndex/Indexer/References/ReferenceExtractor.cs + - src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs +--- + +## English + +- **Rust multiline conditional derives now emit trait type references (#2052)** - `cfg_attr(..., derive(...))` attributes spanning multiple lines and nested cfg predicates now index every derived trait, including qualified trait paths. + +## 日本語 + +- **Rust の複数行条件付き derive が trait 型参照を出すようになりました (#2052)** - 複数行にまたがる `cfg_attr(..., derive(...))` とネストした cfg 条件でも、修飾付き trait path を含めて各 derive trait をインデックスします。 diff --git a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs index 3de06de5f3..4f02f99734 100644 --- a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs @@ -56,6 +56,157 @@ internal static class RustReferenceExtractor @"(?(?:(?:r#)?\w+::)*r#\w+(?:::(?:r#)?\w+)*)(?:<[^>\n]+>)?\s*\(", RegexOptions.Compiled); + public static void EmitMultilineAttributeReferences( + string[] preparedLines, + List references, + HashSet seen, + long fileId, + Func resolveContainer) + { + for (var lineIndex = 0; lineIndex < preparedLines.Length; lineIndex++) + { + var line = preparedLines[lineIndex]; + for (var column = 0; column < line.Length; column++) + { + if (line[column] != '#') + continue; + + var openBracket = FindRustAttributeOpenBracket(line, column); + if (openBracket < 0) + continue; + + var (attribute, endLineIndex, endColumn) = ReadRustAttribute(preparedLines, lineIndex, openBracket); + EmitDeriveReferencesFromAttribute( + attribute, + lineIndex, + openBracket, + references, + seen, + fileId, + resolveContainer); + + if (endLineIndex == lineIndex) + column = Math.Max(column, endColumn); + else + break; + } + } + } + + private static int FindRustAttributeOpenBracket(string line, int hashIndex) + { + var cursor = hashIndex + 1; + while (cursor < line.Length && char.IsWhiteSpace(line[cursor])) + cursor++; + if (cursor < line.Length && line[cursor] == '!') + { + cursor++; + while (cursor < line.Length && char.IsWhiteSpace(line[cursor])) + cursor++; + } + + return cursor < line.Length && line[cursor] == '[' ? cursor : -1; + } + + private static (string Attribute, int EndLineIndex, int EndColumn) ReadRustAttribute( + string[] lines, + int startLineIndex, + int openBracket) + { + var parts = new List(); + var depth = 0; + for (var lineIndex = startLineIndex; lineIndex < lines.Length; lineIndex++) + { + var line = lines[lineIndex]; + var startColumn = lineIndex == startLineIndex ? openBracket : 0; + parts.Add(line[startColumn..]); + + for (var column = startColumn; column < line.Length; column++) + { + var c = line[column]; + if (c == 'r') + { + var rawEnd = TrySkipRawString(line, column); + if (rawEnd > column) + { + column = rawEnd; + continue; + } + } + + if (c == '"' || c == '\'') + { + column = SkipQuotedString(line, column, c); + continue; + } + + if (c == '[' || c == '(') + { + depth++; + continue; + } + + if (c != ']' && c != ')') + continue; + + depth--; + if (c == ']' && depth == 0) + return (string.Join('\n', parts), lineIndex, column); + } + } + + return (string.Join('\n', parts), lines.Length - 1, lines[^1].Length); + } + + private static void EmitDeriveReferencesFromAttribute( + string attribute, + int startLineIndex, + int startColumn, + List references, + HashSet seen, + long fileId, + Func resolveContainer) + { + var deriveIndex = FindRustAttributeDeriveIndex(attribute); + if (deriveIndex < 0) + return; + + var openParen = SkipWhitespace(attribute, deriveIndex + "derive".Length); + if (openParen >= attribute.Length || attribute[openParen] != '(') + return; + + var closeParen = FindMatchingDelimiter(attribute, openParen, '(', ')'); + if (closeParen <= openParen) + return; + + var typesStart = openParen + 1; + var (lineNumber, column) = GetLineColumn(attribute, startLineIndex, startColumn, typesStart); + EmitDeriveTypeList( + attribute.Substring(typesStart, closeParen - typesStart), + column, + references, + seen, + fileId, + attribute.Trim(), + lineNumber, + resolveContainer(lineNumber, column)); + } + + private static int FindRustAttributeDeriveIndex(string attribute) + { + for (var index = 0; index < attribute.Length; index++) + { + if (!IsIdentifierAt(attribute, index, "derive")) + continue; + + var cursor = SkipWhitespace(attribute, index + "derive".Length); + if (cursor < attribute.Length && attribute[cursor] == '(') + return index; + } + + return -1; + } + public static string MaskAttributeBodies(string line) { var masked = default(char[]); @@ -235,10 +386,29 @@ private static void EmitDeriveTypeList( string context, int lineNumber, SymbolRecord? container) + => EmitDeriveTypeList( + typesGroup.Value, + typesGroup.Index, + references, + seen, + fileId, + context, + lineNumber, + container); + + private static void EmitDeriveTypeList( + string types, + int typesStartIndex, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + SymbolRecord? container) { - foreach (var (segmentStart, segmentLength) in ReferenceExtractor.SplitTopLevelCommaSpans(typesGroup.Value)) + foreach (var (segmentStart, segmentLength) in ReferenceExtractor.SplitTopLevelCommaSpans(types)) { - var fragment = typesGroup.Value.Substring(segmentStart, segmentLength); + var fragment = types.Substring(segmentStart, segmentLength); var typeStart = TypedLanguageReferenceExtractor.SkipTypePrefixTrivia(fragment, 0); var typeEnd = TypedLanguageReferenceExtractor.FindTypeExpressionEnd(fragment, typeStart); if (typeEnd <= typeStart) @@ -246,7 +416,7 @@ private static void EmitDeriveTypeList( TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( fragment.Substring(typeStart, typeEnd - typeStart), - typesGroup.Index + segmentStart + typeStart, + typesStartIndex + segmentStart + typeStart, "rust", references, seen, @@ -257,6 +427,79 @@ private static void EmitDeriveTypeList( } } + private static int FindMatchingDelimiter(string text, int openIndex, char open, char close) + { + var depth = 0; + for (var index = openIndex; index < text.Length; index++) + { + var c = text[index]; + if (c == 'r') + { + var rawEnd = TrySkipRawString(text, index); + if (rawEnd > index) + { + index = rawEnd; + continue; + } + } + + if (c == '"' || c == '\'') + { + index = SkipQuotedString(text, index, c); + continue; + } + + if (c == open) + { + depth++; + continue; + } + + if (c != close) + continue; + + depth--; + if (depth == 0) + return index; + } + + return -1; + } + + private static bool IsIdentifierAt(string text, int index, string identifier) + { + if (index > 0 && IsRustIdentifierPart(text[index - 1])) + return false; + if (index + identifier.Length > text.Length) + return false; + if (!text.AsSpan(index, identifier.Length).SequenceEqual(identifier)) + return false; + return index + identifier.Length >= text.Length || !IsRustIdentifierPart(text[index + identifier.Length]); + } + + private static (int LineNumber, int Column) GetLineColumn( + string text, + int startLineIndex, + int startColumn, + int offset) + { + var lineNumber = startLineIndex + 1; + var column = startColumn; + for (var index = 0; index < offset && index < text.Length; index++) + { + if (text[index] == '\n') + { + lineNumber++; + column = 0; + continue; + } + + column++; + } + + return (lineNumber, column); + } + public static void EmitTypePositionReferences( string preparedLine, List references, diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 612ea1dc0a..638dc19064 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -1141,6 +1141,15 @@ bool HasActiveSameFileCSharpTypeCandidate(string typeExpression, int lineNumber) references, seen); } + else if (language == "rust") + { + RustReferenceExtractor.EmitMultilineAttributeReferences( + preparedLines, + references, + seen, + fileId, + (lineNumber, _) => FindInnermostContainer(containerCandidates, lineNumber)); + } var pendingCSharpMultiLineTypePattern = default(CSharpMultiLineTypePatternState); var pendingCSharpWhereConstraint = language == "csharp" ? new CSharpWhereConstraintState() : null; var csharpLocalNamesByFunction = language == "csharp" diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index be0acd776d..115c7e4679 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -31209,6 +31209,30 @@ struct User; Assert.DoesNotContain(references, r => r.SymbolName == "cfg_attr" && r.ReferenceKind == "type_reference"); } + [Fact] + public void Extract_RustMultilineCfgAttrDeriveAttributes_CaptureTraitTypeReferences() + { + const string content = """ + #[cfg_attr( + all(test, not(miri)), + derive( + Debug, + Clone, + serde::Serialize + ) + )] + struct User; + """; + + var symbols = SymbolExtractor.Extract(1, "rust", content); + var references = ReferenceExtractor.Extract(1, "rust", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "Debug" && r.ReferenceKind == "type_reference"); + Assert.Contains(references, r => r.SymbolName == "Clone" && r.ReferenceKind == "type_reference"); + Assert.Contains(references, r => r.SymbolName == "Serialize" && r.ReferenceKind == "type_reference"); + Assert.DoesNotContain(references, r => r.SymbolName == "cfg_attr" && r.ReferenceKind == "type_reference"); + } + [Fact] public void Extract_RustAttributes_CaptureAnnotationReferences() { From d5a2f3fd860662ca64918adae7e973bba55c354d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:06:06 +0900 Subject: [PATCH 2/5] Index Rust unsafe blocks and mut refs (#2053) --- changelog.d/unreleased/2053.fixed.md | 18 ++++++++++ .../Languages/RustReferenceExtractor.cs | 33 +++++++++++++++++++ .../Indexer/Symbols/SymbolExtractor.cs | 1 + .../ReferenceExtractorTests.cs | 17 ++++++++++ tests/CodeIndex.Tests/SymbolExtractorTests.cs | 19 +++++++++++ 5 files changed, 88 insertions(+) create mode 100644 changelog.d/unreleased/2053.fixed.md diff --git a/changelog.d/unreleased/2053.fixed.md b/changelog.d/unreleased/2053.fixed.md new file mode 100644 index 0000000000..4243d037d5 --- /dev/null +++ b/changelog.d/unreleased/2053.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 2053 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs +--- + +## English + +- **Rust unsafe blocks and mutable reference types are now indexed (#2053)** - `unsafe { ... }` regions are emitted as scoped containers, and `&mut Type` positions now keep the referenced type visible for unsafe-code audits. + +## 日本語 + +- **Rust の unsafe block と mutable reference 型をインデックスするようになりました (#2053)** - `unsafe { ... }` 領域をスコープ付きコンテナとして出力し、`&mut Type` 位置の参照型も unsafe コード監査で見えるようにしました。 diff --git a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs index 4f02f99734..6ffd15416d 100644 --- a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs @@ -40,6 +40,9 @@ internal static class RustReferenceExtractor private static readonly Regex StructLiteralRegex = new( $@"(?{RustIdentifierPattern}(?:::{RustIdentifierPattern})*)(?:::\s*<(?[^>\n]+)>)?\s*\{{", RegexOptions.Compiled); + private static readonly Regex MutableReferenceTypeRegex = new( + $@"&\s*mut\s+(?{RustIdentifierPattern}(?:::{RustIdentifierPattern})*)", + RegexOptions.Compiled); // Rust macro calls use `!` plus one of `()`, `[]`, or `{}` instead of the shared trailing `(`. // Capture path-qualified macro names so `std::println!`, `log::info!`, and `my_macro!` @@ -532,9 +535,39 @@ public static void EmitTypePositionReferences( EmitAssociatedValueReceiverTypeReferences(preparedLine, references, seen, fileId, context, lineNumber, resolveContainerForColumn); EmitStructLiteralInstantiationReferences(preparedLine, references, seen, fileId, context, lineNumber, resolveContainerForColumn, enumContainer); EmitImplAndTraitTypeReferences(preparedLine, references, seen, fileId, context, lineNumber, resolveContainerForColumn); + EmitMutableReferenceTypeReferences(preparedLine, references, seen, fileId, context, lineNumber, resolveContainerForColumn); EmitGenericBoundReferences(preparedLine, references, seen, fileId, context, lineNumber, resolveContainerForColumn); } + private static void EmitMutableReferenceTypeReferences( + string preparedLine, + List references, + HashSet seen, + long fileId, + string context, + int lineNumber, + Func resolveContainerForColumn) + { + foreach (Match match in MutableReferenceTypeRegex.Matches(preparedLine)) + { + var typeGroup = match.Groups["type"]; + var typeName = NormalizeIdentifier(typeGroup.Value); + if (typeName == "self" || typeName == "Self") + continue; + + ReferenceExtractor.AddReference( + references, + seen, + fileId, + typeName, + typeGroup.Index, + "type_reference", + context, + lineNumber, + resolveContainerForColumn(typeGroup.Index)); + } + } + private static void EmitLifetimeReferences( string preparedLine, List references, diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 8f2ff12b88..0b5a2e99a9 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -1392,6 +1392,7 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // fn with expanded modifiers: async, const, unsafe, default, extern (ABI optional) / // 拡張修飾子: async, const, unsafe, default, extern(ABI は省略可) new("function", new Regex(@"^\s*(?:(?pub(?:\([^)]*\))?)\s+)?(?:(?:async|const|unsafe|default|extern(?:\s+""[^""]+"")?)\s+)*fn\s+(?(?:r#)?\w+)", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), + new("class", new Regex(@"^\s*(?unsafe)\s*\{", RegexOptions.Compiled), BodyStyle.Brace), new("struct", new Regex(@"^\s*(?:(?pub(?:\([^)]*\))?)\s+)?(?:struct|union)\s+(?(?:r#)?\w+)", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), new("enum", new Regex(@"^\s*(?:(?pub(?:\([^)]*\))?)\s+)?enum\s+(?(?:r#)?\w+)", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), // Enum variants / `Red`, `Ok(T)`, `Circle { radius: f64 }`, `Point` diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 115c7e4679..e471bca139 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -31288,6 +31288,23 @@ struct Service { && r.SymbolName is "impl" or "dyn" or "const" or "mut" or "ref" or "static"); } + [Fact] + public void Extract_RustMutableReferenceTypes_CaptureReferencedType() + { + const string content = """ + fn demo(buffer: &mut Buffer) { + let next: &mut crate::io::Cursor = todo!(); + } + """; + + var symbols = SymbolExtractor.Extract(1, "rust", content); + var references = ReferenceExtractor.Extract(1, "rust", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "Buffer" && r.ReferenceKind == "type_reference"); + Assert.Contains(references, r => r.SymbolName == "crate::io::Cursor" && r.ReferenceKind == "type_reference"); + Assert.DoesNotContain(references, r => r.SymbolName == "mut" && r.ReferenceKind == "type_reference"); + } + [Fact] public void Extract_RustLifetimeParameters_CaptureExplicitLifetimeReferences() { diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index ecd7fb5960..7b5840a68a 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -14470,6 +14470,25 @@ public void Extract_Rust_DetectsExpandedFeatures() Assert.Contains(symbols, s => s.Kind == "struct" && s.Name == "MyUnion"); } + [Fact] + public void Extract_Rust_DetectsUnsafeBlockContainer() + { + const string content = """ + fn demo() { + unsafe { + let p = Box::leak(Box::new(42)); + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "rust", content); + + var unsafeBlock = Assert.Single(symbols, s => s.Kind == "class" && s.Name == "unsafe"); + Assert.Equal(2, unsafeBlock.Line); + Assert.Equal(2, unsafeBlock.BodyStartLine); + Assert.Equal(4, unsafeBlock.BodyEndLine); + } + [Fact] public void Extract_Rust_DetectsPubUseStatements() { From 3606418c2ba3e1912734053cf9edf7dcb80d7740 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:47:39 +0900 Subject: [PATCH 3/5] Address Rust reference review findings (#2052 #2053) --- .../Languages/RustReferenceExtractor.cs | 68 ++++++++++++++----- .../ReferenceExtractorTests.cs | 23 ++++++- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs index 6ffd15416d..7ba2ce0a29 100644 --- a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs @@ -41,7 +41,7 @@ internal static class RustReferenceExtractor $@"(?{RustIdentifierPattern}(?:::{RustIdentifierPattern})*)(?:::\s*<(?[^>\n]+)>)?\s*\{{", RegexOptions.Compiled); private static readonly Regex MutableReferenceTypeRegex = new( - $@"&\s*mut\s+(?{RustIdentifierPattern}(?:::{RustIdentifierPattern})*)", + @"&\s*mut\b", RegexOptions.Compiled); // Rust macro calls use `!` plus one of `()`, `[]`, or `{}` instead of the shared trailing `(`. @@ -182,17 +182,16 @@ private static void EmitDeriveReferencesFromAttribute( if (closeParen <= openParen) return; - var typesStart = openParen + 1; - var (lineNumber, column) = GetLineColumn(attribute, startLineIndex, startColumn, typesStart); - EmitDeriveTypeList( - attribute.Substring(typesStart, closeParen - typesStart), - column, + EmitMultilineDeriveTypeList( + attribute, + openParen + 1, + closeParen, + startLineIndex, + startColumn, references, seen, fileId, - attribute.Trim(), - lineNumber, - resolveContainer(lineNumber, column)); + resolveContainer); } private static int FindRustAttributeDeriveIndex(string attribute) @@ -430,6 +429,41 @@ private static void EmitDeriveTypeList( } } + private static void EmitMultilineDeriveTypeList( + string attribute, + int typesStart, + int typesEnd, + int attributeStartLineIndex, + int attributeStartColumn, + List references, + HashSet seen, + long fileId, + Func resolveContainer) + { + var types = attribute.Substring(typesStart, typesEnd - typesStart); + foreach (var (segmentStart, segmentLength) in ReferenceExtractor.SplitTopLevelCommaSpans(types)) + { + var fragment = types.Substring(segmentStart, segmentLength); + var typeStart = TypedLanguageReferenceExtractor.SkipTypePrefixTrivia(fragment, 0); + var typeEnd = TypedLanguageReferenceExtractor.FindTypeExpressionEnd(fragment, typeStart); + if (typeEnd <= typeStart) + continue; + + var absoluteTypeStart = typesStart + segmentStart + typeStart; + var (lineNumber, column) = GetLineColumn(attribute, attributeStartLineIndex, attributeStartColumn, absoluteTypeStart); + TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( + fragment.Substring(typeStart, typeEnd - typeStart), + column, + "rust", + references, + seen, + fileId, + attribute.Trim(), + lineNumber, + resolveContainer(lineNumber, column)); + } + } + private static int FindMatchingDelimiter(string text, int openIndex, char open, char close) { var depth = 0; @@ -550,21 +584,21 @@ private static void EmitMutableReferenceTypeReferences( { foreach (Match match in MutableReferenceTypeRegex.Matches(preparedLine)) { - var typeGroup = match.Groups["type"]; - var typeName = NormalizeIdentifier(typeGroup.Value); - if (typeName == "self" || typeName == "Self") + var typeStart = TypedLanguageReferenceExtractor.SkipTypePrefixTrivia(preparedLine, match.Index + match.Length); + var typeEnd = TypedLanguageReferenceExtractor.FindTypeExpressionEnd(preparedLine, typeStart); + if (typeEnd <= typeStart) continue; - ReferenceExtractor.AddReference( + TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( + preparedLine.Substring(typeStart, typeEnd - typeStart), + typeStart, + "rust", references, seen, fileId, - typeName, - typeGroup.Index, - "type_reference", context, lineNumber, - resolveContainerForColumn(typeGroup.Index)); + resolveContainerForColumn(typeStart)); } } diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 920d332a19..7b162524d6 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -31281,6 +31281,9 @@ struct User; Assert.Contains(references, r => r.SymbolName == "Debug" && r.ReferenceKind == "type_reference"); Assert.Contains(references, r => r.SymbolName == "Clone" && r.ReferenceKind == "type_reference"); Assert.Contains(references, r => r.SymbolName == "Serialize" && r.ReferenceKind == "type_reference"); + Assert.Contains(references, r => r.SymbolName == "Debug" && r.Line == 4 && r.Column == 9); + Assert.Contains(references, r => r.SymbolName == "Clone" && r.Line == 5 && r.Column == 9); + Assert.Contains(references, r => r.SymbolName == "Serialize" && r.Line == 6 && r.Column == 16); Assert.DoesNotContain(references, r => r.SymbolName == "cfg_attr" && r.ReferenceKind == "type_reference"); } @@ -31352,10 +31355,28 @@ fn demo(buffer: &mut Buffer) { var references = ReferenceExtractor.Extract(1, "rust", content, symbols); Assert.Contains(references, r => r.SymbolName == "Buffer" && r.ReferenceKind == "type_reference"); - Assert.Contains(references, r => r.SymbolName == "crate::io::Cursor" && r.ReferenceKind == "type_reference"); + Assert.Contains(references, r => r.SymbolName == "Cursor" && r.ReferenceKind == "type_reference"); Assert.DoesNotContain(references, r => r.SymbolName == "mut" && r.ReferenceKind == "type_reference"); } + [Fact] + public void Extract_RustMutableDynAndImplReferences_CaptureTraitType() + { + const string content = """ + fn demo(writer: &mut dyn Write, parser: &mut impl Parser) { + } + """; + + var symbols = SymbolExtractor.Extract(1, "rust", content); + var references = ReferenceExtractor.Extract(1, "rust", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "Write" && r.ReferenceKind == "type_reference"); + Assert.Contains(references, r => r.SymbolName == "Parser" && r.ReferenceKind == "type_reference"); + Assert.DoesNotContain( + references, + r => r.ReferenceKind == "type_reference" && (r.SymbolName is "dyn" or "impl" or "mut")); + } + [Fact] public void Extract_RustLifetimeParameters_CaptureExplicitLifetimeReferences() { From efcc301df2e012946acd12c0c12acd53c433903b Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:59:24 +0900 Subject: [PATCH 4/5] Avoid Rust mutable borrow false references (#2053) --- .../Languages/RustReferenceExtractor.cs | 19 +++++++++++++++++++ .../ReferenceExtractorTests.cs | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs index 7ba2ce0a29..c7875fe19c 100644 --- a/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs @@ -584,6 +584,9 @@ private static void EmitMutableReferenceTypeReferences( { foreach (Match match in MutableReferenceTypeRegex.Matches(preparedLine)) { + if (!IsMutableReferenceTypeContext(preparedLine, match.Index)) + continue; + var typeStart = TypedLanguageReferenceExtractor.SkipTypePrefixTrivia(preparedLine, match.Index + match.Length); var typeEnd = TypedLanguageReferenceExtractor.FindTypeExpressionEnd(preparedLine, typeStart); if (typeEnd <= typeStart) @@ -602,6 +605,22 @@ private static void EmitMutableReferenceTypeReferences( } } + private static bool IsMutableReferenceTypeContext(string preparedLine, int ampersandIndex) + { + var cursor = ampersandIndex - 1; + while (cursor >= 0 && char.IsWhiteSpace(preparedLine[cursor])) + cursor--; + + if (cursor < 0) + return false; + if (preparedLine[cursor] == ':') + return true; + + return preparedLine[cursor] == '>' + && cursor > 0 + && preparedLine[cursor - 1] == '-'; + } + private static void EmitLifetimeReferences( string preparedLine, List references, diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 7b162524d6..12f929d97f 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -31377,6 +31377,22 @@ fn demo(writer: &mut dyn Write, parser: &mut impl Parser) { r => r.ReferenceKind == "type_reference" && (r.SymbolName is "dyn" or "impl" or "mut")); } + [Fact] + public void Extract_RustMutableBorrowExpression_DoesNotEmitTypeReference() + { + const string content = """ + fn demo(buffer: &mut Buffer) { + take(&mut buffer); + } + """; + + var symbols = SymbolExtractor.Extract(1, "rust", content); + var references = ReferenceExtractor.Extract(1, "rust", content, symbols); + + Assert.Contains(references, r => r.SymbolName == "Buffer" && r.ReferenceKind == "type_reference"); + Assert.DoesNotContain(references, r => r.SymbolName == "buffer" && r.ReferenceKind == "type_reference"); + } + [Fact] public void Extract_RustLifetimeParameters_CaptureExplicitLifetimeReferences() { From 619e65128e67b6e4f74b3ce8cca026e16f075df6 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:03:22 +0900 Subject: [PATCH 5/5] Detect Rust unsafe expression blocks (#2053) --- .../Indexer/Symbols/SymbolExtractor.cs | 2 +- tests/CodeIndex.Tests/SymbolExtractorTests.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index c8775f4c9b..66f3b7c413 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -1395,7 +1395,7 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult // fn with expanded modifiers: async, const, unsafe, default, extern (ABI optional) / // 拡張修飾子: async, const, unsafe, default, extern(ABI は省略可) new("function", new Regex(@"^\s*(?:(?pub(?:\([^)]*\))?)\s+)?(?:(?:async|const|unsafe|default|extern(?:\s+""[^""]+"")?)\s+)*fn\s+(?(?:r#)?\w+)", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), - new("class", new Regex(@"^\s*(?unsafe)\s*\{", RegexOptions.Compiled), BodyStyle.Brace), + new("class", new Regex(@"\b(?unsafe)\s*\{", RegexOptions.Compiled), BodyStyle.Brace), new("struct", new Regex(@"^\s*(?:(?pub(?:\([^)]*\))?)\s+)?(?:struct|union)\s+(?(?:r#)?\w+)", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), new("enum", new Regex(@"^\s*(?:(?pub(?:\([^)]*\))?)\s+)?enum\s+(?(?:r#)?\w+)", RegexOptions.Compiled), BodyStyle.Brace, "visibility"), // Enum variants / `Red`, `Ok(T)`, `Circle { radius: f64 }`, `Point` diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index aa5dfdfbf5..100f38231c 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -14570,6 +14570,25 @@ fn demo() { Assert.Equal(4, unsafeBlock.BodyEndLine); } + [Fact] + public void Extract_Rust_DetectsUnsafeBlockInExpression() + { + const string content = """ + fn demo() { + let value = unsafe { + read_raw() + }; + } + """; + + var symbols = SymbolExtractor.Extract(1, "rust", content); + + var unsafeBlock = Assert.Single(symbols, s => s.Kind == "class" && s.Name == "unsafe"); + Assert.Equal(2, unsafeBlock.Line); + Assert.Equal(2, unsafeBlock.BodyStartLine); + Assert.Equal(4, unsafeBlock.BodyEndLine); + } + [Fact] public void Extract_Rust_DetectsPubUseStatements() {