Skip to content

Commit

Permalink
Merge pull request #89 from tjenkinson/refactor-atomic-group-tracker
Browse files Browse the repository at this point in the history
simplify atomic group handling
  • Loading branch information
tjenkinson authored May 27, 2022
2 parents 8d65a7e + e58e25a commit 843a345
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 214 deletions.
46 changes: 2 additions & 44 deletions src/__snapshots__/redos-detector.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -292511,51 +292511,9 @@ exports[`RedosDetector isSafe cases /a+a{0,3}$/ 4`] = `"Regex is safe. There cou

exports[`RedosDetector isSafe cases /a?(?=(a{0,1}))\\1$/ 1`] = `null`;

exports[`RedosDetector isSafe cases /a?(?=(a{0,1}))\\1$/ 2`] = `
Object {
"infinite": false,
"value": 1,
}
`;

exports[`RedosDetector isSafe cases /a?(?=(a{0,1}))\\1$/ 3`] = `
Array [
Object {
"trail": Array [
Object {
"a": Object {
"backreferenceStack": Array [],
"node": Object {
"end": Object {
"offset": 1,
},
"source": "a",
"start": Object {
"offset": 0,
},
},
"quantifierIterations": "removed",
},
"b": Object {
"backreferenceStack": Array [],
"node": Object {
"end": Object {
"offset": 18,
},
"source": "a",
"start": Object {
"offset": 17,
},
},
"quantifierIterations": "removed",
},
},
],
},
]
`;
exports[`RedosDetector isSafe cases /a?(?=(a{0,1}))\\1$/ 2`] = `Array []`;

exports[`RedosDetector isSafe cases /a?(?=(a{0,1}))\\1$/ 4`] = `"Regex is safe. There could be at most 1 backtrack."`;
exports[`RedosDetector isSafe cases /a?(?=(a{0,1}))\\1$/ 3`] = `"Regex is safe."`;

exports[`RedosDetector isSafe cases /a?(?=a?)$/ 1`] = `null`;

Expand Down
75 changes: 0 additions & 75 deletions src/atomic-groups-to-synchronisation-checker-keys.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/character-groups.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { intersectRanges, OurRange } from './our-range';
import { mergeSets, subtractSets } from './set';
import { mergeSets, subtractSets } from './sets';

export type MutableCharacterGroups = {
characterClassEscapes: Set<string>;
Expand Down
42 changes: 14 additions & 28 deletions src/checker-reader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { areSetsEqual, setsOverlap } from './sets';
import {
AstNode,
CharacterClass,
Expand Down Expand Up @@ -28,15 +29,11 @@ import {
characterReaderLevel2TypeSplit,
CharacterReaderLevel2Value,
} from './character-reader/character-reader-level-2';
import { atomicGroupsToSynchronisationCheckerKeys } from './atomic-groups-to-synchronisation-checker-keys';
import { BackReferenceStack } from './character-reader/character-reader-level-1';
import { Groups } from './nodes/group';
import { InfiniteLoopTracker } from './infinite-loop-tracker';
import { last } from './arrays';
import { MyFeatures } from './parse';
import { setsOverlap } from './set';
import { SidesEqualChecker } from './sides-equal-checker';
import { synchronisationCheck } from './synchronisation-checker';

export type CheckerInput = Readonly<{
atomicGroupOffsets: ReadonlySet<number>;
Expand Down Expand Up @@ -350,34 +347,23 @@ export function* buildCheckerReader(input: CheckerInput): CheckerReader {
return;
}

const syncResult = synchronisationCheck(
atomicGroupsInSync,
atomicGroupsToSynchronisationCheckerKeys({
atomicGroupOffsets: input.atomicGroupOffsets,
leftGroupsNow: leftValue.groups,
leftPreceedingGroups: leftValue.preceedingZeroWidthEntries.reduce<
Groups[]
>(
(acc, entry) =>
entry.type === 'groups' ? [...acc, entry.groups] : acc,
[]
),
rightGroupsNow: rightValue.groups,
rightPreceedingGroups: rightValue.preceedingZeroWidthEntries.reduce<
Groups[]
>(
(acc, entry) =>
entry.type === 'groups' ? [...acc, entry.groups] : acc,
[]
),
})
const leftAtomicGroups = new Set(
[...leftValue.groups.keys()].filter((group) =>
input.atomicGroupOffsets.has(group.range[0])
)
);

if (syncResult.type === 'goneOutOfSync') {
const rightAtomicGroups = new Set(
[...rightValue.groups.keys()].filter((group) =>
input.atomicGroupOffsets.has(group.range[0])
)
);
if (!areSetsEqual(leftAtomicGroups, rightAtomicGroups)) {
// if we are not entering/leaving an atomic group in sync
// then bail, as atomic groups can't give something up to be
// consumed somewhere else
dispose();
return;
}
atomicGroupsInSync = syncResult.keysInSync;

const newEntry: TrailEntry = {
intersection,
Expand Down
2 changes: 1 addition & 1 deletion src/redos-detector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ describe('RedosDetector', () => {
[/(?=(a{0,1}))\1a?$/, true],
[/(?=(a?))\1a?$/, true],
[/(?=(a?b))\1a?$/, true],
[/a?(?=(a{0,1}))\1$/, false],
[/a?(?=(a{0,1}))\1$/, true],
[/(?=(a?a?))\1a$/, false],

// atomic group workaround not detected because group not entire lookahead
Expand Down
55 changes: 55 additions & 0 deletions src/sets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { areSetsEqual, mergeSets, setsOverlap, subtractSets } from './sets';

describe('Sets', () => {
describe('areSetsEqual', () => {
it('works', () => {
expect(areSetsEqual(new Set(), new Set())).toBe(true);
expect(areSetsEqual(new Set([1]), new Set([]))).toBe(false);
expect(areSetsEqual(new Set([]), new Set([1]))).toBe(false);
expect(areSetsEqual(new Set([1]), new Set([1]))).toBe(true);
expect(areSetsEqual(new Set([1, 2]), new Set([1, 3]))).toBe(false);
expect(areSetsEqual(new Set([2, 1]), new Set([1, 2]))).toBe(true);
});
});

describe('mergeSets', () => {
it('works', () => {
expect([...mergeSets(new Set(), new Set())]).toStrictEqual([]);
expect([...mergeSets(new Set([1]), new Set([2]))].sort()).toStrictEqual([
1, 2,
]);
expect(
[...mergeSets(new Set([1, 2]), new Set([2]))].sort()
).toStrictEqual([1, 2]);
expect(
[...mergeSets(new Set([1, 2]), new Set([2, 3]))].sort()
).toStrictEqual([1, 2, 3]);
});
});

describe('subtractSets', () => {
it('works', () => {
expect([...subtractSets(new Set(), new Set())]).toStrictEqual([]);
expect(
[...subtractSets(new Set([1]), new Set([2]))].sort()
).toStrictEqual([1]);
expect(
[...subtractSets(new Set([1, 2]), new Set([2]))].sort()
).toStrictEqual([1]);
expect(
[...subtractSets(new Set([1, 2]), new Set([2, 3]))].sort()
).toStrictEqual([1]);
});
});

describe('setsOverlap', () => {
it('works', () => {
expect(setsOverlap(new Set(), new Set())).toBe(false);
expect(setsOverlap(new Set([1]), new Set([]))).toBe(false);
expect(setsOverlap(new Set([]), new Set([1]))).toBe(false);
expect(setsOverlap(new Set([1]), new Set([1]))).toBe(true);
expect(setsOverlap(new Set([1, 2]), new Set([1, 3]))).toBe(true);
expect(setsOverlap(new Set([2, 1]), new Set([1, 2]))).toBe(true);
});
});
});
4 changes: 4 additions & 0 deletions src/set.ts → src/sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export function mergeSets<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
return new Set([...a, ...b]);
}

export function areSetsEqual<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
return a.size === b.size && mergeSets(a, b).size === a.size;
}

export function subtractSets<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
const newSet = new Set(a);
b.forEach((bEntry) => newSet.delete(bEntry));
Expand Down
65 changes: 0 additions & 65 deletions src/synchronisation-checker.ts

This file was deleted.

0 comments on commit 843a345

Please sign in to comment.