* Checks if we're currently inside a ParenExpr by looking backwards in the input * to count unmatched opening parens that likely started a ParenExpr. * * We only consider a '(' as a ParenExpr start if it's preceded by whitespace or * start-of-input, since "test()" has a '(' that's part of a word
(input: InputStream)
| 343 | * Handles escaped characters (backslash followed by any character). |
| 344 | */ |
| 345 | function hasUnmatchedOpenParen(input: InputStream): boolean { |
| 346 | // Count parens backwards from current position |
| 347 | let depth = 0; |
| 348 | let offset = -1; |
| 349 | |
| 350 | // Look backwards up to 1000 characters (reasonable limit) |
| 351 | while (offset >= -1000) { |
| 352 | const ch = input.peek(offset); |
| 353 | if (ch === EOF) { |
| 354 | // Reached start of input - if we have negative depth, there's an unmatched '(' |
| 355 | return depth < 0; |
| 356 | } |
| 357 | |
| 358 | // Check if this character is escaped (preceded by backslash) |
| 359 | // Note: we need to be careful about escaped backslashes (\\) |
| 360 | // For simplicity, if we see a backslash immediately before, skip this char |
| 361 | const prevCh = input.peek(offset - 1); |
| 362 | if (prevCh === 92 /* backslash */) { |
| 363 | // Check if the backslash itself is escaped |
| 364 | const prevPrevCh = input.peek(offset - 2); |
| 365 | if (prevPrevCh !== 92) { |
| 366 | // Single backslash - this char is escaped, skip it |
| 367 | offset--; |
| 368 | continue; |
| 369 | } |
| 370 | // Double backslash - the backslash is escaped, so current char is not |
| 371 | } |
| 372 | |
| 373 | if (ch === CLOSE_PAREN) { |
| 374 | depth++; |
| 375 | } else if (ch === OPEN_PAREN) { |
| 376 | // Check what's before this '(' |
| 377 | const beforeParen = input.peek(offset - 1); |
| 378 | |
| 379 | // A '(' starts a ParenExpr if it's preceded by: |
| 380 | // - EOF or whitespace (e.g., "(hello)" or "test (hello)") |
| 381 | // - '-' for negation (e.g., "-(hello)") |
| 382 | // - ':' for prefix values (e.g., "repo:(foo or bar)") |
| 383 | const isDefinitelyParenExprStart = |
| 384 | beforeParen === EOF || |
| 385 | isWhitespace(beforeParen) || |
| 386 | beforeParen === DASH || |
| 387 | beforeParen === COLON; |
| 388 | |
| 389 | // Special case: '(' preceded by '(' could be nested ParenExprs like "((hello))" |
| 390 | // BUT it could also be part of a word like "test((nested))" |
| 391 | // To distinguish: if prev is '(', check what's before THAT '(' |
| 392 | let isParenExprStart = isDefinitelyParenExprStart; |
| 393 | if (!isParenExprStart && beforeParen === OPEN_PAREN) { |
| 394 | // Check what's before the previous '(' |
| 395 | const prevPrevCh = input.peek(offset - 2); |
| 396 | // Only count as ParenExpr if the preceding '(' is also at a token boundary |
| 397 | isParenExprStart = |
| 398 | prevPrevCh === EOF || |
| 399 | isWhitespace(prevPrevCh) || |
| 400 | prevPrevCh === DASH || |
| 401 | prevPrevCh === COLON; |
| 402 | } |
no test coverage detected