Improve Recovery of Unterminated Regular Expressions (#58289)

Co-authored-by: Ron Buckton <ron.buckton@microsoft.com>
This commit is contained in:
graphemecluster
2024-05-25 03:46:26 +08:00
committed by GitHub
parent 842cf177db
commit d0ef028841
20 changed files with 223 additions and 125 deletions
+1 -1
View File
@@ -31974,7 +31974,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
scanner.setScriptTarget(sourceFile.languageVersion);
scanner.setLanguageVariant(sourceFile.languageVariant);
scanner.setOnError((message, length, arg0) => {
// emulate `parseErrorAtPosition` from parser.ts
// For providing spelling suggestions
const start = scanner!.getTokenEnd();
if (message.category === DiagnosticCategory.Message && lastError && start === lastError.start && length === lastError.length) {
const error = createDetachedDiagnostic(sourceFile.fileName, sourceFile.text, start, length, message, arg0);
+1 -6
View File
@@ -62,7 +62,6 @@ import {
DeleteExpression,
Diagnostic,
DiagnosticArguments,
DiagnosticCategory,
DiagnosticMessage,
Diagnostics,
DiagnosticWithDetachedLocation,
@@ -2144,11 +2143,7 @@ namespace Parser {
// Don't report another error if it would just be at the same position as the last error.
const lastError = lastOrUndefined(parseDiagnostics);
let result: DiagnosticWithDetachedLocation | undefined;
if (message.category === DiagnosticCategory.Message && lastError && start === lastError.start && length === lastError.length) {
result = createDetachedDiagnostic(fileName, sourceText, start, length, message, ...args);
addRelatedInfo(lastError, result);
}
else if (!lastError || start !== lastError.start) {
if (!lastError || start !== lastError.start) {
result = createDetachedDiagnostic(fileName, sourceText, start, length, message, ...args);
parseDiagnostics.push(result);
}
+101 -71
View File
@@ -1613,7 +1613,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
isRegularExpression && shouldEmitInvalidEscapeError && escapedValue >= 0xD800 && escapedValue <= 0xDBFF &&
pos + 6 < end && text.substring(pos, pos + 2) === "\\u" && charCodeUnchecked(pos + 2) !== CharacterCodes.openBrace
) {
// For regular expressions in Unicode mode, \u HexLeadSurrogate \u HexTrailSurrogate is treated as a single character
// For regular expressions in any Unicode mode, \u HexLeadSurrogate \u HexTrailSurrogate is treated as a single character
// for the purpose of determining whether a character class range is out of order
// https://tc39.es/ecma262/#prod-RegExpUnicodeEscapeSequence
const nextStart = pos;
@@ -2424,10 +2424,11 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
function reScanSlashToken(reportErrors?: boolean): SyntaxKind {
if (token === SyntaxKind.SlashToken || token === SyntaxKind.SlashEqualsToken) {
// Quickly get to the end of regex such that we know the flags
let p = tokenStart + 1;
const startOfRegExpBody = tokenStart + 1;
pos = startOfRegExpBody;
let inEscape = false;
// Although nested character classes are allowed in Unicode Sets mode,
// an unescaped slash is nevertheless invalid even in a character class in Unicode mode.
// an unescaped slash is nevertheless invalid even in a character class in any Unicode mode.
// Additionally, parsing nested character classes will misinterpret regexes like `/[[]/`
// as unterminated, consuming characters beyond the slash. (This even applies to `/[[]/v`,
// which should be parsed as a well-terminated regex with an incomplete character class.)
@@ -2436,16 +2437,9 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
while (true) {
// If we reach the end of a file, or hit a newline, then this is an unterminated
// regex. Report error and return what we have so far.
if (p >= end) {
const ch = charCodeChecked(pos);
if (ch === CharacterCodes.EOF || isLineBreak(ch)) {
tokenFlags |= TokenFlags.Unterminated;
error(Diagnostics.Unterminated_regular_expression_literal);
break;
}
const ch = charCodeUnchecked(p);
if (isLineBreak(ch)) {
tokenFlags |= TokenFlags.Unterminated;
error(Diagnostics.Unterminated_regular_expression_literal);
break;
}
@@ -2457,7 +2451,6 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
else if (ch === CharacterCodes.slash && !inCharacterClass) {
// A slash within a character class is permissible,
// but in general it signals the end of the regexp literal.
p++;
break;
}
else if (ch === CharacterCodes.openBracket) {
@@ -2469,47 +2462,89 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
else if (ch === CharacterCodes.closeBracket) {
inCharacterClass = false;
}
p++;
pos++;
}
const isUnterminated = !!(tokenFlags & TokenFlags.Unterminated);
const endOfBody = p - (isUnterminated ? 0 : 1);
let regExpFlags = RegularExpressionFlags.None;
while (p < end) {
const ch = charCodeUnchecked(p);
if (!isIdentifierPart(ch, languageVersion)) {
break;
const endOfRegExpBody = pos;
if (tokenFlags & TokenFlags.Unterminated) {
// Search for the nearest unbalanced bracket for better recovery. Since the expression is
// invalid anyways, we take nested square brackets into consideration for the best guess.
pos = startOfRegExpBody;
inEscape = false;
let characterClassDepth = 0;
let inDecimalQuantifier = false;
let groupDepth = 0;
while (pos < endOfRegExpBody) {
const ch = charCodeUnchecked(pos);
if (inEscape) {
inEscape = false;
}
else if (ch === CharacterCodes.backslash) {
inEscape = true;
}
else if (ch === CharacterCodes.openBracket) {
characterClassDepth++;
}
else if (ch === CharacterCodes.closeBracket && characterClassDepth) {
characterClassDepth--;
}
else if (!characterClassDepth) {
if (ch === CharacterCodes.openBrace) {
inDecimalQuantifier = true;
}
else if (ch === CharacterCodes.closeBrace && inDecimalQuantifier) {
inDecimalQuantifier = false;
}
else if (!inDecimalQuantifier) {
if (ch === CharacterCodes.openParen) {
groupDepth++;
}
else if (ch === CharacterCodes.closeParen && groupDepth) {
groupDepth--;
}
else if (ch === CharacterCodes.closeParen || ch === CharacterCodes.closeBracket || ch === CharacterCodes.closeBrace) {
// We encountered an unbalanced bracket outside a character class. Treat this position as the end of regex.
break;
}
}
}
pos++;
}
// Whitespaces and semicolons at the end are not likely to be part of the regex
while (isWhiteSpaceLike(charCodeChecked(pos - 1)) || charCodeChecked(pos - 1) === CharacterCodes.semicolon) pos--;
error(Diagnostics.Unterminated_regular_expression_literal, tokenStart, pos - tokenStart);
}
else {
// Consume the slash character
pos++;
let regExpFlags = RegularExpressionFlags.None;
while (true) {
const ch = codePointChecked(pos);
if (ch === CharacterCodes.EOF || !isIdentifierPart(ch, languageVersion)) {
break;
}
if (reportErrors) {
const flag = characterToRegularExpressionFlag(String.fromCharCode(ch));
if (flag === undefined) {
error(Diagnostics.Unknown_regular_expression_flag, pos, 1);
}
else if (regExpFlags & flag) {
error(Diagnostics.Duplicate_regular_expression_flag, pos, 1);
}
else if (((regExpFlags | flag) & RegularExpressionFlags.AnyUnicodeMode) === RegularExpressionFlags.AnyUnicodeMode) {
error(Diagnostics.The_Unicode_u_flag_and_the_Unicode_Sets_v_flag_cannot_be_set_simultaneously, pos, 1);
}
else {
regExpFlags |= flag;
checkRegularExpressionFlagAvailable(flag, pos);
}
}
pos++;
}
if (reportErrors) {
const flag = characterToRegularExpressionFlag(String.fromCharCode(ch));
if (flag === undefined) {
error(Diagnostics.Unknown_regular_expression_flag, p, 1);
}
else if (regExpFlags & flag) {
error(Diagnostics.Duplicate_regular_expression_flag, p, 1);
}
else if (((regExpFlags | flag) & RegularExpressionFlags.UnicodeMode) === RegularExpressionFlags.UnicodeMode) {
error(Diagnostics.The_Unicode_u_flag_and_the_Unicode_Sets_v_flag_cannot_be_set_simultaneously, p, 1);
}
else {
regExpFlags |= flag;
checkRegularExpressionFlagAvailable(flag, p);
}
scanRange(startOfRegExpBody, endOfRegExpBody - startOfRegExpBody, () => {
scanRegularExpressionWorker(regExpFlags, /*annexB*/ true);
});
}
p++;
}
pos = p;
if (reportErrors) {
const saveTokenStart = tokenStart;
const saveTokenFlags = tokenFlags;
const savePos = pos;
const saveEnd = end;
pos = tokenStart + 1;
end = endOfBody;
scanRegularExpressionWorker(regExpFlags, isUnterminated, /*annexB*/ true);
tokenStart = saveTokenStart;
tokenFlags = saveTokenFlags;
pos = savePos;
end = saveEnd;
}
tokenValue = text.substring(tokenStart, pos);
token = SyntaxKind.RegularExpressionLiteral;
@@ -2517,7 +2552,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
return token;
}
function scanRegularExpressionWorker(regExpFlags: RegularExpressionFlags, isUnterminated: boolean, annexB: boolean) {
function scanRegularExpressionWorker(regExpFlags: RegularExpressionFlags, annexB: boolean) {
// Why var? It avoids TDZ checks in the runtime which can be costly.
// See: https://github.com/microsoft/TypeScript/issues/52924
/* eslint-disable no-var */
@@ -2525,9 +2560,9 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
/** Grammar parameter */
var unicodeSetsMode = !!(regExpFlags & RegularExpressionFlags.UnicodeSets);
/** Grammar parameter */
var unicodeMode = !!(regExpFlags & RegularExpressionFlags.UnicodeMode);
var anyUnicodeMode = !!(regExpFlags & RegularExpressionFlags.AnyUnicodeMode);
if (unicodeMode) {
if (anyUnicodeMode) {
// Annex B treats any unicode mode as the strict syntax.
annexB = false;
}
@@ -2535,7 +2570,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
/** @see {scanClassSetExpression} */
var mayContainStrings = false;
/** The number of numeric (anonymous) capturing groups defined in the regex. */
/** The number of all (named and unnamed) capturing groups defined in the regex. */
var numberOfCapturingGroups = 0;
/** All named capturing groups defined in the regex. */
var groupSpecifiers: Set<string> | undefined;
@@ -2684,7 +2719,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
error(Diagnostics.Incomplete_quantifier_Digit_expected, digitsStart, 0);
}
else {
if (unicodeMode) {
if (anyUnicodeMode) {
error(Diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, start, 1, String.fromCharCode(ch));
}
isPreviousTermQuantifiable = true;
@@ -2696,7 +2731,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
}
}
else if (!min) {
if (unicodeMode) {
if (anyUnicodeMode) {
error(Diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, start, 1, String.fromCharCode(ch));
}
isPreviousTermQuantifiable = true;
@@ -2740,11 +2775,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
// falls through
case CharacterCodes.closeBracket:
case CharacterCodes.closeBrace:
if (isUnterminated && !isInGroup) {
// Assume what starting from the character to be outside of the regex
return;
}
if (unicodeMode || ch === CharacterCodes.closeParen) {
if (anyUnicodeMode || ch === CharacterCodes.closeParen) {
error(Diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, pos, 1, String.fromCharCode(ch));
}
pos++;
@@ -2801,7 +2832,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
scanGroupName(/*isReference*/ true);
scanExpectedChar(CharacterCodes.greaterThan);
}
else if (unicodeMode) {
else if (anyUnicodeMode) {
error(Diagnostics.k_must_be_followed_by_a_capturing_group_name_enclosed_in_angle_brackets, pos - 2, 2);
}
break;
@@ -2844,6 +2875,9 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.backslash);
let ch = charCodeChecked(pos);
switch (ch) {
case CharacterCodes.EOF:
error(Diagnostics.Undetermined_character_escape, pos - 1, 1);
return "\\";
case CharacterCodes.c:
pos++;
ch = charCodeChecked(pos);
@@ -2851,7 +2885,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
pos++;
return String.fromCharCode(ch & 0x1f);
}
if (unicodeMode) {
if (anyUnicodeMode) {
error(Diagnostics.c_must_be_followed_by_an_ASCII_letter, pos - 2, 2);
}
else if (atomEscape && annexB) {
@@ -2882,12 +2916,8 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
pos++;
return String.fromCharCode(ch);
default:
if (pos >= end) {
error(Diagnostics.Undetermined_character_escape, pos - 1, 1);
return "\\";
}
pos--;
return scanEscapeSequence(/*shouldEmitInvalidEscapeError*/ unicodeMode, /*isRegularExpression*/ annexB ? "annex-b" : true);
return scanEscapeSequence(/*shouldEmitInvalidEscapeError*/ anyUnicodeMode, /*isRegularExpression*/ annexB ? "annex-b" : true);
}
}
@@ -3433,11 +3463,11 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
}
}
scanExpectedChar(CharacterCodes.closeBrace);
if (!unicodeMode) {
if (!anyUnicodeMode) {
error(Diagnostics.Unicode_property_value_expressions_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set, start, pos - start);
}
}
else if (unicodeMode) {
else if (anyUnicodeMode) {
error(Diagnostics._0_must_be_followed_by_a_Unicode_property_value_expression_enclosed_in_braces, pos - 2, 2, String.fromCharCode(ch));
}
return true;
@@ -3459,7 +3489,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean
}
function scanSourceCharacter(): string {
const size = unicodeMode ? charSize(charCodeChecked(pos)) : 1;
const size = anyUnicodeMode ? charSize(charCodeChecked(pos)) : 1;
pos += size;
return size > 0 ? text.substring(pos - size, pos) : "";
}
+11 -11
View File
@@ -2765,17 +2765,17 @@ export interface RegularExpressionLiteral extends LiteralExpression {
// dprint-ignore
/** @internal */
export const enum RegularExpressionFlags {
None = 0,
HasIndices = 1 << 0, // d
Global = 1 << 1, // g
IgnoreCase = 1 << 2, // i
Multiline = 1 << 3, // m
DotAll = 1 << 4, // s
Unicode = 1 << 5, // u
UnicodeSets = 1 << 6, // v
Sticky = 1 << 7, // y
UnicodeMode = Unicode | UnicodeSets,
Modifiers = IgnoreCase | Multiline | DotAll,
None = 0,
HasIndices = 1 << 0, // d
Global = 1 << 1, // g
IgnoreCase = 1 << 2, // i
Multiline = 1 << 3, // m
DotAll = 1 << 4, // s
Unicode = 1 << 5, // u
UnicodeSets = 1 << 6, // v
Sticky = 1 << 7, // y
AnyUnicodeMode = Unicode | UnicodeSets,
Modifiers = IgnoreCase | Multiline | DotAll,
}
export interface NoSubstitutionTemplateLiteral extends LiteralExpression, TemplateLiteralLikeNode, Declaration {
+1
View File
@@ -47,6 +47,7 @@ export * from "./unittests/paths.js";
export * from "./unittests/printer.js";
export * from "./unittests/programApi.js";
export * from "./unittests/publicApi.js";
export * from "./unittests/regExpScannerRecovery.js";
export * from "./unittests/reuseProgramStructure.js";
export * from "./unittests/semver.js";
export * from "./unittests/services/cancellableLanguageServiceOperations.js";
@@ -160,7 +160,7 @@ describe("unittests:: incrementalParser::", () => {
const oldText = ts.ScriptSnapshot.fromString(source);
const newTextAndChange = withInsert(oldText, semicolonIndex, "/");
compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 0);
compareTrees(oldText, newTextAndChange.text, newTextAndChange.textChangeRange, 4);
});
it("Regular expression 2", () => {
@@ -0,0 +1,81 @@
import * as ts from "../_namespaces/ts.js";
describe("unittests:: regExpScannerRecovery", () => {
const testCases = [
"/",
"/[]",
"/{}",
"/()",
"/foo",
"/foo[]",
"/foo{}",
"/foo()",
"/[]foo",
"/{}foo",
"/()foo",
"/{[]}",
"/([])",
"/[)}({]",
"/({[]})",
"/\\[",
"/\\{",
"/\\(",
"/[\\[]",
"/(\\[)",
"/{\\[}",
"/[\\(]",
"/(\\()",
"/{\\(}",
"/[\\{]",
"/(\\{)",
"/{\\{}",
"/\\{(\\[\\([{])",
"/\\]",
"/\\}",
"/\\)",
"/[\\]]",
"/(\\])",
"/{\\]}",
"/[\\)]",
"/(\\))",
"/{\\)}",
"/[\\}]",
"/(\\})",
"/{\\}}",
"/({[\\])]})",
];
const whiteSpaceSequences = [
"",
" ",
"\t\f",
"\u3000\u2003",
];
for (const testCase of testCases) {
for (const whiteSpaces of whiteSpaceSequences) {
const testCaseWithWhiteSpaces = testCase + whiteSpaces;
const sources = [
`const regex = ${testCaseWithWhiteSpaces};`,
`(${testCaseWithWhiteSpaces});`,
`([${testCaseWithWhiteSpaces}]);`,
`({prop: ${testCaseWithWhiteSpaces}});`,
`({prop: ([(${testCaseWithWhiteSpaces})])});`,
`({[(${testCaseWithWhiteSpaces}).source]: 42});`,
];
for (const source of sources) {
it("stops parsing unterminated regexes at correct position: " + JSON.stringify(source), () => {
const { parseDiagnostics } = ts.createLanguageServiceSourceFile(
/*fileName*/ "",
ts.ScriptSnapshot.fromString(source),
ts.ScriptTarget.Latest,
/*version*/ "0",
/*setNodeParents*/ false,
);
const diagnostic = ts.find(parseDiagnostics, ({ code }) => code === ts.Diagnostics.Unterminated_regular_expression_literal.code);
assert(diagnostic, "There should be an 'Unterminated regular expression literal.' error");
assert.equal(diagnostic.start, source.indexOf("/"), "Diagnostic should start at where the regex starts");
assert.equal(diagnostic.length, testCase.length, "Diagnostic should end at where the regex ends");
});
}
}
}
});
@@ -1,13 +1,10 @@
parser645086_1.ts(1,13): error TS1005: ',' expected.
parser645086_1.ts(1,14): error TS1134: Variable declaration expected.
parser645086_1.ts(1,15): error TS1161: Unterminated regular expression literal.
==== parser645086_1.ts (3 errors) ====
==== parser645086_1.ts (2 errors) ====
var v = /[]/]/
~
!!! error TS1005: ',' expected.
~
!!! error TS1134: Variable declaration expected.
!!! error TS1161: Unterminated regular expression literal.
!!! error TS1134: Variable declaration expected.
@@ -1,13 +1,10 @@
parser645086_2.ts(1,14): error TS1005: ',' expected.
parser645086_2.ts(1,15): error TS1134: Variable declaration expected.
parser645086_2.ts(1,16): error TS1161: Unterminated regular expression literal.
==== parser645086_2.ts (3 errors) ====
==== parser645086_2.ts (2 errors) ====
var v = /[^]/]/
~
!!! error TS1005: ',' expected.
~
!!! error TS1134: Variable declaration expected.
!!! error TS1161: Unterminated regular expression literal.
!!! error TS1134: Variable declaration expected.
@@ -1,7 +1,7 @@
parserMissingToken2.ts(1,2): error TS1161: Unterminated regular expression literal.
parserMissingToken2.ts(1,1): error TS1161: Unterminated regular expression literal.
==== parserMissingToken2.ts (1 errors) ====
/ b;
~~~
!!! error TS1161: Unterminated regular expression literal.
@@ -4,4 +4,4 @@
/ b;
//// [parserMissingToken2.js]
/ b;;
/ b;
@@ -2,6 +2,6 @@
=== parserMissingToken2.ts ===
/ b;
>/ b; : RegExp
> : ^^^^^^
>/ b : RegExp
> : ^^^^^^
@@ -1,13 +1,10 @@
parserRegularExpressionDivideAmbiguity4.ts(1,1): error TS2304: Cannot find name 'foo'.
parserRegularExpressionDivideAmbiguity4.ts(1,6): error TS1161: Unterminated regular expression literal.
parserRegularExpressionDivideAmbiguity4.ts(1,17): error TS1005: ')' expected.
parserRegularExpressionDivideAmbiguity4.ts(1,5): error TS1161: Unterminated regular expression literal.
==== parserRegularExpressionDivideAmbiguity4.ts (3 errors) ====
==== parserRegularExpressionDivideAmbiguity4.ts (2 errors) ====
foo(/notregexp);
~~~
!!! error TS2304: Cannot find name 'foo'.
!!! error TS1161: Unterminated regular expression literal.
!!! error TS1005: ')' expected.
~~~~~~~~~~
!!! error TS1161: Unterminated regular expression literal.
@@ -4,4 +4,4 @@
foo(/notregexp);
//// [parserRegularExpressionDivideAmbiguity4.js]
foo(/notregexp););
foo(/notregexp);
@@ -2,10 +2,10 @@
=== parserRegularExpressionDivideAmbiguity4.ts ===
foo(/notregexp);
>foo(/notregexp); : any
> : ^^^
>foo(/notregexp) : any
> : ^^^
>foo : any
> : ^^^
>/notregexp); : RegExp
> : ^^^^^^
>/notregexp : RegExp
> : ^^^^^^
@@ -10,7 +10,7 @@ file.tsx(11,1): error TS2362: The left-hand side of an arithmetic operation must
file.tsx(11,8): error TS1003: Identifier expected.
file.tsx(11,9): error TS2304: Cannot find name 'data'.
file.tsx(11,13): error TS1005: ';' expected.
file.tsx(11,20): error TS1161: Unterminated regular expression literal.
file.tsx(11,19): error TS1161: Unterminated regular expression literal.
==== file.tsx (13 errors) ====
@@ -49,5 +49,5 @@ file.tsx(11,20): error TS1161: Unterminated regular expression literal.
!!! error TS2304: Cannot find name 'data'.
~
!!! error TS1005: ';' expected.
~~
!!! error TS1161: Unterminated regular expression literal.
@@ -22,4 +22,4 @@ data = { 32: } / > ;
{
32;
}
/>;;
/>;
@@ -56,6 +56,6 @@ declare module JSX {
> : ^^^
>32 : 32
> : ^^
>/>; : RegExp
> : ^^^^^^
>/> : RegExp
> : ^^^^^^
@@ -1,7 +1,7 @@
unterminatedRegexAtEndOfSource1.ts(1,10): error TS1161: Unterminated regular expression literal.
unterminatedRegexAtEndOfSource1.ts(1,9): error TS1161: Unterminated regular expression literal.
==== unterminatedRegexAtEndOfSource1.ts (1 errors) ====
var a = /
~
!!! error TS1161: Unterminated regular expression literal.
+1 -1
View File
@@ -5,4 +5,4 @@
goTo.marker('1');
edit.insert("\n");
verify.currentFileContentIs("var re = /\\w+ \n /;");
verify.currentFileContentIs("var re = /\\w+\n /;");