From 1704777e39ae5c6149ec4eab89cdfe814cf30af5 Mon Sep 17 00:00:00 2001 From: rohit-jsfreaky Date: Wed, 16 Sep 2026 23:21:13 +0530 Subject: [PATCH] perf(csharp): resume past the \$/@ prefix run so malformed files stay linear (#3376) (cherry picked from commit 7f0fd6ddfed927abec1de5cc9b7a48241ef845d0) --- graphify/extract.py | 22 +++++++++++++------- tests/test_csharp_verbatim_interpolation.py | 23 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/graphify/extract.py b/graphify/extract.py index 045d9dded..ce96c7ce0 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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) diff --git a/tests/test_csharp_verbatim_interpolation.py b/tests/test_csharp_verbatim_interpolation.py index fc04882cd..5f0061785 100644 --- a/tests/test_csharp_verbatim_interpolation.py +++ b/tests/test_csharp_verbatim_interpolation.py @@ -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?"