perf(csharp): resume past the \$/@ prefix run so malformed files stay linear (#3376)

(cherry picked from commit 7f0fd6ddfe)
This commit is contained in:
rohit-jsfreaky authored and safishamsi committed 2026-10-12 22:37:55 +01:00
1 parent 709bba316b
commit 1704777e39
2 files changed
+37 -8

No files matched your search

+15 -7
View File
@@ -3167,8 +3167,14 @@ def extract_ruby(path: Path) -> dict:
return _extract_generic(path, _RUBY_CONFIG)
def _csharp_string_opener(source: bytes, i: int) -> "tuple[int, int, bool, bool] | None":
"""If a string literal starts at ``i``, return ``(quote_pos, quote_run, interpolated, verbatim)``.
def _csharp_string_opener(source: bytes, i: int) -> "tuple[int, int, bool, bool]":
"""Read a possible string-literal opener at ``i``.
Returns ``(prefix_end, quote_run, interpolated, verbatim)``. ``quote_run`` is
0 when no string starts here, and ``prefix_end`` is always where the run of
``$``/``@`` characters stopped — the caller needs that even on a miss, so it
can resume past the run instead of re-walking it from the next byte (a long
run in malformed source would otherwise cost O(n^2)).
C# lets the ``$`` and ``@`` prefixes appear in either order and repeat, and
a raw string literal opens with a run of three or more quotes, so the prefix
@@ -3187,7 +3193,7 @@ def _csharp_string_opener(source: bytes, i: int) -> "tuple[int, int, bool, bool]
break
j += 1
if source[j:j + 1] != b'"':
return None
return j, 0, interpolated, verbatim
run = 0
while source[j + run:j + run + 1] == b'"':
run += 1
@@ -3229,11 +3235,13 @@ def _csharp_broken_escape_offsets(source: bytes) -> list[int]:
j += 1
i = j + 1
continue
opener = _csharp_string_opener(source, i)
if opener is None:
i += 1
quote, run, interpolated, verbatim = _csharp_string_opener(source, i)
if run == 0:
# No string opens here. Resume at the end of the `$`/`@` run rather
# than at i + 1: re-walking a long run from every offset is what
# makes a malformed file quadratic.
i = quote if quote > i else i + 1
continue
quote, run, interpolated, verbatim = opener
if run >= 3:
# Raw string literal: closed by a quote run of the same length.
end = source.find(b'"' * run, quote + run)
+22 -1
View File
@@ -16,7 +16,11 @@ from __future__ import annotations
import os
from pathlib import Path
from graphify.extract import _normalize_csharp_verbatim_interpolation, extract
from graphify.extract import (
_csharp_broken_escape_offsets,
_normalize_csharp_verbatim_interpolation,
extract,
)
# The offending literal, kept in one place: `}` then an escaped quote then the
# terminator. Written as a plain string so the C# quoting stays readable.
@@ -164,3 +168,20 @@ def test_two_offending_literals_in_one_file(tmp_path):
labels = {str(n.get("label", "")).lstrip(".") for n in result["nodes"]}
for member in ("First()", "Between()", "Second()", "Last()"):
assert member in labels, f"{member} missing from {sorted(labels)}"
def test_long_prefix_run_does_not_blow_up(tmp_path):
"""A malformed file is the normal input here -- the scan only runs on files
the grammar already rejected -- so a long run of `$`/`@` must stay linear.
Resuming one byte past the start of such a run re-walks it from every offset:
4k characters took 0.8s that way, and the cost quadruples each doubling.
"""
import time
src = b"class C { " + (b"$" * 1_000_000) + b" }"
started = time.perf_counter()
assert _csharp_broken_escape_offsets(src) == []
elapsed = time.perf_counter() - started
# Linear finishes in well under a second; the quadratic form needs hours.
assert elapsed < 5.0, f"prefix scan took {elapsed:.1f}s -- quadratic again?"