Tests: Cache results for exp backtracking check (#3356)

This commit is contained in:
Michael Schmidt 2022-03-13 10:57:32 +01:00 committed by GitHub
parent 17ed91604e
commit ead22e1e03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 113 additions and 65 deletions

View File

@ -8,7 +8,7 @@ const TestCase = require('./helper/test-case');
const { BFS, BFSPathToPrismTokenPath, parseRegex } = require('./helper/util');
const { languages } = require('../components.json');
const { visitRegExpAST } = require('regexpp');
const { transform, combineTransformers, getIntersectionWordSets, JS, Words, NFA, Transformers } = require('refa');
const { transform, combineTransformers, getIntersectionWordSets, JS, Words, NFA, Transformers, isDisjointWith } = require('refa');
const scslre = require('scslre');
const { argv } = require('yargs');
const RAA = require('regexp-ast-analysis');
@ -461,6 +461,50 @@ const transformer = combineTransformers([
]);
/** @type {Map<string, Map<string, Error | null>>} */
const resultCache = new Map();
/**
* @param {string} cacheName
* @returns {Map<string, Error | null>}
*/
function getResultCache(cacheName) {
let cache = resultCache.get(cacheName);
if (cache === undefined) {
resultCache.set(cacheName, cache = new Map());
}
return cache;
}
/**
* @param {string} cacheName
* @param {T} cacheKey
* @param {(node: T) => void} compute
* @returns {void}
* @template {import('regexpp/ast').Node} T
*/
function withResultCache(cacheName, cacheKey, compute) {
const hasBackRef = RAA.hasSomeDescendant(cacheKey, n => n.type === 'Backreference');
if (hasBackRef) {
compute(cacheKey);
return;
}
const cache = getResultCache(cacheName);
let cached = cache.get(cacheKey.raw);
if (cached === undefined) {
try {
compute(cacheKey);
cached = null;
} catch (error) {
cached = error;
}
cache.set(cacheKey.raw, cached);
}
if (cached) {
throw cached;
}
}
/**
* @param {string} path
* @param {RegExp} pattern
@ -510,6 +554,7 @@ function checkExponentialBacktracking(path, pattern, ast) {
return;
}
withResultCache('disjointAlternatives', node, () => {
const alternatives = node.alternatives;
const total = toNFA(alternatives[0]);
@ -519,7 +564,7 @@ function checkExponentialBacktracking(path, pattern, ast) {
const current = toNFA(a);
current.withoutEmptyWord();
if (!total.isDisjointWith(current)) {
if (!isDisjointWith(total, current)) {
assert.fail(`${path}: The alternative \`${a.raw}\` is not disjoint with at least one previous alternative.`
+ ` This will cause exponential backtracking.`
+ `\n\nTo fix this issue, you have to rewrite the ${node.type} \`${node.raw}\`.`
@ -536,6 +581,7 @@ function checkExponentialBacktracking(path, pattern, ast) {
total.union(current);
}
}
});
}
visitRegExpAST(ast.pattern, {
@ -555,6 +601,7 @@ function checkExponentialBacktracking(path, pattern, ast) {
return; // not a group
}
withResultCache('2star', node, () => {
// The idea here is the following:
//
// We have found a part `A*` of the regex (`A` is assumed to not accept the empty word). Let `I` be
@ -574,7 +621,7 @@ function checkExponentialBacktracking(path, pattern, ast) {
const twoStar = nfa.copy();
twoStar.quantify(2, Infinity);
if (!nfa.isDisjointWith(twoStar)) {
if (!isDisjointWith(nfa, twoStar)) {
const word = Words.pickMostReadableWord(firstOf(getIntersectionWordSets(nfa, twoStar)));
const example = Words.fromUnicodeToString(word);
assert.fail(`${path}: The quantifier \`${node.raw}\` ambiguous for all words ${JSON.stringify(example)}.repeat(n) for any n>1.`
@ -598,6 +645,7 @@ function checkExponentialBacktracking(path, pattern, ast) {
+ ` If you are trying to make this test pass for a pull request but can\'t fix the issue yourself, then make the pull request (or commit) anyway, a maintainer will help you.`
+ `\n\nFull pattern:\n${pattern}`);
}
});
},
});