Skip to content

Commit

Permalink
Fix: catastrophic backtracking in astUtils linebreak regex (fixes #7893
Browse files Browse the repository at this point in the history
…) (#7898)

* Fix: catastrophic backtracking in astUtils linebreak regex (fixes #7893)

This fixes an issue where `astUtils.getLocationFromRangeIndex` and `astUtils.getRangeIndexFromLocation` were using a regular expression susceptible to catastrophic backtracking. The match would take quadratic time in the length of the last line of the file. Since the file in #7893 contains a 1.5 million character source map URL on the last line, rules like `no-multiple-empty-lines` would hang when using ast-utils to split the file into lines.

This issue only applies to files without trailing newlines, and is only noticable when the last line of the file contains more than 30000 characters or so. Since only a few rules use these `astUtils` functions, this would only appear when either `no-useless-escape` or `no-multiple-empty-lines` reports an error for the file.

Simplified example: Node 7.4.0 hangs when evaluating this expression.

```js
/[^\n]*\n/.test('A'.repeat(1000000))
```

* Add explanatory comments
  • Loading branch information
not-an-aardvark authored and nzakas committed Jan 12, 2017
1 parent 995554c commit 427543a
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 2 deletions.
19 changes: 17 additions & 2 deletions lib/ast-utils.js
Expand Up @@ -290,8 +290,23 @@ const lineIndexCache = new WeakMap();
function getLineIndices(sourceCode) {

if (!lineIndexCache.has(sourceCode)) {
const lineIndices = (sourceCode.text.match(/[^\r\n\u2028\u2029]*(\r\n|\r|\n|\u2028|\u2029)/g) || [])
.reduce((indices, line) => indices.concat(indices[indices.length - 1] + line.length), [0]);
const lineIndices = [0];
const lineEndingPattern = /\r\n|[\r\n\u2028\u2029]/g;
let match;

/*
* Previously, this function was implemented using a regex that
* matched a sequence of non-linebreak characters followed by a
* linebreak, then adding the lengths of the matches. However,
* this caused a catastrophic backtracking issue when the end
* of a file contained a large number of non-newline characters.
* To avoid this, the current implementation just matches newlines
* and uses match.index to get the correct line start indices.
*/

while ((match = lineEndingPattern.exec(sourceCode.text))) {
lineIndices.push(match.index + match[0].length);
}

// Store the sourceCode object in a WeakMap to avoid iterating over all of the lines every time a sourceCode object is passed in.
lineIndexCache.set(sourceCode, lineIndices);
Expand Down
7 changes: 7 additions & 0 deletions tests/lib/rules/no-multiple-empty-lines.js
Expand Up @@ -309,6 +309,13 @@ ruleTester.run("no-multiple-empty-lines", rule, {
errors: [getExpectedError(1)],
options: [{ max: 1 }],
parserOptions: { ecmaVersion: 6 }
},
{

// https://github.com/eslint/eslint/issues/7893
code: `a\n\n\n\n${"a".repeat(1e5)}`,
output: `a\n\n\n${"a".repeat(1e5)}`,
errors: [getExpectedError(2)]
}
]
});

0 comments on commit 427543a

Please sign in to comment.