Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changelog.d/unreleased/1441.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 1441
affected:
- src/CodeIndex/Indexer/References/ReferenceExtractor.cs
- src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs
- tests/CodeIndex.Tests/ReferenceExtractorTests.cs
---

## English

- **Rust raw-string attributes no longer emit phantom imports (#1441)** — Rust reference extraction now masks attribute bodies before scanning `use` statements, so `#[doc = r"use foo::bar;"]` no longer creates a false import/reference.

## 日本語

- **Rust の raw string 属性から phantom import が生成されないようになりました (#1441)** — Rust の参照抽出は `use` 文スキャン前に属性本文をマスクするため、`#[doc = r"use foo::bar;"]` が誤った import/reference を生成しなくなりました。
124 changes: 124 additions & 0 deletions src/CodeIndex/Indexer/References/Languages/RustReferenceExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,130 @@ internal static class RustReferenceExtractor
@"(?<![\w$])(?<name>(?:(?:r#)?\w+::)*r#\w+(?:::(?:r#)?\w+)*)(?:<[^>\n]+>)?\s*\(",
RegexOptions.Compiled);

public static string MaskAttributeBodies(string line)
{
var masked = default(char[]);
for (var index = 0; index < line.Length; index++)
{
if (line[index] != '#')
continue;

var cursor = index + 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++;
}

if (cursor >= line.Length || line[cursor] != '[')
continue;

masked ??= line.ToCharArray();
var end = FindAttributeEnd(line, cursor);
ReplaceWithSpaces(masked, index, end > cursor ? end - index + 1 : line.Length - index);
index = end > cursor ? end : line.Length;
}

return masked == null ? line : new string(masked);
}

private static int FindAttributeEnd(string line, int openBracket)
{
var depth = 0;
for (var index = openBracket; index < line.Length; index++)
{
var c = line[index];
if (c == 'r')
{
var rawEnd = TrySkipRawString(line, index);
if (rawEnd > index)
{
index = rawEnd;
continue;
}
}

if (c == '"' || c == '\'')
{
index = SkipQuotedString(line, index, c);
continue;
}

if (c == '[')
{
depth++;
continue;
}

if (c != ']')
continue;

depth--;
if (depth == 0)
return index;
}

return -1;
}

private static int TrySkipRawString(string line, int start)
{
var cursor = start + 1;
while (cursor < line.Length && line[cursor] == '#')
cursor++;
if (cursor >= line.Length || line[cursor] != '"')
return -1;

var hashCount = cursor - start - 1;
for (var index = cursor + 1; index < line.Length; index++)
{
if (line[index] == '"' && HasHashRun(line, index + 1, hashCount))
return index + hashCount;
}

return line.Length - 1;
}

private static bool HasHashRun(string line, int start, int hashCount)
{
if (start + hashCount > line.Length)
return false;
for (var offset = 0; offset < hashCount; offset++)
{
if (line[start + offset] != '#')
return false;
}

return true;
}

private static int SkipQuotedString(string line, int start, char quote)
{
for (var index = start + 1; index < line.Length; index++)
{
if (line[index] == '\\')
{
index++;
continue;
}

if (line[index] == quote)
return index;
}

return line.Length - 1;
}

private static void ReplaceWithSpaces(char[] chars, int start, int length)
{
var end = Math.Min(chars.Length, start + length);
for (var index = start; index < end; index++)
chars[index] = ' ';
}

public static void EmitAdditionalCallReferences(string preparedLine, Action<string, int> addCallLikeReference)
{
foreach (Match match in RawIdentifierCallRegex.Matches(preparedLine))
Expand Down
3 changes: 2 additions & 1 deletion src/CodeIndex/Indexer/References/ReferenceExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1976,8 +1976,9 @@ bool ShouldSuppressDefinitionCall(string resolvedName, int callIndex)
var rustEnumContainer = rustEnumCandidates != null
? FindInnermostContainer(rustEnumCandidates, lineNumber)
: null;
var rustTypePositionLine = RustReferenceExtractor.MaskAttributeBodies(preparedLine);
RustReferenceExtractor.EmitTypePositionReferences(
preparedLine,
rustTypePositionLine,
references,
seen,
fileId,
Expand Down
22 changes: 22 additions & 0 deletions tests/CodeIndex.Tests/ReferenceExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17995,6 +17995,28 @@ public void Extract_RustRawString_DoesNotLeakPhantomCallReferences()
Assert.Contains(references, r => r.SymbolName == "real_call" && r.ContainerName == "caller");
}

[Fact]
public void Extract_RustAttributeRawString_DoesNotLeakPhantomUseReferences()
{
const string content = """
#[doc = r"use baz::qux;"]
pub fn f() {
real_call();
}

use crate::actual::Thing;

fn real_call() {}
""";

var symbols = SymbolExtractor.Extract(1, "rust", content);
var references = ReferenceExtractor.Extract(1, "rust", content, symbols);

Assert.DoesNotContain(references, r => r.SymbolName == "qux");
Assert.Contains(references, r => r.SymbolName == "Thing" && r.ReferenceKind == "reference");
Assert.Contains(references, r => r.SymbolName == "real_call" && r.ContainerName == "f");
}

[Fact]
public void Extract_JsTemplateLiteral_DoesNotLeakPhantomCallsButKeepsInterpolationCalls()
{
Expand Down
Loading