diff --git a/grammar.pegjs b/grammar.pegjs index adf3ded..fbf9777 100644 --- a/grammar.pegjs +++ b/grammar.pegjs @@ -14,6 +14,12 @@ } }); } + + // https://github.com/estools/esquery/issues/68 + // Inside all /regexp/ literals, we replace escaped-backslashes with the \x2F equivalent. + input = input.replaceAll(/\/((?:[^\/\\]|\\.)*?)\//g, (match) => { + return match.replaceAll("\\/", "\\\\x2F"); + }); } start @@ -97,8 +103,12 @@ attr path = i:identifierName { return { type: 'literal', value: i }; } type = "type(" _ t:[^ )]+ _ ")" { return { type: 'type', value: t.join('') }; } flags = [imsu]+ - regex = "/" d:[^/]+ "/" flgs:flags? { return { - type: 'regexp', value: new RegExp(d.join(''), flgs ? flgs.join('') : '') }; + regex = "/" d:[^/]+ "/" flgs:flags? { + // https://github.com/estools/esquery/issues/68 + const text = d.join('').replaceAll("\\\\x2F", "\\/"); + return { + type: 'regexp', value: new RegExp(text, flgs ? flgs.join('') : '') + }; } field = "." i:identifierName is:("." identifierName)* { diff --git a/parser.js b/parser.js index eac0a00..6dbc27f 100644 --- a/parser.js +++ b/parser.js @@ -250,8 +250,12 @@ peg$c75 = peg$literalExpectation("/", false), peg$c76 = /^[^\/]/, peg$c77 = peg$classExpectation(["/"], true, false), - peg$c78 = function(d, flgs) { return { - type: 'regexp', value: new RegExp(d.join(''), flgs ? flgs.join('') : '') }; + peg$c78 = function(d, flgs) { + // https://github.com/estools/esquery/issues/68 + const text = d.join('').replaceAll("\\\\x2F", "\\/"); + return { + type: 'regexp', value: new RegExp(text, flgs ? flgs.join('') : '') + }; }, peg$c79 = function(i, is) { return { type: 'field', name: is.reduce(function(memo, p){ return memo + p[0] + p[1]; }, i)}; @@ -2667,6 +2671,12 @@ }); } + // https://github.com/estools/esquery/issues/68 + // Inside all /regexp/ literals, we replace escaped-backslashes with the \x2F equivalent. + input = input.replaceAll(/\/((?:[^\/\\]|\\.)*?)\//g, (match) => { + return match.replaceAll("\\/", "\\\\x2F"); + }); + peg$result = peg$startRuleFunction(); diff --git a/tests/fixtures/literalSlash.js b/tests/fixtures/literalSlash.js new file mode 100644 index 0000000..effce87 --- /dev/null +++ b/tests/fixtures/literalSlash.js @@ -0,0 +1,13 @@ +import * as esprima from 'esprima'; + +const parsed = esprima.parse(` + var s1 = "foo/bar"; + var s2 = "foo//bar"; + + var b1 = "foo\\/bar"; + var b2 = "foo\\/\\/bar"; +`); + +export default parsed; + + diff --git a/tests/queryAttribute.js b/tests/queryAttribute.js index 6b5be0a..2dc2d29 100644 --- a/tests/queryAttribute.js +++ b/tests/queryAttribute.js @@ -2,6 +2,7 @@ import esquery from '../esquery.js'; import literal from './fixtures/literal.js'; import conditional from './fixtures/conditional.js'; import forLoop from './fixtures/forLoop.js'; +import literalSlash from './fixtures/literalSlash.js'; import simpleFunction from './fixtures/simpleFunction.js'; import simpleProgram from './fixtures/simpleProgram.js'; @@ -160,6 +161,48 @@ describe('Attribute query', function () { ]); }); + it('single backslash-escaped slash in a literal', function () { + const matches = esquery(literalSlash, '[value="foo\\/bar"]'); + assert.includeMembers(matches, [ + literalSlash.body[2].declarations[0].init + ]); + }); + + it('single backslash-escaped slash in a regexp', function () { + const matches = esquery(literalSlash, '[value=/foo\\/bar/]'); + assert.includeMembers(matches, [ + literalSlash.body[0].declarations[0].init + ]); + }); + + it('single encoded slash in a regexp', function () { + const matches = esquery(literalSlash, '[value=/foo\\x2Fbar/]'); + assert.includeMembers(matches, [ + literalSlash.body[0].declarations[0].init + ]); + }); + + it('double backslash-escaped slash in a literal', function () { + const matches = esquery(literalSlash, '[value="foo\\/\\/bar"]'); + assert.includeMembers(matches, [ + literalSlash.body[3].declarations[0].init + ]); + }); + + it('double backslash-escaped slash in a regexp', function () { + const matches = esquery(literalSlash, '[value=/foo\\/\\/bar/]'); + assert.includeMembers(matches, [ + literalSlash.body[1].declarations[0].init + ]); + }); + + it('double backslash-escaped slash in a regexp', function () { + const matches = esquery(literalSlash, '[value=/foo\\x2F\\x2Fbar/]'); + assert.includeMembers(matches, [ + literalSlash.body[1].declarations[0].init + ]); + }); + it('multiple regexp flags (i and u)', function () { const matches = esquery(simpleProgram, '[name=/\\u{61}|[SDFY]/iu]'); assert.includeMembers(matches, [