Skip to content

Commit

Permalink
Improve error message for unrecognized escape sequences.
Browse files Browse the repository at this point in the history
  • Loading branch information
pdubroy committed Jun 3, 2015
1 parent 45972f9 commit 56d8ff5
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 48 deletions.
4 changes: 3 additions & 1 deletion dist/ohm-grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ module.exports = ohm.makeRecipe(function() {
.define('escapeChar_tab', [], this.prim('\\t'))
.define('escapeChar_unicodeEscape', [], this.seq(this.prim('\\u'), this.app('hexDigit'), this.app('hexDigit'), this.app('hexDigit'), this.app('hexDigit')))
.define('escapeChar_hexEscape', [], this.seq(this.prim('\\x'), this.app('hexDigit'), this.app('hexDigit')))
.define('escapeChar', [], this.alt(this.app('escapeChar_backslash'), this.app('escapeChar_doubleQuote'), this.app('escapeChar_singleQuote'), this.app('escapeChar_backspace'), this.app('escapeChar_lineFeed'), this.app('escapeChar_carriageReturn'), this.app('escapeChar_tab'), this.app('escapeChar_unicodeEscape'), this.app('escapeChar_hexEscape')))
.define('escapeChar_unrecognized', [], this.seq(this.prim('\\'), this.not(this.app('controlChar')), this.not(this.prim(' ')), this.app('_')))
.define('escapeChar', [], this.alt(this.app('escapeChar_backslash'), this.app('escapeChar_doubleQuote'), this.app('escapeChar_singleQuote'), this.app('escapeChar_backspace'), this.app('escapeChar_lineFeed'), this.app('escapeChar_carriageReturn'), this.app('escapeChar_tab'), this.app('escapeChar_unicodeEscape'), this.app('escapeChar_hexEscape'), this.app('escapeChar_unrecognized')))
.define('controlChar', [], this.prim(/[\000-\037]/))
.define('regExp', [], this.seq(this.prim('/'), this.app('reCharClass'), this.prim('/')), 'a regular expression')
.define('reCharClass_unicode', [], this.seq(this.prim('\\p{'), this.plus(this.prim(/[A-Za-z]/)), this.prim('}')))
.define('reCharClass_ordinary', [], this.seq(this.prim('['), this.star(this.alt(this.prim('\\]'), this.seq(this.not(this.prim(']')), this.app('_')))), this.prim(']')))
Expand Down
29 changes: 24 additions & 5 deletions dist/ohm.js

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions dist/ohm.min.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/Interval.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ Interval.prototype = {
},

getLineAndColumnMessage: function() {
return util.getLineAndColumnMessage(this.inputStream.source, this.startIdx);
var range = [this.startIdx, this.endIdx];
return util.getLineAndColumnMessage(this.inputStream.source, this.startIdx, range);
},

// Returns a new Interval which contains the same contents as this one,
Expand Down
13 changes: 13 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ var GrammarSyntaxError = makeCustomError(
}
);

var UnrecognizedEscapeSequence = makeCustomError(
'ohm.error.UnrecognizedEscapeSequence',
function(interval) {
this.interval = interval;
var seq = interval.contents;
var c = seq[1];
this.message = interval.getLineAndColumnMessage() +
'Unrecognized escape sequence "' + seq + '". Did you mean ' +
JSON.stringify(seq) + ' or ' + JSON.stringify(c) + '?';
}
);

// Undeclared grammar

var UndeclaredGrammar = makeCustomError(
Expand Down Expand Up @@ -261,6 +273,7 @@ module.exports = {
MultipleErrors: MultipleErrors,
UndeclaredGrammar: UndeclaredGrammar,
UndeclaredRule: UndeclaredRule,
UnrecognizedEscapeSequence: UnrecognizedEscapeSequence,
WrongNumberOfParameters: WrongNumberOfParameters,

throwErrors: function(errors) {
Expand Down
7 changes: 5 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,13 @@ function buildGrammar(match, namespace, optOhmGrammarForTesting) {
},

strChar: function(_) {
return this.interval.contents;
return _.visit();
},

escapeChar: function(_) {
escapeChar: function(child) {
if (child.ctorName === 'escapeChar_unrecognized') {
throw new errors.UnrecognizedEscapeSequence(child.interval);
}
return this.interval.contents;
},

Expand Down
4 changes: 4 additions & 0 deletions src/ohm-grammar.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ Ohm {
| "\\t" -- tab
| "\\u" hexDigit hexDigit hexDigit hexDigit -- unicodeEscape
| "\\x" hexDigit hexDigit -- hexEscape
| "\\" ~controlChar ~" " _ -- unrecognized

controlChar
= /[\000-\037]/

regExp (a regular expression)
= "/" reCharClass "/"
Expand Down
51 changes: 20 additions & 31 deletions test/ohm-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,41 +390,30 @@ test('char', function(t) {
test('string', function(t) {
var m = ohm.grammar('M { foo = "foo\\b\\n\\r\\t\\\\\\\"\\u01bcff\\x8f" }');

test('direct match, no stream', function(t) {
it('recognition', function() {
t.ok(m.match('foo\b\n\r\t\\"'));
t.equal(m.match('foo1').failed(), true);
t.equal(m.match('bar').failed(), true);
t.equal(m.match(null).failed(), true);
});

it('semantic actions', function() {
var s = m.semantics().addAttribute('v', {});
var cst = m.match('foo\b\n\r\t\\"\u01bcff\x8f');
t.equal(s(cst).v, 'foo\b\n\r\t\\"\u01bcff\x8f');
});

it('unrecognized escape characters are parse errors', function() {
t.throws(function() { ohm.grammar('G { r = "\\w" }'); }, /Failed to parse grammar/);
});
it('recognition', function() {
t.ok(m.match('foo\b\n\r\t\\"'));
t.equal(m.match('foo1').failed(), true);
t.equal(m.match('bar').failed(), true);
t.equal(m.match(null).failed(), true);
});

t.end();
it('semantic actions', function() {
var s = m.semantics().addAttribute('v', {});
var cst = m.match('foo\b\n\r\t\\"\u01bcff\x8f');
t.equal(s(cst).v, 'foo\b\n\r\t\\"\u01bcff\x8f');
});

test('match in string stream', function(t) {
it('recognition', function() {
t.ok(m.match('foo\b\n\r\t\\"\u01bcff\x8f'));
t.equal(m.match('foo1').failed(), true);
t.equal(m.match('bar').failed(), true);
});
// Printable chars after "\" throw UnrecognizedEscapeSequence.
t.throws(function() { ohm.grammar('G { r = "\\w" }'); }, /Unrecognized escape sequence/);
t.throws(function() { ohm.grammar('G { r = "\\!" }'); }, /Unrecognized escape sequence/);
t.throws(function() { ohm.grammar('G { r = "\\~" }'); }, /Unrecognized escape sequence/);

// Non-printable chars or " " after "\" are just a regular parse error.
t.throws(function() { ohm.grammar('G { r = "\\\0" }'); }, /Failed to parse grammar/);
t.throws(function() { ohm.grammar('G { r = "\\\n" }'); }, /Failed to parse grammar/);
t.throws(function() { ohm.grammar('G { r = "\\\t" }'); }, /Failed to parse grammar/);
t.throws(function() { ohm.grammar('G { r = "\\ " }'); }, /Failed to parse grammar/);

it('semantic actions', function() {
var s = m.semantics().addAttribute('v', {});
var cst = m.match('foo\b\n\r\t\\"\u01bcff\x8f');
t.equal(s(cst).v, 'foo\b\n\r\t\\"\u01bcff\x8f');
});
t.end();
});
t.end();
});

Expand Down
20 changes: 18 additions & 2 deletions test/test-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ test('many expressions with nullable operands', function(t) {
t.equal(e.message, [
'Line 1, col 14:',
'> 1 | G { start = ("a"?)*}',
' ^',
' ^~~~',
'Nullable expression "a"? is not allowed inside \'*\' (possible infinite loop)'].join('\n'));
}

Expand All @@ -100,7 +100,7 @@ test('many expressions with nullable operands', function(t) {
t.equal(e.message, [
'Line 1, col 14:',
'> 1 | G { start = ("a"?)+}',
' ^',
' ^~~~',
'Nullable expression "a"? is not allowed inside \'+\' (possible infinite loop)'].join('\n'));
}

Expand Down Expand Up @@ -142,3 +142,19 @@ test('errors from makeGrammar()', function(t) {

t.end();
});

test('unrecognized escape sequences', function(t) {
function getExceptionMessage(ruleBody) {
try {
ohm.grammar('G { r = ' + ruleBody + ' }');
t.fail('Expected an exception to be thrown');
} catch (e) {
return e.message;
}
}

t.ok(getExceptionMessage('"\\!"').match(/Did you mean "\\\\!" or "!"?/), '"\\!"');
t.ok(getExceptionMessage('"\\w"').match(/Did you mean "\\\\w" or "w"?/), '"\\w"');

t.end();
});

0 comments on commit 56d8ff5

Please sign in to comment.