test(webui): harden extractFunc to skip braces in string/regex/comment literals
Address greptile P2 on the vscroll recycled-anchor test harness: the
brace-counting function extractor desynced on a bare { or } inside a string,
template, regex, or comment within the extracted function body, silently
truncating the extract. Skip those spans so brace matching runs on real code
structure only, and add a regression test with a function whose string/regex/
comment literals contain unbalanced-looking braces.
Verified: the naive extractor fails all four tests (the compensation function
now contains querySelector attribute-selector strings that desync it); the
hardened extractor passes.
This commit is contained in:
1 parent
ad1843f31b
commit
7811b13f49
1 file changed
+80
-10
@@ -52,21 +52,55 @@ def _run_node(source: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _extract_func_script(js: str) -> str:
|
def _extract_func_script(js: str) -> str:
|
||||||
return f"""
|
# extractFunc brace-matches the function body but SKIPS braces that live
|
||||||
const src = {js!r};
|
# inside string / template / regex literals and comments, so a future edit
|
||||||
function extractFunc(name) {{
|
# adding e.g. `warn('expected {k}')` inside an extracted function cannot
|
||||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
# desync the depth counter (greptile P2 on this PR). Built with a plain
|
||||||
|
# string (not an f-string) so the JS braces need no doubling.
|
||||||
|
prelude = "const src = " + json.dumps(js) + ";\n"
|
||||||
|
body = r"""
|
||||||
|
function extractFunc(name) {
|
||||||
|
const re = new RegExp('function\\s+' + name + '\\s*\\(');
|
||||||
const start = src.search(re);
|
const start = src.search(re);
|
||||||
if (start < 0) throw new Error(name + ' not found');
|
if (start < 0) throw new Error(name + ' not found');
|
||||||
let i = src.indexOf('{{', start);
|
let i = src.indexOf('{', start);
|
||||||
let depth = 1; i++;
|
let depth = 1; i++;
|
||||||
while (depth > 0 && i < src.length) {{
|
let str = null; // current string/template delimiter, or null
|
||||||
if (src[i] === '{{') depth++;
|
let inLine = false; // inside // line comment
|
||||||
else if (src[i] === '}}') depth--;
|
let inBlock = false; // inside /* block comment */
|
||||||
|
let inRegex = false; // inside / regex literal /
|
||||||
|
let prev = ''; // last significant (non-space) code char, for regex detection
|
||||||
|
while (depth > 0 && i < src.length) {
|
||||||
|
const c = src[i];
|
||||||
|
const n = src[i + 1];
|
||||||
|
if (inLine) { if (c === '\n') inLine = false; i++; continue; }
|
||||||
|
if (inBlock) { if (c === '*' && n === '/') { inBlock = false; i++; } i++; continue; }
|
||||||
|
if (str) {
|
||||||
|
if (c === '\\') { i += 2; continue; }
|
||||||
|
if (c === str) str = null;
|
||||||
|
i++; continue;
|
||||||
|
}
|
||||||
|
if (inRegex) {
|
||||||
|
if (c === '\\') { i += 2; continue; }
|
||||||
|
if (c === '/') inRegex = false;
|
||||||
|
i++; continue;
|
||||||
|
}
|
||||||
|
if (c === '/' && n === '/') { inLine = true; i += 2; continue; }
|
||||||
|
if (c === '/' && n === '*') { inBlock = true; i += 2; continue; }
|
||||||
|
if (c === '"' || c === "'" || c === '`') { str = c; i++; continue; }
|
||||||
|
// A '/' starts a regex only where a value is expected, i.e. after an
|
||||||
|
// operator/paren/comma — not after an identifier/number/closing paren.
|
||||||
|
if (c === '/' && !'})]0123456789'.includes(prev) && !/[A-Za-z_$]/.test(prev)) {
|
||||||
|
inRegex = true; i++; continue;
|
||||||
|
}
|
||||||
|
if (c === '{') depth++;
|
||||||
|
else if (c === '}') depth--;
|
||||||
|
if (c.trim()) prev = c;
|
||||||
i++;
|
i++;
|
||||||
}}
|
}
|
||||||
return src.slice(start, i);
|
return src.slice(start, i);
|
||||||
}}"""
|
}"""
|
||||||
|
return prelude + body
|
||||||
|
|
||||||
|
|
||||||
def test_compensate_recovers_via_session_idx_when_rawidx_row_recycled():
|
def test_compensate_recovers_via_session_idx_when_rawidx_row_recycled():
|
||||||
@@ -209,3 +243,39 @@ console.log(JSON.stringify({scrollTopMutated}));
|
|||||||
"with no topPadBefore captured and no anchor row, the fallback must "
|
"with no topPadBefore captured and no anchor row, the fallback must "
|
||||||
"leave scrollTop untouched (NaN-guarded), not throw or write garbage"
|
"leave scrollTop untouched (NaN-guarded), not throw or write garbage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extract_func_skips_braces_inside_string_and_regex_literals():
|
||||||
|
"""The Node-harness extractFunc must brace-match on real code structure only,
|
||||||
|
skipping braces inside string / template / regex literals and comments. A
|
||||||
|
naive depth counter desyncs on a bare '{' or '}' inside a string literal and
|
||||||
|
truncates the extract (greptile P2). This locks the robust behavior."""
|
||||||
|
tricky = (
|
||||||
|
"function _tricky(){\n"
|
||||||
|
" const a = 'has a bare } brace';\n"
|
||||||
|
" const b = \"and an open { one\";\n"
|
||||||
|
" const c = `template ${'x'} literal`;\n"
|
||||||
|
" const re = /\\}[{]/; // regex with unbalanced-looking braces\n"
|
||||||
|
" // a line comment with } and {\n"
|
||||||
|
" /* block comment with { and } */\n"
|
||||||
|
" return a.length + b.length + c.length + (re ? 1 : 0);\n"
|
||||||
|
"}\n"
|
||||||
|
)
|
||||||
|
source = _extract_func_script(tricky) + """
|
||||||
|
const extracted = extractFunc('_tricky');
|
||||||
|
// The extract must end at the REAL closing brace (full function), not a premature
|
||||||
|
// one desynced by a string/regex brace. Eval it and call it to prove it is whole.
|
||||||
|
eval(extracted);
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
endsAtRealBrace: extracted.trimEnd().endsWith('}'),
|
||||||
|
hasReturn: extracted.indexOf('return a.length') !== -1,
|
||||||
|
callable: typeof _tricky === 'function',
|
||||||
|
result: _tricky(),
|
||||||
|
}));
|
||||||
|
"""
|
||||||
|
metrics = json.loads(_run_node(source))
|
||||||
|
assert metrics["hasReturn"] is True, "extract truncated before the return statement"
|
||||||
|
assert metrics["endsAtRealBrace"] is True
|
||||||
|
assert metrics["callable"] is True
|
||||||
|
# 'has a bare } brace'(18) + 'and an open { one'(17) + 'template x literal'(18) + 1
|
||||||
|
assert metrics["result"] == 54
|
||||||
Reference in new issue
Block a user