From 065f4e1b752a78eaa2149aee04d4d67838a60865 Mon Sep 17 00:00:00 2001 From: David Majda Date: Fri, 3 Apr 2015 15:59:46 +0200 Subject: [PATCH] Improve location info in syntax errors Replace |line|, |column|, and |offset| properties of |SyntaxError| with the |location| property. It contains an object similar to the one returned by the |location| function available in action code: { start: { offset: 23, line: 5, column: 6 }, end: { offset: 25, line: 5, column: 8 } } For syntax errors produced in the middle of the input, |start| refers to the first unparsed character and |end| refers to the character behind it (meaning the span is 1 character). This corresponds to the portion of the input in the |found| property. For syntax errors produced the end of the input, both |start| and |end| refer to a character past the end of the input (meaning the span is 0 characters). For syntax errors produced by calling |expected| or |error| functions in action code the location info is the same as the |location| function would return. --- README.md | 4 +- bin/pegjs | 4 +- lib/compiler/passes/generate-javascript.js | 68 ++++++++------- lib/parser.js | 68 ++++++++------- .../generated-parser-behavior.spec.js | 84 ++++++++++++------- 5 files changed, 138 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 7243390..a10b18a 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ Using the Parser Using the generated parser is simple — just call its `parse` method and pass an input string as a parameter. The method will return a parse result (the exact value depends on the grammar used to build the parser) or throw an exception if -the input is invalid. The exception will contain `offset`, `line`, `column`, -`expected`, `found` and `message` properties with more details about the error. +the input is invalid. The exception will contain `location`, `expected`, `found` +and `message` properties with more details about the error. parser.parse("abba"); // returns ["a", "b", "b", "a"] diff --git a/bin/pegjs b/bin/pegjs index 74c577f..b3d4b04 100755 --- a/bin/pegjs +++ b/bin/pegjs @@ -253,8 +253,8 @@ readStream(inputStream, function(input) { try { source = PEG.buildParser(input, options); } catch (e) { - if (e.line !== undefined && e.column !== undefined) { - abort(e.line + ":" + e.column + ": " + e.message); + if (e.location !== undefined) { + abort(e.location.start.line + ":" + e.location.start.column + ": " + e.message); } else { abort(e.message); } diff --git a/lib/compiler/passes/generate-javascript.js b/lib/compiler/passes/generate-javascript.js index 5877d7c..300cf8a 100644 --- a/lib/compiler/passes/generate-javascript.js +++ b/lib/compiler/passes/generate-javascript.js @@ -744,13 +744,11 @@ function generateJavascript(ast, options) { ' child.prototype = new ctor();', ' }', '', - ' function peg$SyntaxError(message, expected, found, offset, line, column) {', + ' function peg$SyntaxError(message, expected, found, location) {', ' this.message = message;', ' this.expected = expected;', ' this.found = found;', - ' this.offset = offset;', - ' this.line = line;', - ' this.column = column;', + ' this.location = location;', '', ' this.name = "SyntaxError";', ' }', @@ -929,33 +927,25 @@ function generateJavascript(ast, options) { ' }', '', ' function location() {', - ' var savedPosDetails = peg$computePosDetails(peg$savedPos),', - ' currPosDetails = peg$computePosDetails(peg$currPos);', - '', - ' return {', - ' start: {', - ' offset: peg$savedPos,', - ' line: savedPosDetails.line,', - ' column: savedPosDetails.column', - ' },', - ' end: {', - ' offset: peg$currPos,', - ' line: currPosDetails.line,', - ' column: currPosDetails.column', - ' }', - ' };', + ' return peg$computeLocation(peg$savedPos, peg$currPos);', ' }', '', ' function expected(description) {', ' throw peg$buildException(', ' null,', ' [{ type: "other", description: description }],', - ' peg$savedPos', + ' input.substring(peg$savedPos, peg$currPos),', + ' peg$computeLocation(peg$savedPos, peg$currPos)', ' );', ' }', '', ' function error(message) {', - ' throw peg$buildException(message, null, peg$savedPos);', + ' throw peg$buildException(', + ' message,', + ' null,', + ' input.substring(peg$savedPos, peg$currPos),', + ' peg$computeLocation(peg$savedPos, peg$currPos)', + ' );', ' }', '', ' function peg$computePosDetails(pos) {', @@ -995,6 +985,24 @@ function generateJavascript(ast, options) { ' };', ' }', '', + ' function peg$computeLocation(startPos, endPos) {', + ' var startPosDetails = peg$computePosDetails(startPos),', + ' endPosDetails = peg$computePosDetails(endPos);', + '', + ' return {', + ' start: {', + ' offset: startPos,', + ' line: startPosDetails.line,', + ' column: startPosDetails.column', + ' },', + ' end: {', + ' offset: endPos,', + ' line: endPosDetails.line,', + ' column: endPosDetails.column', + ' }', + ' };', + ' }', + '', ' function peg$fail(expected) {', ' if (peg$currPos < peg$maxFailPos) { return; }', '', @@ -1006,7 +1014,7 @@ function generateJavascript(ast, options) { ' peg$maxFailExpected.push(expected);', ' }', '', - ' function peg$buildException(message, expected, pos) {', + ' function peg$buildException(message, expected, found, location) {', ' function cleanupExpected(expected) {', ' var i = 1;', '', @@ -1080,9 +1088,6 @@ function generateJavascript(ast, options) { ' return "Expected " + expectedDesc + " but " + foundDesc + " found.";', ' }', '', - ' var posDetails = peg$computePosDetails(pos),', - ' found = pos < input.length ? input.charAt(pos) : null;', - '', ' if (expected !== null) {', ' cleanupExpected(expected);', ' }', @@ -1091,9 +1096,7 @@ function generateJavascript(ast, options) { ' message !== null ? message : buildMessage(expected, found),', ' expected,', ' found,', - ' pos,', - ' posDetails.line,', - ' posDetails.column', + ' location', ' );', ' }', '' @@ -1144,7 +1147,14 @@ function generateJavascript(ast, options) { ' peg$fail({ type: "end", description: "end of input" });', ' }', '', - ' throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos);', + ' throw peg$buildException(', + ' null,', + ' peg$maxFailExpected,', + ' peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,', + ' peg$maxFailPos < input.length', + ' ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)', + ' : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)', + ' );', ' }', ' }', '', diff --git a/lib/parser.js b/lib/parser.js index fcf59a5..4189d78 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -11,13 +11,11 @@ module.exports = (function() { child.prototype = new ctor(); } - function peg$SyntaxError(message, expected, found, offset, line, column) { + function peg$SyntaxError(message, expected, found, location) { this.message = message; this.expected = expected; this.found = found; - this.offset = offset; - this.line = line; - this.column = column; + this.location = location; this.name = "SyntaxError"; } @@ -357,33 +355,25 @@ module.exports = (function() { } function location() { - var savedPosDetails = peg$computePosDetails(peg$savedPos), - currPosDetails = peg$computePosDetails(peg$currPos); - - return { - start: { - offset: peg$savedPos, - line: savedPosDetails.line, - column: savedPosDetails.column - }, - end: { - offset: peg$currPos, - line: currPosDetails.line, - column: currPosDetails.column - } - }; + return peg$computeLocation(peg$savedPos, peg$currPos); } function expected(description) { throw peg$buildException( null, [{ type: "other", description: description }], - peg$savedPos + input.substring(peg$savedPos, peg$currPos), + peg$computeLocation(peg$savedPos, peg$currPos) ); } function error(message) { - throw peg$buildException(message, null, peg$savedPos); + throw peg$buildException( + message, + null, + input.substring(peg$savedPos, peg$currPos), + peg$computeLocation(peg$savedPos, peg$currPos) + ); } function peg$computePosDetails(pos) { @@ -423,6 +413,24 @@ module.exports = (function() { }; } + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos); + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + }; + } + function peg$fail(expected) { if (peg$currPos < peg$maxFailPos) { return; } @@ -434,7 +442,7 @@ module.exports = (function() { peg$maxFailExpected.push(expected); } - function peg$buildException(message, expected, pos) { + function peg$buildException(message, expected, found, location) { function cleanupExpected(expected) { var i = 1; @@ -493,9 +501,6 @@ module.exports = (function() { return "Expected " + expectedDesc + " but " + foundDesc + " found."; } - var posDetails = peg$computePosDetails(pos), - found = pos < input.length ? input.charAt(pos) : null; - if (expected !== null) { cleanupExpected(expected); } @@ -504,9 +509,7 @@ module.exports = (function() { message !== null ? message : buildMessage(expected, found), expected, found, - pos, - posDetails.line, - posDetails.column + location ); } @@ -4903,7 +4906,14 @@ module.exports = (function() { peg$fail({ type: "end", description: "end of input" }); } - throw peg$buildException(null, peg$maxFailExpected, peg$maxFailPos); + throw peg$buildException( + null, + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); } } diff --git a/spec/behavior/generated-parser-behavior.spec.js b/spec/behavior/generated-parser-behavior.spec.js index b362b31..adcfbf0 100644 --- a/spec/behavior/generated-parser-behavior.spec.js +++ b/spec/behavior/generated-parser-behavior.spec.js @@ -1245,9 +1245,10 @@ describe("generated parser behavior", function() { message: 'Expected a but "a" found.', expected: [{ type: "other", description: "a" }], found: "a", - offset: 0, - line: 1, - column: 1 + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } }); }); @@ -1261,9 +1262,10 @@ describe("generated parser behavior", function() { message: "a", expected: null, found: "a", - offset: 0, - line: 1, - column: 1 + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } }); }); }); @@ -1313,7 +1315,6 @@ describe("generated parser behavior", function() { var parser = PEG.buildParser('start = "a" "b" / "a" "c" "d"', options); expect(parser).toFailToParse("ace", { - offset: 2, expected: [{ type: "literal", value: "d", description: '"d"' }] }); }); @@ -1433,16 +1434,37 @@ describe("generated parser behavior", function() { }); describe("position reporting", function() { - it("reports position correctly with invalid input", function() { + it("reports position correctly at the end of input", function() { var parser = PEG.buildParser('start = "a"', options); - expect(parser).toFailToParse("b", { offset: 0, line: 1, column: 1 }); + expect(parser).toFailToParse("", { + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 0, line: 1, column: 1 } + } + }); + }); + + it("reports position correctly in the middle of input", function() { + var parser = PEG.buildParser('start = "a"', options); + + expect(parser).toFailToParse("b", { + location: { + start: { offset: 0, line: 1, column: 1 }, + end: { offset: 1, line: 1, column: 2 } + } + }); }); it("reports position correctly with trailing input", function() { var parser = PEG.buildParser('start = "a"', options); - expect(parser).toFailToParse("aa", { offset: 1, line: 1, column: 2}); + expect(parser).toFailToParse("aa", { + location: { + start: { offset: 1, line: 1, column: 2 }, + end: { offset: 2, line: 1, column: 3 } + } + }); }); it("reports position correctly in complex cases", function() { @@ -1454,38 +1476,44 @@ describe("generated parser behavior", function() { ].join("\n"), options); expect(parser).toFailToParse("1\n2\n\n3\n\n\n4 5 x", { - offset: 13, - line: 7, - column: 5 + location: { + start: { offset: 13, line: 7, column: 5 }, + end: { offset: 14, line: 7, column: 6 } + } }); /* Non-Unix newlines */ expect(parser).toFailToParse("1\rx", { // Old Mac - offset: 2, - line: 2, - column: 1 + location: { + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 2 } + } }); expect(parser).toFailToParse("1\r\nx", { // Windows - offset: 3, - line: 2, - column: 1 + location: { + start: { offset: 3, line: 2, column: 1 }, + end: { offset: 4, line: 2, column: 2 } + } }); expect(parser).toFailToParse("1\n\rx", { // mismatched - offset: 3, - line: 3, - column: 1 + location: { + start: { offset: 3, line: 3, column: 1 }, + end: { offset: 4, line: 3, column: 2 } + } }); /* Strange newlines */ expect(parser).toFailToParse("1\u2028x", { // line separator - offset: 2, - line: 2, - column: 1 + location: { + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 2 } + } }); expect(parser).toFailToParse("1\u2029x", { // paragraph separator - offset: 2, - line: 2, - column: 1 + location: { + start: { offset: 2, line: 2, column: 1 }, + end: { offset: 3, line: 2, column: 2 } + } }); }); });