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/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 3de06de5f3..c7875fe19c 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\b", + 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!` @@ -56,6 +59,156 @@ 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; + + EmitMultilineDeriveTypeList( + attribute, + openParen + 1, + closeParen, + startLineIndex, + startColumn, + references, + seen, + fileId, + resolveContainer); + } + + 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 +388,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 +418,7 @@ private static void EmitDeriveTypeList( TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( fragment.Substring(typeStart, typeEnd - typeStart), - typesGroup.Index + segmentStart + typeStart, + typesStartIndex + segmentStart + typeStart, "rust", references, seen, @@ -257,6 +429,114 @@ 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; + 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, @@ -289,9 +569,58 @@ 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)) + { + if (!IsMutableReferenceTypeContext(preparedLine, match.Index)) + continue; + + var typeStart = TypedLanguageReferenceExtractor.SkipTypePrefixTrivia(preparedLine, match.Index + match.Length); + var typeEnd = TypedLanguageReferenceExtractor.FindTypeExpressionEnd(preparedLine, typeStart); + if (typeEnd <= typeStart) + continue; + + TypedLanguageReferenceExtractor.EmitTypeExpressionReferences( + preparedLine.Substring(typeStart, typeEnd - typeStart), + typeStart, + "rust", + references, + seen, + fileId, + context, + lineNumber, + resolveContainerForColumn(typeStart)); + } + } + + 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/src/CodeIndex/Indexer/References/ReferenceExtractor.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs index 4e6b7ebd28..41ae9b1f99 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.cs @@ -1144,6 +1144,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/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index c4b0bc3694..66f3b7c413 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -1395,6 +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(@"\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/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 960dbae9f0..12f929d97f 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -31260,6 +31260,33 @@ 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.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"); + } + [Fact] public void Extract_RustAttributes_CaptureAnnotationReferences() { @@ -31315,6 +31342,57 @@ 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 == "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_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() { diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 205c028c0f..100f38231c 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -14551,6 +14551,44 @@ 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_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() {