Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/generator/__tests__/generator-basic-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ describe('generator-basic', () => {
test(/[^]/);
});

it('escapes a leading `^` in a non-negative character class', () => {
// An AST whose first class member is a literal `^` (e.g. after a
// transform reorders `[a^]`) must be generated as `[\^a]`, otherwise
// `[^a]` would be read back as a negative class.
const ast = parser.parse('/[a^]/');
ast.body.expressions.reverse();
const generated = generator.generate(ast);
expect(generated).toBe('/[\\^a]/');

// The escaped output round-trips to a non-negative class.
const reparsed = parser.parse(generated);
expect(reparsed.body.negative).toBeFalsy();
});

it('positive lookahead assertion', () => {
test(/(?=abc)/);
});
Expand Down
30 changes: 27 additions & 3 deletions src/generator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,23 @@ const generator = {
},

CharacterClass(node) {
const expressions = node.expressions.map(gen).join('');
const expressions = node.expressions.map(gen);

// A non-negative class whose first element is a literal `^` must escape
// it, otherwise the generated `[^...]` is parsed back as a negative class.
// This surfaces when a transform reorders the class (e.g. the optimizer
// sorting `[a^]` into `[^a]`) or when generating from a hand-built AST.
if (!node.negative && isLeadingCaret(node.expressions[0])) {
expressions[0] = '\\' + expressions[0];
}

const body = expressions.join('');

if (node.negative) {
return `[^${expressions}]`;
return `[^${body}]`;
}

return `[${expressions}]`;
return `[${body}]`;
},

ClassRange(node) {
Expand Down Expand Up @@ -174,6 +184,20 @@ const generator = {
},
};

/**
* Whether a node is an unescaped literal `^` char, which is special when it
* appears first in a non-negative character class.
*/
function isLeadingCaret(node) {
return (
node != null &&
node.type === 'Char' &&
node.kind === 'simple' &&
node.value === '^' &&
!node.escaped
);
}

module.exports = {
/**
* Generates a regexp string from an AST.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,25 @@ describe('char-class-classranges-merge', () => {
expect(re.toString()).toBe('/[<=>?]/');
});

it('keeps a `^` sorted to the front escaped, not turning the class negative', () => {
// `^` (0x5e) sorts before `a` (0x61), so the class is reordered to put
// `^` first. The leading `^` must be escaped, otherwise `[^a]` would be a
// negative class matching the opposite set.
let re = transform(/[a^]/, [
charClassClassrangesMerge,
]);
expect(re.toString()).toBe('/[\\^a]/');

re = transform(/[a^bc]/, [
charClassClassrangesMerge,
]);
expect(re.toString()).toBe('/[\\^a-c]/');

// A genuine negative class is left untouched.
re = transform(/[^a]/, [
charClassClassrangesMerge,
]);
expect(re.toString()).toBe('/[^a]/');
});

});