Skip to content

Commit b99f4ea

Browse files
committed
Handle Perl braced hex constants (#2005)
1 parent 211ce3a commit b99f4ea

2 files changed

Lines changed: 75 additions & 11 deletions

File tree

src/CodeIndex/Indexer/Symbols/SymbolExtractor.Perl.cs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static partial class SymbolExtractor
1313
@"^\s*use\s+constant\s+\{",
1414
RegexOptions.Compiled | RegexOptions.CultureInvariant);
1515
private static readonly Regex PerlHashConstantKeyRegex = new(
16-
@"(?:^|,)\s*(?:""(?<quoted>(?:\\x[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}|\\.|[^""])*)""|'(?<quoted>(?:\\x[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}|\\.|[^'])*)'|(?<bare>[\p{L}_][\p{L}\p{Nd}_]*))\s*=>",
16+
@"(?:^|,)\s*(?:""(?<quoted>(?:\\x\{[0-9A-Fa-f]+\}|\\x[0-9A-Fa-f]{2}|\\.|[^""])*)""|'(?<quoted>(?:\\x\{[0-9A-Fa-f]+\}|\\x[0-9A-Fa-f]{2}|\\.|[^'])*)'|(?<bare>[\p{L}_][\p{L}\p{Nd}_]*))\s*=>",
1717
RegexOptions.Compiled | RegexOptions.CultureInvariant);
1818

1919
private static void ExtractPerlHashConstantSymbols(long fileId, string[] lines, List<SymbolRecord> symbols)
@@ -75,17 +75,22 @@ private static string DecodePerlQuotedConstantEscapes(string value)
7575
}
7676

7777
var marker = value[i + 1];
78-
if (marker == 'x' && i + 3 < value.Length && TryParseHexScalar(value.AsSpan(i + 2, 2), out var hexByte))
78+
if (marker == 'x'
79+
&& i + 3 < value.Length
80+
&& value[i + 2] == '{'
81+
&& TryFindPerlBracedHexEscapeEnd(value, i + 3, out var escapeEnd)
82+
&& TryParseHexScalar(value.AsSpan(i + 3, escapeEnd - (i + 3)), out var scalar)
83+
&& scalar <= 0x10FFFF)
7984
{
80-
builder.Append((char)hexByte);
81-
i += 3;
85+
builder.Append(char.ConvertFromUtf32(scalar));
86+
i = escapeEnd;
8287
continue;
8388
}
8489

85-
if (marker == 'u' && i + 5 < value.Length && TryParseHexScalar(value.AsSpan(i + 2, 4), out var unicodeScalar))
90+
if (marker == 'x' && i + 3 < value.Length && TryParseHexScalar(value.AsSpan(i + 2, 2), out var hexByte))
8691
{
87-
builder.Append(char.ConvertFromUtf32(unicodeScalar));
88-
i += 5;
92+
builder.Append((char)hexByte);
93+
i += 3;
8994
continue;
9095
}
9196

@@ -99,6 +104,21 @@ private static string DecodePerlQuotedConstantEscapes(string value)
99104
private static bool TryParseHexScalar(ReadOnlySpan<char> value, out int scalar)
100105
=> int.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out scalar);
101106

107+
private static bool TryFindPerlBracedHexEscapeEnd(string value, int start, out int end)
108+
{
109+
for (var i = start; i < value.Length; i++)
110+
{
111+
if (value[i] == '}')
112+
{
113+
end = i;
114+
return i > start;
115+
}
116+
}
117+
118+
end = -1;
119+
return false;
120+
}
121+
102122
private readonly record struct PerlBodyLineSegment(int BodyStartIndex, int LineIndex, int ColumnOffset);
103123

104124
private static bool TryCollectPerlHashConstantBody(
@@ -124,11 +144,13 @@ private static bool TryCollectPerlHashConstantBody(
124144
return false;
125145

126146
var builder = new System.Text.StringBuilder();
147+
var inQuotedKey = false;
148+
var quotedKeyDelimiter = '\0';
127149
for (var lineIndex = startLineIndex; lineIndex < lines.Length; lineIndex++)
128150
{
129151
var line = lines[lineIndex];
130152
var segmentStart = lineIndex == startLineIndex ? openBraceIndex + 1 : 0;
131-
var closeBraceIndex = line.IndexOf('}', segmentStart);
153+
var closeBraceIndex = FindPerlHashConstantBlockCloseBrace(line, segmentStart, ref inQuotedKey, ref quotedKeyDelimiter);
132154
var segmentEnd = closeBraceIndex >= 0 ? closeBraceIndex : line.Length;
133155
if (segmentEnd > segmentStart)
134156
{
@@ -149,6 +171,45 @@ private static bool TryCollectPerlHashConstantBody(
149171
return false;
150172
}
151173

174+
private static int FindPerlHashConstantBlockCloseBrace(
175+
string line,
176+
int start,
177+
ref bool inQuotedKey,
178+
ref char quotedKeyDelimiter)
179+
{
180+
for (var i = start; i < line.Length; i++)
181+
{
182+
if (inQuotedKey)
183+
{
184+
if (line[i] == '\\' && i + 1 < line.Length)
185+
{
186+
i++;
187+
continue;
188+
}
189+
190+
if (line[i] == quotedKeyDelimiter)
191+
{
192+
inQuotedKey = false;
193+
quotedKeyDelimiter = '\0';
194+
}
195+
196+
continue;
197+
}
198+
199+
if (line[i] == '"' || line[i] == '\'')
200+
{
201+
inQuotedKey = true;
202+
quotedKeyDelimiter = line[i];
203+
continue;
204+
}
205+
206+
if (line[i] == '}')
207+
return i;
208+
}
209+
210+
return -1;
211+
}
212+
152213
private static (int LineIndex, int Column) ResolvePerlHashConstantBodyPosition(
153214
IReadOnlyList<PerlBodyLineSegment> lineSegments,
154215
int bodyIndex)

tests/CodeIndex.Tests/SymbolExtractorTests.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25305,19 +25305,22 @@ use constant {
2530525305
"foo " => 1,
2530625306
foo => 2,
2530725307
"naïve" => 3,
25308-
"nai\u0308ve" => 4,
25308+
"naïve" => 4,
2530925309
"hex\xEF" => 5,
2531025310
"hexï" => 6,
25311-
" " => 7,
25311+
"braced\x{00EF}" => 7,
25312+
"bracedï" => 8,
25313+
" " => 9,
2531225314
};
2531325315
""";
2531425316

2531525317
var symbols = SymbolExtractor.Extract(1, "perl", content);
2531625318

25317-
Assert.Equal(3, symbols.Count(symbol => symbol.Kind == "function"));
25319+
Assert.Equal(4, symbols.Count(symbol => symbol.Kind == "function"));
2531825320
Assert.Single(symbols.Where(symbol => symbol.Kind == "function" && symbol.Name == "foo"));
2531925321
Assert.Single(symbols.Where(symbol => symbol.Kind == "function" && symbol.Name == "naïve"));
2532025322
Assert.Single(symbols.Where(symbol => symbol.Kind == "function" && symbol.Name == "hexï"));
25323+
Assert.Single(symbols.Where(symbol => symbol.Kind == "function" && symbol.Name == "bracedï"));
2532125324
Assert.DoesNotContain(symbols, symbol => symbol.Name == "foo ");
2532225325
}
2532325326

0 commit comments

Comments
 (0)