From 769a432ff0ef2ef4c788860681e03ccc0c8abd72 Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 31 May 2021 22:08:09 +0500 Subject: [PATCH 01/21] source-map: Create SourceNodes for functions and initializers --- lib/compiler/passes/generate-bytecode.js | 15 ++- lib/compiler/passes/generate-js.js | 103 ++++++++++++++++-- package-lock.json | 49 ++++++++- package.json | 1 + .../compiler/passes/generate-bytecode.spec.js | 77 +++++++++++-- 5 files changed, 221 insertions(+), 24 deletions(-) diff --git a/lib/compiler/passes/generate-bytecode.js b/lib/compiler/passes/generate-bytecode.js index 6e4370d3..1274efad 100644 --- a/lib/compiler/passes/generate-bytecode.js +++ b/lib/compiler/passes/generate-bytecode.js @@ -248,8 +248,13 @@ function generateBytecode(ast) { return index === -1 ? expectations.push(expected) - 1 : index; } - function addFunctionConst(predicate, params, code) { - const func = { predicate, params, body: code }; + function addFunctionConst(predicate, params, node) { + const func = { + predicate, + params, + body: node.code, + location: node.codeLocation, + }; const pattern = JSON.stringify(func); const index = functions.findIndex(f => JSON.stringify(f) === pattern); @@ -322,7 +327,7 @@ function generateBytecode(ast) { function buildSemanticPredicate(node, negative, context) { const functionIndex = addFunctionConst( - true, Object.keys(context.env), node.code + true, Object.keys(context.env), node ); return buildSequence( @@ -438,7 +443,7 @@ function generateBytecode(ast) { const match = node.expression.match | 0; // Function only required if expression can match const functionIndex = emitCall && match !== NEVER_MATCH - ? addFunctionConst(false, Object.keys(env), node.code) + ? addFunctionConst(false, Object.keys(env), node) : null; return emitCall @@ -499,7 +504,7 @@ function generateBytecode(ast) { const functionIndex = addFunctionConst( false, Object.keys(context.env), - context.action.code + context.action ); return buildSequence( diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index ccc048c1..0dcab906 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -5,6 +5,77 @@ const op = require("../opcodes"); const Stack = require("../stack"); const VERSION = require("../../version"); const { stringEscape, regexpClassEscape } = require("../utils"); +const { SourceNode } = require("source-map"); + +/** + * Converts source text from the grammar into the `source-map` object + * + * @param {string} code Multiline string with source code + * @param {import("../peg").Location} location + * Location that represents code block in the grammar + * @param {string?} name Name of the code chunk + * + * @returns {SourceNode} New node that represents code chunk. + * Code will be splitted by lines if necessary + */ +function toSourceNode(code, location, name) { + const line = location.start.line; + // `source-map` columns are 0-based, peggy columns is 1-based + const column = location.start.column - 1; + const lines = code.split("\n"); + + if (lines.length === 1) { + return new SourceNode( + line, column, location.source, code, name + ); + } + + return new SourceNode( + null, null, location.source, lines.map((l, i) => new SourceNode( + line + i, + i === 0 ? column : 0, + location.source, + i === lines.length - 1 ? l : [l, "\n"], + name + )) + ); +} + +/** + * Wraps code line that consists from three parts into `SourceNode`. + * + * @param {string} prefix String that will be prepended before mapped chunk + * @param {string} chunk Chunk for mapping (possible multiline) + * @param {import("../../peg").Location} location + * Location that represents chunk in the grammar + * @param {string} suffix String that will be appended after mapped chunk + * @param {string?} name Name of the code chunk + * + * @returns {SourceNode} New node that represents code chunk. + * Code will be splitted by lines if necessary + */ +function wrapInSourceNode(prefix, chunk, location, suffix, name) { + // If location is not defined (for example, AST node was replaced + // by a plugin and does not provide location information, see + // plugin-api.spec.js/"can replace parser") returns original chunk + if (location) { + return new SourceNode(null, null, location.source, [ + prefix, + toSourceNode(chunk, location, name), + // Mark end location with column information otherwise + // mapping will be always continue to the end of line + new SourceNode( + location.end.line, + // `source-map` columns are 0-based, peggy columns is 1-based + location.end.column - 1, + location.source, + suffix + ), + ]); + } + + return new SourceNode(null, null, null, [prefix, chunk, suffix]); +} // Generates parser JavaScript code. function generateJS(ast, options) { @@ -60,10 +131,13 @@ function generateJS(ast, options) { } } - function buildFunc(a) { - return "function(" + a.params.join(", ") + ") {" - + a.body - + "}"; + function buildFunc(a, i) { + return wrapInSourceNode( + `var ${f(i)} = function(${a.params.join(", ")}) {`, + a.body, + a.location, + "};" + ).toString(); } return ast.literals.map( @@ -72,9 +146,7 @@ function generateJS(ast, options) { (c, i) => "var " + r(i) + " = " + buildRegexp(c) + ";" )).concat("", ast.expectations.map( (c, i) => "var " + e(i) + " = " + buildExpectation(c) + ";" - )).concat("", ast.functions.map( - (c, i) => "var " + f(i) + " = " + buildFunc(c) + ";" - )).join("\n"); + )).concat("", ast.functions.map(buildFunc)).join("\n"); } function generateRuleHeader(ruleNameCode, ruleIndexCode) { @@ -474,11 +546,24 @@ function generateJS(ast, options) { return parts.join("\n"); } + function ast2SourceNode(node) { + // If location is not defined (for example, AST node was replaced + // by a plugin and does not provide location information, see + // plugin-api.spec.js/"can replace parser") returns initializer code + if (node.codeLocation) { + // Append "$" to the name to create an impossible rule name + // so that names will not collide with rule names + return toSourceNode(node.code, node.codeLocation, "$" + node.type); + } + + return node.code; + } + function generateToplevel() { const parts = []; if (ast.topLevelInitializer) { - parts.push(ast.topLevelInitializer.code); + parts.push(ast2SourceNode(ast.topLevelInitializer)); parts.push(""); } @@ -900,7 +985,7 @@ function generateJS(ast, options) { }); if (ast.initializer) { - parts.push(indent2(ast.initializer.code)); + parts.push(ast2SourceNode(ast.initializer)); parts.push(""); } diff --git a/package-lock.json b/package-lock.json index 3acd5858..30600618 100644 --- a/package-lock.json +++ b/package-lock.json @@ -829,6 +829,12 @@ "requires": { "@types/yargs-parser": "*" } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -841,6 +847,14 @@ "callsites": "^3.0.0", "graceful-fs": "^4.2.4", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "@jest/test-result": { @@ -935,6 +949,12 @@ "requires": { "@types/yargs-parser": "*" } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -2256,6 +2276,13 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -3256,6 +3283,14 @@ "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "istanbul-reports": { @@ -5721,9 +5756,9 @@ } }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "dev": true }, "source-map-support": { @@ -5734,6 +5769,14 @@ "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "sourcemap-codec": { diff --git a/package.json b/package.json index 7251e102..68d27bba 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "rimraf": "^3.0.2", "rollup": "^2.56.2", "sinon": "^11.1.2", + "source-map": "^0.7.3", "terser": "^5.7.1", "ts-jest": "^27.0.4", "tsd": "^0.17.0", diff --git a/test/unit/compiler/passes/generate-bytecode.spec.js b/test/unit/compiler/passes/generate-bytecode.spec.js index 6fe4fb95..10130c64 100644 --- a/test/unit/compiler/passes/generate-bytecode.spec.js +++ b/test/unit/compiler/passes/generate-bytecode.spec.js @@ -162,7 +162,16 @@ describe("compiler pass |generateBytecode|", () => { ["a"], [], [{ type: "literal", value: "a", ignoreCase: false }], - [{ predicate: false, params: [], body: " code " }] + [{ + predicate: false, + params: [], + body: " code ", + location: { + source: undefined, + start: { offset: 13, line: 1, column: 14 }, + end: { offset: 19, line: 1, column: 20 }, + }, + }] )); }); }); @@ -186,7 +195,16 @@ describe("compiler pass |generateBytecode|", () => { ["a"], [], [{ type: "literal", value: "a", ignoreCase: false }], - [{ predicate: false, params: ["a"], body: " code " }] + [{ + predicate: false, + params: ["a"], + body: " code ", + location: { + source: undefined, + start: { offset: 15, line: 1, column: 16 }, + end: { offset: 21, line: 1, column: 22 }, + }, + }] )); }); }); @@ -226,7 +244,16 @@ describe("compiler pass |generateBytecode|", () => { { type: "literal", value: "b", ignoreCase: false }, { type: "literal", value: "c", ignoreCase: false }, ], - [{ predicate: false, params: ["a", "b", "c"], body: " code " }] + [{ + predicate: false, + params: ["a", "b", "c"], + body: " code ", + location: { + source: undefined, + start: { offset: 27, line: 1, column: 28 }, + end: { offset: 33, line: 1, column: 34 }, + }, + }] )); }); }); @@ -467,7 +494,16 @@ describe("compiler pass |generateBytecode|", () => { [], [], [], - [{ predicate: true, params: [], body: " code " }] + [{ + predicate: true, + params: [], + body: " code ", + location: { + source: undefined, + start: { offset: 10, line: 1, column: 11 }, + end: { offset: 16, line: 1, column: 17 }, + }, + }] ) ); }); @@ -519,7 +555,16 @@ describe("compiler pass |generateBytecode|", () => { { type: "literal", value: "b", ignoreCase: false }, { type: "literal", value: "c", ignoreCase: false }, ], - [{ predicate: true, params: ["a", "b", "c"], body: " code " }] + [{ + predicate: true, + params: ["a", "b", "c"], + body: " code ", + location: { + source: undefined, + start: { offset: 28, line: 1, column: 29 }, + end: { offset: 34, line: 1, column: 35 }, + }, + }] )); }); }); @@ -548,7 +593,16 @@ describe("compiler pass |generateBytecode|", () => { [], [], [], - [{ predicate: true, params: [], body: " code " }] + [{ + predicate: true, + params: [], + body: " code ", + location: { + source: undefined, + start: { offset: 10, line: 1, column: 11 }, + end: { offset: 16, line: 1, column: 17 }, + }, + }] ) ); }); @@ -600,7 +654,16 @@ describe("compiler pass |generateBytecode|", () => { { type: "literal", value: "b", ignoreCase: false }, { type: "literal", value: "c", ignoreCase: false }, ], - [{ predicate: true, params: ["a", "b", "c"], body: " code " }] + [{ + predicate: true, + params: ["a", "b", "c"], + body: " code ", + location: { + source: undefined, + start: { offset: 28, line: 1, column: 29 }, + end: { offset: 34, line: 1, column: 35 }, + }, + }] )); }); }); From fcae22ae342d792de179a01531b3de99e9d96228 Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 31 May 2021 23:05:27 +0500 Subject: [PATCH 02/21] source-map: Generate tables of constants as a SourceNode --- lib/compiler/passes/generate-js.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 0dcab906..091f8e33 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -133,20 +133,25 @@ function generateJS(ast, options) { function buildFunc(a, i) { return wrapInSourceNode( - `var ${f(i)} = function(${a.params.join(", ")}) {`, + `\nvar ${f(i)} = function(${a.params.join(", ")}) {`, a.body, a.location, "};" - ).toString(); + ); } - return ast.literals.map( - (c, i) => "var " + l(i) + " = " + buildLiteral(c) + ";" - ).concat("", ast.classes.map( - (c, i) => "var " + r(i) + " = " + buildRegexp(c) + ";" - )).concat("", ast.expectations.map( - (c, i) => "var " + e(i) + " = " + buildExpectation(c) + ";" - )).concat("", ast.functions.map(buildFunc)).join("\n"); + return new SourceNode( + null, null, options.grammarSource, [ + ast.literals.map( + (c, i) => "var " + l(i) + " = " + buildLiteral(c) + ";" + ).concat("", ast.classes.map( + (c, i) => "var " + r(i) + " = " + buildRegexp(c) + ";" + )).concat("", ast.expectations.map( + (c, i) => "var " + e(i) + " = " + buildExpectation(c) + ";" + )).concat("").join("\n"), + ast.functions.map(buildFunc), + ] + ); } function generateRuleHeader(ruleNameCode, ruleIndexCode) { @@ -803,7 +808,7 @@ function generateJS(ast, options) { " var peg$startRuleFunctions = " + startRuleFunctions + ";", " var peg$startRuleFunction = " + startRuleFunction + ";", "", - indent2(generateTables()), + indent2(generateTables().toString()), "", " var peg$currPos = 0;", " var peg$savedPos = 0;", From de0ad72c07d54ad2dc219eb95ebdfdfa68de2e4f Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 31 May 2021 23:25:20 +0500 Subject: [PATCH 03/21] source-map: Remove excess `.join("\n")`s Such joins stringify SourceNode's prematurally --- lib/compiler/passes/generate-js.js | 114 ++++++++++++++--------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 091f8e33..fcfe1469 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -160,28 +160,28 @@ function generateJS(ast, options) { parts.push(""); if (options.trace) { - parts.push([ + parts.push( "peg$tracer.trace({", " type: \"rule.enter\",", " rule: " + ruleNameCode + ",", " location: peg$computeLocation(startPos, startPos)", "});", - "", - ].join("\n")); + "" + ); } if (options.cache) { - parts.push([ + parts.push( "var key = peg$currPos * " + ast.rules.length + " + " + ruleIndexCode + ";", "var cached = peg$resultsCache[key];", "", "if (cached) {", " peg$currPos = cached.nextPos;", - "", - ].join("\n")); + "" + ); if (options.trace) { - parts.push([ + parts.push( "if (cached.result !== peg$FAILED) {", " peg$tracer.trace({", " type: \"rule.match\",", @@ -196,15 +196,15 @@ function generateJS(ast, options) { " location: peg$computeLocation(startPos, startPos)", " });", "}", - "", - ].join("\n")); + "" + ); } - parts.push([ + parts.push( " return cached.result;", "}", - "", - ].join("\n")); + "" + ); } return parts.join("\n"); @@ -214,14 +214,14 @@ function generateJS(ast, options) { const parts = []; if (options.cache) { - parts.push([ + parts.push( "", - "peg$resultsCache[key] = { nextPos: peg$currPos, result: " + resultCode + " };", - ].join("\n")); + "peg$resultsCache[key] = { nextPos: peg$currPos, result: " + resultCode + " };" + ); } if (options.trace) { - parts.push([ + parts.push( "", "if (" + resultCode + " !== peg$FAILED) {", " peg$tracer.trace({", @@ -236,14 +236,14 @@ function generateJS(ast, options) { " rule: " + ruleNameCode + ",", " location: peg$computeLocation(startPos, startPos)", " });", - "}", - ].join("\n")); + "}" + ); } - parts.push([ + parts.push( "", - "return " + resultCode + ";", - ].join("\n")); + "return " + resultCode + ";" + ); return parts.join("\n"); } @@ -572,7 +572,7 @@ function generateJS(ast, options) { parts.push(""); } - parts.push([ + parts.push( "function peg$subclass(child, parent) {", " function C() { this.constructor = child; }", " C.prototype = parent.prototype;", @@ -730,11 +730,11 @@ function generateJS(ast, options) { "", " return \"Expected \" + describeExpected(expected) + \" but \" + describeFound(found) + \" found.\";", "};", - "", - ].join("\n")); + "" + ); if (options.trace) { - parts.push([ + parts.push( "function peg$DefaultTracer() {", " this.indentLevel = 0;", "}", @@ -787,8 +787,8 @@ function generateJS(ast, options) { " throw new Error(\"Invalid event type: \" + event.type + \".\");", " }", "};", - "", - ].join("\n")); + "" + ); } const startRuleFunctions = "{ " @@ -798,7 +798,7 @@ function generateJS(ast, options) { + " }"; const startRuleFunction = "peg$parse" + options.allowedStartRules[0]; - parts.push([ + parts.push( "function peg$parse(input, options) {", " options = options !== undefined ? options : {};", "", @@ -816,24 +816,24 @@ function generateJS(ast, options) { " var peg$maxFailPos = 0;", " var peg$maxFailExpected = [];", " var peg$silentFails = 0;", // 0 = report failures, > 0 = silence failures - "", - ].join("\n")); + "" + ); if (options.cache) { - parts.push([ + parts.push( " var peg$resultsCache = {};", - "", - ].join("\n")); + "" + ); } if (options.trace) { - parts.push([ + parts.push( " var peg$tracer = \"tracer\" in options ? options.tracer : new peg$DefaultTracer();", - "", - ].join("\n")); + "" + ); } - parts.push([ + parts.push( " var peg$result;", "", " if (\"startRule\" in options) {", @@ -981,8 +981,8 @@ function generateJS(ast, options) { " location", " );", " }", - "", - ].join("\n")); + "" + ); ast.rules.forEach(rule => { parts.push(indent2(generateRuleFunction(rule))); @@ -994,7 +994,7 @@ function generateJS(ast, options) { parts.push(""); } - parts.push([ + parts.push( " peg$result = peg$startRuleFunction();", "", " if (peg$result !== peg$FAILED && peg$currPos === input.length) {", @@ -1012,8 +1012,8 @@ function generateJS(ast, options) { " : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)", " );", " }", - "}", - ].join("\n")); + "}" + ); return parts.join("\n"); } @@ -1079,12 +1079,12 @@ function generateJS(ast, options) { const parts = []; const dependencyVars = Object.keys(options.dependencies); - parts.push([ + parts.push( generateGeneratedByComment(), "", "\"use strict\";", - "", - ].join("\n")); + "" + ); if (dependencyVars.length > 0) { dependencyVars.forEach(variable => { @@ -1098,12 +1098,12 @@ function generateJS(ast, options) { parts.push(""); } - parts.push([ + parts.push( toplevelCode, "", "module.exports = " + generateParserObject() + ";", - "", - ].join("\n")); + "" + ); return parts.join("\n"); }, @@ -1190,23 +1190,23 @@ function generateJS(ast, options) { ).join(", "); const params = dependencyVars.join(", "); - parts.push([ + parts.push( generateGeneratedByComment(), "(function(root, factory) {", " if (typeof define === \"function\" && define.amd) {", " define(" + dependencies + ", factory);", " } else if (typeof module === \"object\" && module.exports) {", - " module.exports = factory(" + requires + ");", - ].join("\n")); + " module.exports = factory(" + requires + ");" + ); if (options.exportVar !== null) { - parts.push([ + parts.push( " } else {", - " root." + options.exportVar + " = factory();", - ].join("\n")); + " root." + options.exportVar + " = factory();" + ); } - parts.push([ + parts.push( " }", "})(this, function(" + params + ") {", " \"use strict\";", @@ -1215,8 +1215,8 @@ function generateJS(ast, options) { "", indent2("return " + generateParserObject() + ";"), "});", - "", - ].join("\n")); + "" + ); return parts.join("\n"); }, From 4ad80de980897c896f25b4ec800808611d60c7aa Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 31 May 2021 23:44:13 +0500 Subject: [PATCH 04/21] source-map: Do not join arrays prematurely Again, this prevents premature stringification of the SourceNodes --- lib/compiler/passes/generate-js.js | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index fcfe1469..f1282a28 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -207,7 +207,7 @@ function generateJS(ast, options) { ); } - return parts.join("\n"); + return parts; } function generateRuleFooter(ruleNameCode, resultCode) { @@ -245,7 +245,7 @@ function generateJS(ast, options) { "return " + resultCode + ";" ); - return parts.join("\n"); + return parts; } function generateRuleFunction(rule) { @@ -280,10 +280,10 @@ function generateJS(ast, options) { ); parts.push("if (" + cond + ") {"); - parts.push(indent2(thenCode)); + parts.push(...thenCode.map(indent2)); if (elseLength > 0) { parts.push("} else {"); - parts.push(indent2(elseCode)); + parts.push(...elseCode.map(indent2)); } parts.push("}"); } @@ -300,7 +300,7 @@ function generateJS(ast, options) { }); parts.push("while (" + cond + ") {"); - parts.push(indent2(bodyCode)); + parts.push(...bodyCode.map(indent2)); parts.push("}"); } @@ -523,7 +523,7 @@ function generateJS(ast, options) { } } - return parts.join("\n"); + return parts; } const code = compile(rule.bytecode); @@ -536,19 +536,19 @@ function generateJS(ast, options) { parts.push(indent2(stack.defines())); - parts.push(indent2(generateRuleHeader( + parts.push(...generateRuleHeader( "\"" + stringEscape(rule.name) + "\"", asts.indexOfRule(ast, rule.name) - ))); - parts.push(indent2(code)); - parts.push(indent2(generateRuleFooter( + ).map(indent2)); + parts.push(...code.map(indent2)); + parts.push(...generateRuleFooter( "\"" + stringEscape(rule.name) + "\"", stack.result() - ))); + ).map(indent2)); parts.push("}"); - return parts.join("\n"); + return parts; } function ast2SourceNode(node) { @@ -985,7 +985,7 @@ function generateJS(ast, options) { ); ast.rules.forEach(rule => { - parts.push(indent2(generateRuleFunction(rule))); + parts.push(...generateRuleFunction(rule).map(indent2)); parts.push(""); }); @@ -1015,7 +1015,7 @@ function generateJS(ast, options) { "}" ); - return parts.join("\n"); + return parts; } function generateWrapper(toplevelCode) { @@ -1072,7 +1072,7 @@ function generateJS(ast, options) { "", indent2("return " + generateParserObject() + ";"), "})()", - ].join("\n"); + ]; }, commonjs() { @@ -1105,7 +1105,7 @@ function generateJS(ast, options) { "" ); - return parts.join("\n"); + return parts; }, es() { @@ -1136,7 +1136,7 @@ function generateJS(ast, options) { "" ); - return parts.join("\n"); + return parts; }, amd() { @@ -1159,7 +1159,7 @@ function generateJS(ast, options) { indent2("return " + generateParserObject() + ";"), "});", "", - ].join("\n"); + ]; }, globals() { @@ -1173,7 +1173,7 @@ function generateJS(ast, options) { indent2("root." + options.exportVar + " = " + generateParserObject() + ";"), "})(this);", "", - ].join("\n"); + ]; }, umd() { @@ -1218,14 +1218,14 @@ function generateJS(ast, options) { "" ); - return parts.join("\n"); + return parts; }, }; return generators[options.format](); } - ast.code = generateWrapper(generateToplevel()); + ast.code = generateWrapper(generateToplevel().join("\n")).join("\n"); } module.exports = generateJS; From 9bdf8eb876af3ff3f45300419348e8e37c39a954 Mon Sep 17 00:00:00 2001 From: Mingun Date: Tue, 1 Jun 2021 00:27:07 +0500 Subject: [PATCH 05/21] source-map: Remove yet another premature join Here just for uniformity --- lib/compiler/passes/generate-js.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index f1282a28..e3dc8d65 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -1024,7 +1024,7 @@ function generateJS(ast, options) { `// Generated by Peggy ${VERSION}.`, "//", "// https://peggyjs.org/", - ].join("\n"); + ]; } function generateParserObject() { @@ -1064,7 +1064,7 @@ function generateJS(ast, options) { const generators = { bare() { return [ - generateGeneratedByComment(), + ...generateGeneratedByComment(), "(function() {", " \"use strict\";", "", @@ -1076,11 +1076,10 @@ function generateJS(ast, options) { }, commonjs() { - const parts = []; const dependencyVars = Object.keys(options.dependencies); + const parts = generateGeneratedByComment(); parts.push( - generateGeneratedByComment(), "", "\"use strict\";", "" @@ -1109,13 +1108,10 @@ function generateJS(ast, options) { }, es() { - const parts = []; const dependencyVars = Object.keys(options.dependencies); - parts.push( - generateGeneratedByComment(), - "" - ); + const parts = generateGeneratedByComment(); + parts.push(""); if (dependencyVars.length > 0) { dependencyVars.forEach(variable => { @@ -1150,7 +1146,7 @@ function generateJS(ast, options) { const params = dependencyVars.join(", "); return [ - generateGeneratedByComment(), + ...generateGeneratedByComment(), "define(" + dependencies + ", function(" + params + ") {", " \"use strict\";", "", @@ -1164,7 +1160,7 @@ function generateJS(ast, options) { globals() { return [ - generateGeneratedByComment(), + ...generateGeneratedByComment(), "(function(root) {", " \"use strict\";", "", @@ -1177,7 +1173,6 @@ function generateJS(ast, options) { }, umd() { - const parts = []; const dependencyVars = Object.keys(options.dependencies); const dependencyIds = dependencyVars.map(v => options.dependencies[v]); const dependencies = "[" @@ -1190,8 +1185,8 @@ function generateJS(ast, options) { ).join(", "); const params = dependencyVars.join(", "); + const parts = generateGeneratedByComment(); parts.push( - generateGeneratedByComment(), "(function(root, factory) {", " if (typeof define === \"function\" && define.amd) {", " define(" + dependencies + ", factory);", From fa5da4764bb23a64a10772ed96dc2a53b33c7a2c Mon Sep 17 00:00:00 2001 From: Mingun Date: Mon, 31 May 2021 23:21:35 +0500 Subject: [PATCH 06/21] source-map: Remove useless function --- lib/compiler/passes/generate-js.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index e3dc8d65..308e2319 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -1044,23 +1044,6 @@ function generateJS(ast, options) { ].join("\n"); } - function generateParserExports() { - return options.trace - ? [ - "{", - " peg$SyntaxError as SyntaxError,", - " peg$DefaultTracer as DefaultTracer,", - " peg$parse as parse", - "}", - ].join("\n") - : [ - "{", - " peg$SyntaxError as SyntaxError,", - " peg$parse as parse", - "}", - ].join("\n"); - } - const generators = { bare() { return [ @@ -1128,7 +1111,11 @@ function generateJS(ast, options) { parts.push( toplevelCode, "", - "export " + generateParserExports() + ";", + "export {", + " peg$SyntaxError as SyntaxError,", + options.trace ? " peg$DefaultTracer as DefaultTracer," : "", + " peg$parse as parse", + "};", "" ); From d482541793ac2deb37a6c2084e6b6db2fe06a1bb Mon Sep 17 00:00:00 2001 From: Mingun Date: Tue, 1 Jun 2021 01:07:27 +0500 Subject: [PATCH 07/21] source-map: Generate top-level as a SourceNode --- lib/compiler/passes/generate-js.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 308e2319..0bce6f35 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -133,7 +133,7 @@ function generateJS(ast, options) { function buildFunc(a, i) { return wrapInSourceNode( - `\nvar ${f(i)} = function(${a.params.join(", ")}) {`, + `\n var ${f(i)} = function(${a.params.join(", ")}) {`, a.body, a.location, "};" @@ -143,11 +143,11 @@ function generateJS(ast, options) { return new SourceNode( null, null, options.grammarSource, [ ast.literals.map( - (c, i) => "var " + l(i) + " = " + buildLiteral(c) + ";" + (c, i) => " var " + l(i) + " = " + buildLiteral(c) + ";" ).concat("", ast.classes.map( - (c, i) => "var " + r(i) + " = " + buildRegexp(c) + ";" + (c, i) => " var " + r(i) + " = " + buildRegexp(c) + ";" )).concat("", ast.expectations.map( - (c, i) => "var " + e(i) + " = " + buildExpectation(c) + ";" + (c, i) => " var " + e(i) + " = " + buildExpectation(c) + ";" )).concat("").join("\n"), ast.functions.map(buildFunc), ] @@ -808,7 +808,7 @@ function generateJS(ast, options) { " var peg$startRuleFunctions = " + startRuleFunctions + ";", " var peg$startRuleFunction = " + startRuleFunction + ";", "", - indent2(generateTables().toString()), + generateTables(), "", " var peg$currPos = 0;", " var peg$savedPos = 0;", @@ -1015,7 +1015,12 @@ function generateJS(ast, options) { "}" ); - return parts; + return new SourceNode( + // This expression has a better readability when on two lines + // eslint-disable-next-line function-call-argument-newline + null, null, options.grammarSource, + parts.map(s => s instanceof SourceNode ? s : s + "\n") + ); } function generateWrapper(toplevelCode) { @@ -1207,7 +1212,7 @@ function generateJS(ast, options) { return generators[options.format](); } - ast.code = generateWrapper(generateToplevel().join("\n")).join("\n"); + ast.code = generateWrapper(generateToplevel().toString()).join("\n"); } module.exports = generateJS; From 6eece336faea3c5d578847bab7bae86f1363a502 Mon Sep 17 00:00:00 2001 From: Mingun Date: Tue, 1 Jun 2021 01:21:22 +0500 Subject: [PATCH 08/21] source-map: Generate module wrappers as a SourceNode --- lib/compiler/passes/generate-js.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 0bce6f35..60a0eabc 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -1056,7 +1056,7 @@ function generateJS(ast, options) { "(function() {", " \"use strict\";", "", - indent2(toplevelCode), + toplevelCode, "", indent2("return " + generateParserObject() + ";"), "})()", @@ -1088,8 +1088,7 @@ function generateJS(ast, options) { parts.push( toplevelCode, "", - "module.exports = " + generateParserObject() + ";", - "" + "module.exports = " + generateParserObject() + ";" ); return parts; @@ -1120,8 +1119,7 @@ function generateJS(ast, options) { " peg$SyntaxError as SyntaxError,", options.trace ? " peg$DefaultTracer as DefaultTracer," : "", " peg$parse as parse", - "};", - "" + "};" ); return parts; @@ -1142,11 +1140,10 @@ function generateJS(ast, options) { "define(" + dependencies + ", function(" + params + ") {", " \"use strict\";", "", - indent2(toplevelCode), + toplevelCode, "", indent2("return " + generateParserObject() + ";"), "});", - "", ]; }, @@ -1156,11 +1153,10 @@ function generateJS(ast, options) { "(function(root) {", " \"use strict\";", "", - indent2(toplevelCode), + toplevelCode, "", indent2("root." + options.exportVar + " = " + generateParserObject() + ";"), "})(this);", - "", ]; }, @@ -1198,21 +1194,27 @@ function generateJS(ast, options) { "})(this, function(" + params + ") {", " \"use strict\";", "", - indent2(toplevelCode), + toplevelCode, "", indent2("return " + generateParserObject() + ";"), - "});", - "" + "});" ); return parts; }, }; - return generators[options.format](); + const parts = generators[options.format](); + + return new SourceNode( + // This expression has a better readability when on two lines + // eslint-disable-next-line function-call-argument-newline + null, null, options.grammarSource, + parts.map(s => s instanceof SourceNode ? s : s + "\n") + ); } - ast.code = generateWrapper(generateToplevel().toString()).join("\n"); + ast.code = generateWrapper(generateToplevel()).toString(); } module.exports = generateJS; From 8b7d69dd9e4782bd6ade98a0c403e0019bf974bf Mon Sep 17 00:00:00 2001 From: Mingun Date: Tue, 1 Jun 2021 21:50:54 +0500 Subject: [PATCH 09/21] source-map: Represent generated code in the AST as a SourceNode --- lib/compiler/index.js | 4 ++-- lib/compiler/passes/generate-js.js | 2 +- lib/peg.d.ts | 9 +++++++-- test/api/plugin-api.spec.js | 5 ++++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/compiler/index.js b/lib/compiler/index.js index d50e9932..966b5d67 100644 --- a/lib/compiler/index.js +++ b/lib/compiler/index.js @@ -93,10 +93,10 @@ const compiler = { switch (options.output) { case "parser": - return eval(ast.code); + return eval(ast.code.toString()); case "source": - return ast.code; + return ast.code.toString(); default: throw new Error("Invalid output format: " + options.output + "."); diff --git a/lib/compiler/passes/generate-js.js b/lib/compiler/passes/generate-js.js index 60a0eabc..b5ba331e 100644 --- a/lib/compiler/passes/generate-js.js +++ b/lib/compiler/passes/generate-js.js @@ -1214,7 +1214,7 @@ function generateJS(ast, options) { ); } - ast.code = generateWrapper(generateToplevel()).toString(); + ast.code = generateWrapper(generateToplevel()); } module.exports = generateJS; diff --git a/lib/peg.d.ts b/lib/peg.d.ts index 74def43d..5c89a3db 100644 --- a/lib/peg.d.ts +++ b/lib/peg.d.ts @@ -1,5 +1,7 @@ // Based on PEG.js Type Definitions by: vvakame , Tobias Kahlert , C.J. Bell +import { SourceNode } from "source-map"; + /** Interfaces that describe the abstract syntax tree used by Peggy. */ declare namespace ast { /** @@ -50,8 +52,11 @@ declare namespace ast { /** List of all rules in that grammar. */ rules: Rule[]; - /** Added by the `generateJs` pass and contains the JS code. */ - code?: string; + /** + * Added by the `generateJs` pass and contains the JS code and the source + * map for it. + */ + code?: SourceNode; } /** diff --git a/test/api/plugin-api.spec.js b/test/api/plugin-api.spec.js index f64dbb17..68f91d7f 100644 --- a/test/api/plugin-api.spec.js +++ b/test/api/plugin-api.spec.js @@ -1,6 +1,7 @@ "use strict"; const chai = require("chai"); +const { SourceNode } = require("source-map"); const peg = require("../../lib/peg"); const expect = chai.expect; @@ -98,7 +99,9 @@ describe("plugin API", () => { const plugin = { use(config) { function pass(ast) { - ast.code = "({ parse: function() { return 42; } })"; + ast.code = new SourceNode( + 1, 0, "plugin", "({ parse: function() { return 42; } })" + ); } config.passes.generate = [pass]; From b352e92359b7e5a778499d71f8cfcf50f5ec86b9 Mon Sep 17 00:00:00 2001 From: Mingun Date: Sun, 25 Jul 2021 18:39:25 +0500 Subject: [PATCH 10/21] source-map: Don't Repeat Yourself - introduce new interface SourceOptionsBase --- lib/peg.d.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/peg.d.ts b/lib/peg.d.ts index 5c89a3db..c80ba397 100644 --- a/lib/peg.d.ts +++ b/lib/peg.d.ts @@ -890,14 +890,25 @@ export interface BuildOptionsBase { } export interface ParserBuildOptions extends BuildOptionsBase { - /** If set to `"parser"`, the method will return generated parser object; if set to `"source"`, it will return parser source code as a string (default: `"parser"`) */ + /** + * If set to `"parser"`, the method will return generated parser object; + * if set to `"source"`, it will return parser source code as a string + * (default: `"parser"`) + */ output?: "parser"; } -export interface OutputFormatAmdCommonjsEs extends BuildOptionsBase { - /** If set to `"parser"`, the method will return generated parser object; if set to `"source"`, it will return parser source code as a string (default: `"parser"`) */ +/** Base options for all source-generating formats. */ +interface SourceOptionsBase extends BuildOptionsBase { + /** + * If set to `"parser"`, the method will return generated parser object; + * if set to `"source"`, it will return parser source code as a string + * (default: `"parser"`) + */ output: "source"; +} +export interface OutputFormatAmdCommonjsEs extends SourceOptionsBase { /** Format of the generated parser (`"amd"`, `"bare"`, `"commonjs"`, `"es"`, `"globals"`, or `"umd"`); valid only when `output` is set to `"source"` (default: `"bare"`) */ format: "amd" | "commonjs" | "es"; /** @@ -909,10 +920,7 @@ export interface OutputFormatAmdCommonjsEs extends BuildOptionsBase { dependencies?: Dependencies; } -export interface OutputFormatUmd extends BuildOptionsBase { - /** If set to `"parser"`, the method will return generated parser object; if set to `"source"`, it will return parser source code as a string (default: `"parser"`) */ - output: "source"; - +export interface OutputFormatUmd extends SourceOptionsBase { /** Format of the generated parser (`"amd"`, `"bare"`, `"commonjs"`, `"es"`, `"globals"`, or `"umd"`); valid only when `output` is set to `"source"` (default: `"bare"`) */ format: "umd"; /** @@ -930,10 +938,7 @@ export interface OutputFormatUmd extends BuildOptionsBase { exportVar?: string; } -export interface OutputFormatGlobals extends BuildOptionsBase { - /** If set to `"parser"`, the method will return generated parser object; if set to `"source"`, it will return parser source code as a string (default: `"parser"`) */ - output: "source"; - +export interface OutputFormatGlobals extends SourceOptionsBase { /** Format of the generated parser (`"amd"`, `"bare"`, `"commonjs"`, `"es"`, `"globals"`, or `"umd"`); valid only when `output` is set to `"source"` (default: `"bare"`) */ format: "globals"; /** @@ -944,10 +949,7 @@ export interface OutputFormatGlobals extends BuildOptionsBase { exportVar: string; } -export interface OutputFormatBare extends BuildOptionsBase { - /** If set to `"parser"`, the method will return generated parser object; if set to `"source"`, it will return parser source code as a string (default: `"parser"`) */ - output: "source"; - +export interface OutputFormatBare extends SourceOptionsBase { /** Format of the generated parser (`"amd"`, `"bare"`, `"commonjs"`, `"es"`, `"globals"`, or `"umd"`); valid only when `output` is set to `"source"` (default: `"bare"`) */ format?: "bare"; } From c08e4d8e75925ed79c6db06991c37408e7257652 Mon Sep 17 00:00:00 2001 From: Mingun Date: Tue, 1 Jun 2021 22:14:17 +0500 Subject: [PATCH 11/21] source-map: Implement API for producing source maps Regenerate parser, add documentation and tests --- CHANGELOG.md | 5 + README.md | 12 ++ docs/documentation.html | 14 ++ lib/compiler/index.js | 3 +- lib/parser.js | 329 +++++++++++++++++++------------------ lib/peg.d.ts | 77 ++++++++- test/api/pegjs-api.spec.js | 65 ++++++++ test/types/peg.test-d.ts | 48 ++++++ 8 files changed, 386 insertions(+), 167 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c746a11d..3e42507a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ This file documents all notable changes to Peggy. Released: TBD +### Major Changes + +- Add support for generating source maps. + [@Mingun](https://github.com/peggyjs/peggy/pull/163) + ### Minor Changes - New CLI [@hildjj](https://github.com/peggyjs/peggy/pull/167) diff --git a/README.md b/README.md index 32d25ca7..464172f0 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Follow these steps to upgrade: — more powerful than traditional LL(_k_) and LR(_k_) parsers - Usable [from your browser](https://peggyjs.org/online), from the command line, or via JavaScript API +- [Source map](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map) support ## Getting Started @@ -210,8 +211,19 @@ object to `peg.generate`. The following options are supported: object; if set to `"source"`, it will return parser source code as a string (default: `"parser"`) - `plugins` — plugins to use. See the [Plugins API](#plugins-api) section +- `sourceMap` — if set to `true`, the method will return a [`SourceNode`] object + instead of the string; you can get source code by calling `toString()` method + or source code and mapping by calling `toStringWithSourceMap()` method, see + the [`SourceNode`] documentation; valid only when `output` is set to `"source"` + (default: `false`) + + > **Note**: because of bug [source-map/444] you should also set `grammarSource` to + > a not-empty string if you set this value to `true` - `trace` — makes the parser trace its progress (default: `false`) +[`SourceNode`]: https://github.com/mozilla/source-map#sourcenode +[source-map/444]: https://github.com/mozilla/source-map/issues/444 + ## Using the Parser To use the generated parser, call its `parse` method and pass an input string diff --git a/docs/documentation.html b/docs/documentation.html index d9602ec5..65df4444 100644 --- a/docs/documentation.html +++ b/docs/documentation.html @@ -291,6 +291,20 @@

JavaScript API

plugins
Plugins to use. See the Plugins API section.
+
sourceMap
+

If set to true, the method will return a + SourceNode + object instead of the string; you can get source code by calling toString() + method or source code and mapping by calling toStringWithSourceMap() method, + see the SourceNode + documentation; valid only when output is set to "source" + (default: false)

+
+

Note: because of bug source-map/444 you should also set grammarSource to + a not-empty string if you set this value to true

+
+
+
trace
Makes the parser trace its progress (default: false).
diff --git a/lib/compiler/index.js b/lib/compiler/index.js index 966b5d67..bec3b051 100644 --- a/lib/compiler/index.js +++ b/lib/compiler/index.js @@ -71,6 +71,7 @@ const compiler = { exportVar: null, format: "bare", output: "parser", + sourceMap: false, trace: false, }); @@ -96,7 +97,7 @@ const compiler = { return eval(ast.code.toString()); case "source": - return ast.code.toString(); + return options.sourceMap ? ast.code : ast.code.toString(); default: throw new Error("Invalid output format: " + options.output + "."); diff --git a/lib/parser.js b/lib/parser.js index 4185e11e..4917937d 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -22,7 +22,6 @@ "!": "semantic_not" }; - function peg$subclass(child, parent) { function C() { this.constructor = child; } C.prototype = parent.prototype; @@ -333,184 +332,187 @@ function peg$parse(input, options) { var peg$e73 = peg$literalExpectation(";", false); var peg$f0 = function(topLevelInitializer, initializer, rules) { - return { - type: "grammar", - topLevelInitializer, - initializer, - rules, - location: location() - }; + return { + type: "grammar", + topLevelInitializer, + initializer, + rules, + location: location() }; + }; var peg$f1 = function(code) { - return { - type: "top_level_initializer", - code: code[0], - codeLocation: code[1], - location: location() - }; + return { + type: "top_level_initializer", + code: code[0], + codeLocation: code[1], + location: location() }; + }; var peg$f2 = function(code) { - return { - type: "initializer", - code: code[0], - codeLocation: code[1], - location: location() - }; + return { + type: "initializer", + code: code[0], + codeLocation: code[1], + location: location() }; + }; var peg$f3 = function(name, displayName, expression) { - return { - type: "rule", - name: name[0], - nameLocation: name[1], - expression: displayName !== null - ? { - type: "named", - name: displayName, - expression, - location: location() - } - : expression, - location: location() - }; - }; - var peg$f4 = function(head, tail) { - return tail.length > 0 + return { + type: "rule", + name: name[0], + nameLocation: name[1], + expression: displayName !== null ? { - type: "choice", - alternatives: [head].concat(tail), - location: location() - } - : head; - }; - var peg$f5 = function(expression, code) { - return code !== null - ? { - type: "action", + type: "named", + name: displayName, expression, - code: code[0], - codeLocation: code[1], location: location() } - : expression; + : expression, + location: location() }; + }; + var peg$f4 = function(head, tail) { + return tail.length > 0 + ? { + type: "choice", + alternatives: [head].concat(tail), + location: location() + } + : head; + }; + var peg$f5 = function(expression, code) { + return code !== null + ? { + type: "action", + expression, + code: code[0], + codeLocation: code[1], + location: location() + } + : expression; + }; var peg$f6 = function(head, tail) { - return ((tail.length > 0) || (head.type === "labeled" && head.pick)) - ? { - type: "sequence", - elements: [head].concat(tail), - location: location() - } - : head; - }; + return ((tail.length > 0) || (head.type === "labeled" && head.pick)) + ? { + type: "sequence", + elements: [head].concat(tail), + location: location() + } + : head; + }; var peg$f7 = function(pluck, label, expression) { - if (expression.type.startsWith("semantic_")) { - error("\"@\" cannot be used on a semantic predicate", pluck); - } - return { - type: "labeled", - label: label !== null ? label[0] : null, - // Use location of "@" if label is unavailable - labelLocation: label !== null ? label[1] : pluck, - pick: true, - expression, - location: location() - }; + if (expression.type.startsWith("semantic_")) { + error("\"@\" cannot be used on a semantic predicate", pluck); + } + return { + type: "labeled", + label: label !== null ? label[0] : null, + // Use location of "@" if label is unavailable + labelLocation: label !== null ? label[1] : pluck, + pick: true, + expression, + location: location() }; + }; var peg$f8 = function(label, expression) { - return { - type: "labeled", - label: label[0], - labelLocation: label[1], - expression, - location: location() - }; + return { + type: "labeled", + label: label[0], + labelLocation: label[1], + expression, + location: location() }; + }; var peg$f9 = function() { return location(); }; var peg$f10 = function(label) { - if (reservedWords.indexOf(label[0]) >= 0) { - error(`Label can't be a reserved word "${label[0]}"`, label[1]); - } + if (reservedWords.indexOf(label[0]) >= 0) { + error(`Label can't be a reserved word "${label[0]}"`, label[1]); + } - return label; - }; + return label; + }; var peg$f11 = function(operator, expression) { - return { - type: OPS_TO_PREFIXED_TYPES[operator], - expression, - location: location() - }; + return { + type: OPS_TO_PREFIXED_TYPES[operator], + expression, + location: location() }; + }; var peg$f12 = function(expression, operator) { - return { - type: OPS_TO_SUFFIXED_TYPES[operator], - expression, - location: location() - }; + return { + type: OPS_TO_SUFFIXED_TYPES[operator], + expression, + location: location() }; + }; var peg$f13 = function(expression) { - // The purpose of the "group" AST node is just to isolate label scope. We - // don't need to put it around nodes that can't contain any labels or - // nodes that already isolate label scope themselves. This leaves us with - // "labeled" and "sequence". - return expression.type === "labeled" || expression.type === "sequence" - ? { type: "group", expression, location: location() } - : expression; - }; + // The purpose of the "group" AST node is just to isolate label scope. We + // don't need to put it around nodes that can't contain any labels or + // nodes that already isolate label scope themselves. This leaves us with + // "labeled" and "sequence". + return expression.type === "labeled" || expression.type === "sequence" + ? { type: "group", expression, location: location() } + : expression; + }; var peg$f14 = function(name) { - return { type: "rule_ref", name: name[0], location: location() }; - }; + return { type: "rule_ref", name: name[0], location: location() }; + }; var peg$f15 = function(operator, code) { - return { - type: OPS_TO_SEMANTIC_PREDICATE_TYPES[operator], - code: code[0], - codeLocation: code[1], - location: location() - }; + return { + type: OPS_TO_SEMANTIC_PREDICATE_TYPES[operator], + code: code[0], + codeLocation: code[1], + location: location() }; + }; var peg$f16 = function(head, tail) { - return [head + tail.join(""), location()]; - }; + return [head + tail.join(""), location()]; + }; var peg$f17 = function(value, ignoreCase) { - return { - type: "literal", - value, - ignoreCase: ignoreCase !== null, - location: location() - }; + return { + type: "literal", + value, + ignoreCase: ignoreCase !== null, + location: location() }; + }; var peg$f18 = function(chars) { return chars.join(""); }; - var peg$f19 = function(inverted, parts, ignoreCase) { - return { - type: "class", - parts: parts.filter(part => part !== ""), - inverted: inverted !== null, - ignoreCase: ignoreCase !== null, - location: location() - }; - }; - var peg$f20 = function(begin, end) { - if (begin.charCodeAt(0) > end.charCodeAt(0)) { - error( - "Invalid character range: " + text() + "." - ); - } - - return [begin, end]; - }; - var peg$f21 = function() { return ""; }; - var peg$f22 = function() { return "\0"; }; - var peg$f23 = function() { return "\b"; }; - var peg$f24 = function() { return "\f"; }; - var peg$f25 = function() { return "\n"; }; - var peg$f26 = function() { return "\r"; }; - var peg$f27 = function() { return "\t"; }; - var peg$f28 = function() { return "\v"; }; - var peg$f29 = function(digits) { - return String.fromCharCode(parseInt(digits, 16)); + var peg$f19 = function(chars) { return chars.join(""); }; + var peg$f20 = function(inverted, parts, ignoreCase) { + return { + type: "class", + parts: parts.filter(part => part !== ""), + inverted: inverted !== null, + ignoreCase: ignoreCase !== null, + location: location() }; - var peg$f30 = function() { return { type: "any", location: location() }; }; - var peg$f31 = function(code) { return [code, location()]; }; + }; + var peg$f21 = function(begin, end) { + if (begin.charCodeAt(0) > end.charCodeAt(0)) { + error( + "Invalid character range: " + text() + "." + ); + } + return [begin, end]; + }; + var peg$f22 = function() { return ""; }; + var peg$f23 = function() { return "\0"; }; + var peg$f24 = function() { return "\b"; }; + var peg$f25 = function() { return "\f"; }; + var peg$f26 = function() { return "\n"; }; + var peg$f27 = function() { return "\r"; }; + var peg$f28 = function() { return "\t"; }; + var peg$f29 = function() { return "\v"; }; + var peg$f30 = function(digits) { + return String.fromCharCode(parseInt(digits, 16)); + }; + var peg$f31 = function(digits) { + return String.fromCharCode(parseInt(digits, 16)); + }; + var peg$f32 = function() { return { type: "any", location: location() }; }; + var peg$f33 = function(code) { return [code, location()]; }; var peg$currPos = 0; var peg$savedPos = 0; var peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -2042,7 +2044,7 @@ function peg$parse(input, options) { } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f18(s2); + s0 = peg$f19(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2275,7 +2277,7 @@ function peg$parse(input, options) { s5 = null; } peg$savedPos = s0; - s0 = peg$f19(s2, s3, s5); + s0 = peg$f20(s2, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2310,7 +2312,7 @@ function peg$parse(input, options) { s3 = peg$parseClassCharacter(); if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f20(s1, s3); + s0 = peg$f21(s1, s3); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2422,7 +2424,7 @@ function peg$parse(input, options) { s2 = peg$parseLineTerminatorSequence(); if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f21(); + s0 = peg$f22(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2461,7 +2463,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f22(); + s0 = peg$f23(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2529,7 +2531,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f23(); + s1 = peg$f24(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2543,7 +2545,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f24(); + s1 = peg$f25(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2557,7 +2559,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f25(); + s1 = peg$f26(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2571,7 +2573,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f26(); + s1 = peg$f27(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2585,7 +2587,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f27(); + s1 = peg$f28(); } s0 = s1; if (s0 === peg$FAILED) { @@ -2599,7 +2601,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f28(); + s1 = peg$f29(); } s0 = s1; } @@ -2718,7 +2720,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f29(s2); + s0 = peg$f30(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2778,7 +2780,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f29(s2); + s0 = peg$f31(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2832,7 +2834,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f30(); + s1 = peg$f32(); } s0 = s1; @@ -2885,7 +2887,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parseCode(); peg$savedPos = s0; - s1 = peg$f31(s1); + s1 = peg$f33(s1); s0 = s1; return s0; @@ -3373,9 +3375,8 @@ function peg$parse(input, options) { } - // Cannot use Set here because of native IE support. - const reservedWords = options.reservedWords || []; - + // Cannot use Set here because of native IE support. + const reservedWords = options.reservedWords || []; peg$result = peg$startRuleFunction(); diff --git a/lib/peg.d.ts b/lib/peg.d.ts index c80ba397..43324122 100644 --- a/lib/peg.d.ts +++ b/lib/peg.d.ts @@ -726,8 +726,35 @@ export namespace compiler { function compile( ast: ast.Grammar, stages: Stages, - options: SourceBuildOptions + options: SourceBuildOptions & { sourceMap?: false } ): string; + + /** + * Generates a parser source and source map from a specified grammar AST. + * + * Note that not all errors are detected during the generation and some may + * protrude to the generated parser and cause its malfunction. + * + * @param ast Abstract syntax tree of the grammar from a parser + * @param stages List of compilation stages + * @param options Compilation options + * + * @return An object used to obtain a parser source code and source map + * + * @throws {GrammarError} If the AST contains a semantic error, for example, + * duplicated labels + */ + function compile( + ast: ast.Grammar, + stages: Stages, + options: SourceBuildOptions & { sourceMap: true } + ): SourceNode; + + function compile( + ast: ast.Grammar, + stages: Stages, + options: SourceBuildOptions & { sourceMap: boolean } + ): string | SourceNode; } /** Provides information pointing to a location within a source. */ @@ -987,7 +1014,53 @@ export function generate(grammar: string, options?: ParserBuildOptions): Parser; * @throws {GrammarError} If the grammar contains a semantic error, for example, * duplicated labels */ -export function generate(grammar: string, options: SourceBuildOptions): string; +export function generate( + grammar: string, + options: SourceBuildOptions & { sourceMap?: false } +): string; + +/** + * Returns the generated source code and its source map as a `SourceNode` + * object. You can get the generated code and the source map by using a + * `SourceNode` API. Generated code will be in the specified module format. + * + * Note, that `SourceNode.source`s of the generated source map will depend + * on the `options.grammarSource` value. Therefore, value `options.grammarSource` + * will propagate to the `sources` array of the source map. That array MUST + * contains absolute paths or paths, relative to the source map location. + * + * Because at that level we don't known location of the source map, you probably + * will need to fix locations: + * + * ```ts + * const mapDir = path.dirname(generatedParserJsMap); + * const source = peggy.generate(...).toStringWithSourceMap({ + * file: path.relative(mapDir, generatedParserJs), + * }); + * const json = source.map.toJSON(); + * json.sources = json.sources.map(src => { + * return src === null ? null : path.relative(mapDir, src); + * }); + * ``` + * + * @param grammar String in the format described by the meta-grammar in the + * `parser.pegjs` file + * @param options Options that allow you to customize returned parser object + * + * @throws {SyntaxError} If the grammar contains a syntax error, for example, + * an unclosed brace + * @throws {GrammarError} If the grammar contains a semantic error, for example, + * duplicated labels + */ +export function generate( + grammar: string, + options: SourceBuildOptions & { sourceMap: true } +): SourceNode; + +export function generate( + grammar: string, + options: SourceBuildOptions & { sourceMap: boolean } +): string | SourceNode; // Export all exported stuff under a global variable PEG in non-module environments export as namespace PEG; diff --git a/test/api/pegjs-api.spec.js b/test/api/pegjs-api.spec.js index c21d80e9..9d2a5a6e 100644 --- a/test/api/pegjs-api.spec.js +++ b/test/api/pegjs-api.spec.js @@ -4,6 +4,7 @@ const chai = require("chai"); const peg = require("../../lib/peg"); const sinon = require("sinon"); const pkg = require("../../package.json"); +const { SourceMapConsumer } = require("source-map"); const expect = chai.expect; @@ -195,5 +196,69 @@ describe("Peggy API", () => { it("accepts custom options", () => { peg.generate("start = 'a'", { grammarSource: 42 }); }); + + describe("generates source map", () => { + function findLocationOf(input, chunk) { + const offset = input.indexOf(chunk); + let line = 1; + let column = 0; + + for (let i = 0; i < offset; ++i) { + if (input.charCodeAt(i) === 10) { + ++line; + column = 0; + } else { + ++column; + } + } + + return { line, column }; + } + + const GLOBAL_INITIALIZER = "GLOBAL\nINITIALIZER"; + const PER_PARSE_INITIALIZER = "PER-PARSE\nINITIALIZER"; + const NOT_BLOCK = "NOT\nBLOCK"; + const AND_BLOCK = "AND\nBLOCK"; + const ACTION_BLOCK = "ACTION\nBLOCK"; + const SOURCE = ` + {{${GLOBAL_INITIALIZER}}} + {${PER_PARSE_INITIALIZER}} + RULE_1 = !{${NOT_BLOCK}} 'a' rule:RULE_2 {${ACTION_BLOCK}}; + RULE_2 'named' = &{${AND_BLOCK}} @'b'; + `; + + function check(chunk, source, name) { + const node = peg.generate(SOURCE, { + grammarSource: source, + output: "source", + sourceMap: true, + }); + const { code, map } = node.toStringWithSourceMap(); + + const original = findLocationOf(SOURCE, chunk); + const generated = findLocationOf(code, chunk); + + return SourceMapConsumer.fromSourceMap(map).then(consumer => { + expect(consumer.originalPositionFor(generated)) + .to.be.deep.equal(Object.assign(original, { source, name })); + }); + } + + for (const source of [ + // Because of https://github.com/mozilla/source-map/issues/444 this variants not working + // undefined, + // null, + // "", + "-", + ]) { + describe(`with source = ${chai.util.inspect(source)}`, () => { + it("global initializer", () => check(GLOBAL_INITIALIZER, source, "$top_level_initializer")); + it("per-parse initializer", () => check(PER_PARSE_INITIALIZER, source, "$initializer")); + it("action block", () => check(ACTION_BLOCK, source, null)); + it("semantic and predicate", () => check(AND_BLOCK, source, null)); + it("semantic not predicate", () => check(NOT_BLOCK, source, null)); + }); + } + }); }); }); diff --git a/test/types/peg.test-d.ts b/test/types/peg.test-d.ts index 80e08350..9825766f 100644 --- a/test/types/peg.test-d.ts +++ b/test/types/peg.test-d.ts @@ -1,5 +1,6 @@ import * as peggy from "../.."; import tsd, { expectType } from "tsd"; +import { SourceNode } from "source-map"; import formatter from "tsd/dist/lib/formatter"; import { join } from "path"; import { readFileSync } from "fs"; @@ -58,6 +59,53 @@ describe("peg.d.ts", () => { expectType(p2); }); + it("generates a source map", () => { + const p1 = peggy.generate(src, { output: "source" }); + expectType(p1); + + const p2 = peggy.generate(src, { output: "source", sourceMap: false }); + expectType(p2); + + const p3 = peggy.generate(src, { output: "source", sourceMap: true }); + expectType(p3); + + const p4 = peggy.generate(src, { output: "source", sourceMap: true as boolean }); + expectType(p4); + }); + + it("compiles with source map", () => { + const ast = peggy.parser.parse(src); + expectType(ast); + + const p1 = peggy.compiler.compile( + ast, + peggy.compiler.passes, + { output: "source" } + ); + expectType(p1); + + const p2 = peggy.compiler.compile( + ast, + peggy.compiler.passes, + { output: "source", sourceMap: false } + ); + expectType(p2); + + const p3 = peggy.compiler.compile( + ast, + peggy.compiler.passes, + { output: "source", sourceMap: true } + ); + expectType(p3); + + const p4 = peggy.compiler.compile( + ast, + peggy.compiler.passes, + { output: "source", sourceMap: true as boolean } + ); + expectType(p4); + }); + it("creates an AST", () => { const grammar = peggy.parser.parse(src); expectType(grammar); From cadc935defc33f7baa2bd1f20487d90150732ab6 Mon Sep 17 00:00:00 2001 From: Mingun Date: Sat, 21 Aug 2021 00:57:33 +0500 Subject: [PATCH 12/21] source-map: Run CLI tests in the test/cli directory --- test/cli/run.spec.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/cli/run.spec.ts b/test/cli/run.spec.ts index e4441887..777900ec 100644 --- a/test/cli/run.spec.ts +++ b/test/cli/run.spec.ts @@ -64,6 +64,7 @@ function exec(opts: Options = {}) { } const c = spawn(bin, args, { + cwd: __dirname, stdio: "pipe", env, }); @@ -330,15 +331,8 @@ Options: it("handles plugins", async() => { // Plugin, starting with "./" - const plugin = "./" + path.relative( - process.cwd(), - path.resolve(__dirname, "./fixtures/plugin.js") - ); - - const bad = "./" + path.relative( - process.cwd(), - path.resolve(__dirname, "./fixtures/bad.js") - ); + const plugin = "./fixtures/plugin.js"; + const bad = "./fixtures/bad.js"; await expect(exec({ args: [ From ef9f01414aab9a53b1d88fda162eeb50bbb630ee Mon Sep 17 00:00:00 2001 From: Mingun Date: Sun, 17 Oct 2021 02:49:39 +0500 Subject: [PATCH 13/21] source-map: Do not break process until generated code/source map will be written Also exit with error code 2 when CLI fails because of invalid test input and update CLI tests to check exit code --- README.md | 14 +-- bin/peggy.js | 22 +++-- bin/peggy.mjs | 21 +++-- docs/documentation.html | 6 +- test/cli/run.spec.ts | 183 +++++++++++++++++++++++++++------------- 5 files changed, 164 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 464172f0..374b5305 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,15 @@ You can tweak the generated parser with several options: name with extension changed to `.js`, or stdout if no input file is given. - `--plugin ` — makes Peggy use a specified plugin (can be specified multiple times) -- `-t`, `--test ` - Test the parser with the given text, outputting the - result of running the parser against this input -- `-T`, `--test-file ` - Test the parser with the contents of the - given file, outputting the result of running the parser against this input +- `-t`, `--test ` — Test the parser with the given text, outputting the + result of running the parser against this input. + If the input to be tested is not parsed, the CLI will exit with code 2 +- `-T`, `--test-file ` — Test the parser with the contents of the + given file, outputting the result of running the parser against this input. + If the input to be tested is not parsed, the CLI will exit with code 2 - `--trace` — makes the parser trace its progress -- `-v`, `--version` - output the version number -- `-h`, `--help` - display help for command +- `-v`, `--version` — output the version number +- `-h`, `--help` — display help for command If you specify options using `-c ` or `--extra-options-file `, you will need to ensure you are using the correct types. In particular, you may diff --git a/bin/peggy.js b/bin/peggy.js index ac365c0d..564a26cd 100755 --- a/bin/peggy.js +++ b/bin/peggy.js @@ -2689,22 +2689,27 @@ function abort(message) { process.exit(1); } -function abortError(msg, error) { +function showError(msg, error) { console.error(msg); if (typeof error.format === "function") { - abort(error.format([{ + console.error(error.format([{ source: testGrammarSource, text: testText, }])); } else { if (verbose) { - abort(error); + console.error(error); } else { - abort(`Error: ${error.message}`); + console.error(`Error: ${error.message}`); } } } +function abortError(msg, error) { + showError(msg, error); + process.exit(1); +} + function addExtraOptions(json, options) { let extraOptions; @@ -2802,11 +2807,11 @@ const cliOptions = program ) .option( "-t, --test ", - "Test the parser with the given text, outputting the result of running the parser instead of the parser itself" + "Test the parser with the given text, outputting the result of running the parser instead of the parser itself. If the input to be tested is not parsed, the CLI will exit with code 2" ) .option( "-T, --test-file ", - "Test the parser with the contents of the given file, outputting the result of running the parser instead of the parser itself" + "Test the parser with the contents of the given file, outputting the result of running the parser instead of the parser itself. If the input to be tested is not parsed, the CLI will exit with code 2" ) .option("--trace", "Enable tracing in generated parser") .addOption( @@ -3060,7 +3065,10 @@ readStream(inputStream, input => { maxStringLength: Infinity, })); } catch (e) { - abortError("Error parsing test:", e); + showError("Error parsing test:", e); + // We want to wait until source code/source map will be written + process.exitCode = 2; } } }); +//# sourceMappingURL=peggy.js.map diff --git a/bin/peggy.mjs b/bin/peggy.mjs index 7c0dec1b..f5ab9bef 100644 --- a/bin/peggy.mjs +++ b/bin/peggy.mjs @@ -15,22 +15,27 @@ function abort(message) { process.exit(1); } -function abortError(msg, error) { +function showError(msg, error) { console.error(msg); if (typeof error.format === "function") { - abort(error.format([{ + console.error(error.format([{ source: testGrammarSource, text: testText, }])); } else { if (verbose) { - abort(error); + console.error(error); } else { - abort(`Error: ${error.message}`); + console.error(`Error: ${error.message}`); } } } +function abortError(msg, error) { + showError(msg, error); + process.exit(1); +} + function addExtraOptions(json, options) { let extraOptions; @@ -128,11 +133,11 @@ const cliOptions = program ) .option( "-t, --test ", - "Test the parser with the given text, outputting the result of running the parser instead of the parser itself" + "Test the parser with the given text, outputting the result of running the parser instead of the parser itself. If the input to be tested is not parsed, the CLI will exit with code 2" ) .option( "-T, --test-file ", - "Test the parser with the contents of the given file, outputting the result of running the parser instead of the parser itself" + "Test the parser with the contents of the given file, outputting the result of running the parser instead of the parser itself. If the input to be tested is not parsed, the CLI will exit with code 2" ) .option("--trace", "Enable tracing in generated parser") .addOption( @@ -386,7 +391,9 @@ readStream(inputStream, input => { maxStringLength: Infinity, })); } catch (e) { - abortError("Error parsing test:", e); + showError("Error parsing test:", e); + // We want to wait until source code/source map will be written + process.exitCode = 2; } } }); diff --git a/docs/documentation.html b/docs/documentation.html index 65df4444..d5490955 100644 --- a/docs/documentation.html +++ b/docs/documentation.html @@ -176,11 +176,13 @@

Command Line

-t, --test <text>
Test the parser with the given text, outputting the result of running - the parser against this input
+ the parser against this input. + If the input to be tested is not parsed, the CLI will exit with code 2
-T, --test-file <text>
Test the parser with the contents of the given file, outputting the - result of running the parser against this input
+ result of running the parser against this input. + If the input to be tested is not parsed, the CLI will exit with code 2
--trace
Makes the parser trace its progress.
diff --git a/test/cli/run.spec.ts b/test/cli/run.spec.ts index 777900ec..ff831a96 100644 --- a/test/cli/run.spec.ts +++ b/test/cli/run.spec.ts @@ -123,10 +123,14 @@ Options: specified multiple times) -t, --test Test the parser with the given text, outputting the result of running the parser - instead of the parser itself + instead of the parser itself. If the input + to be tested is not parsed, the CLI will + exit with code 2 -T, --test-file Test the parser with the contents of the given file, outputting the result of running - the parser instead of the parser itself + the parser instead of the parser itself. If + the input to be tested is not parsed, the + CLI will exit with code 2 --trace Enable tracing in generated parser -h, --help display help for command `; @@ -140,9 +144,11 @@ Options: }); it("rejects invalid options", async() => { - await expect(exec({ + const result = expect(exec({ args: ["--invalid-option"], - })).rejects.toThrow(ExecError); + })); + await result.rejects.toThrow(ExecError); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("handles start rules", async() => { @@ -153,10 +159,12 @@ Options: /startRuleFunctions = { foo: [^, ]+, bar: [^, ]+, baz: \S+ }/ ); - await expect(exec({ + const result = expect(exec({ args: ["--allowed-start-rules"], stdin: "foo = '1'", - })).rejects.toThrow("option '--allowed-start-rules ' argument missing"); + })); + await result.rejects.toThrow("option '--allowed-start-rules ' argument missing"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("enables caching", async() => { @@ -186,15 +194,19 @@ Options: stdin: "foo = '1' { return new c.Command(); }", })).resolves.toMatch(/jest = require\("jest"\)/); - await expect(exec({ + let result = expect(exec({ args: ["--dependency"], stdin: "foo = '1' { return new c.Command(); }", - })).rejects.toThrow("option '-d, --dependency ' argument missing"); + })); + await result.rejects.toThrow("option '-d, --dependency ' argument missing"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["-d", "c:commander", "--format", "globals"], stdin: "foo = '1' { return new c.Command(); }", - })).rejects.toThrow("Can't use the -d/--dependency option with the \"globals\" module format."); + })); + await result.rejects.toThrow("Can't use the -d/--dependency option with the \"globals\" module format."); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("handles exportVar", async() => { @@ -203,15 +215,19 @@ Options: stdin: "foo = '1'", })).resolves.toMatch(/^\s*root\.football = /m); - await expect(exec({ + let result = expect(exec({ args: ["--export-var"], stdin: "foo = '1'", - })).rejects.toThrow("option '-e, --export-var ' argument missing"); + })); + await result.rejects.toThrow("option '-e, --export-var ' argument missing"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["--export-var", "football"], stdin: "foo = '1'", - })).rejects.toThrow("Can't use the -e/--export-var option with the \"commonjs\" module format."); + })); + await result.rejects.toThrow("Can't use the -e/--export-var option with the \"commonjs\" module format."); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("handles extra options", async() => { @@ -220,20 +236,26 @@ Options: stdin: 'foo = "1"', })).resolves.toMatch(/^define\(/m); - await expect(exec({ + let result = expect(exec({ args: ["--extra-options"], stdin: 'foo = "1"', - })).rejects.toThrow("--extra-options ' argument missing"); + })); + await result.rejects.toThrow("--extra-options ' argument missing"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["--extra-options", "{"], stdin: 'foo = "1"', - })).rejects.toThrow("Error parsing JSON:"); + })); + await result.rejects.toThrow("Error parsing JSON:"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["--extra-options", "1"], stdin: 'foo = "1"', - })).rejects.toThrow("The JSON with extra options has to represent an object."); + })); + await result.rejects.toThrow("The JSON with extra options has to represent an object."); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("handles extra options in a file", async() => { @@ -255,35 +277,47 @@ Options: stdin: foobarbaz, })).resolves.toMatch(/^define\(/m); - await expect(exec({ + let result = expect(exec({ args: ["-c", optFileJS], stdin: "foo = zazzy:'1'", - })).rejects.toThrow("Error: Label can't be a reserved word \"zazzy\""); + })); + await result.rejects.toThrow("Error: Label can't be a reserved word \"zazzy\""); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["-c", optFile, "____ERROR____FILE_DOES_NOT_EXIST"], stdin: "foo = '1'", - })).rejects.toThrow("Do not specify input both on command line and in config file."); + })); + await result.rejects.toThrow("Do not specify input both on command line and in config file."); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["--extra-options-file"], stdin: 'foo = "1"', - })).rejects.toThrow("--extra-options-file ' argument missing"); + })); + await result.rejects.toThrow("--extra-options-file ' argument missing"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["--extra-options-file", "____ERROR____FILE_DOES_NOT_EXIST"], stdin: 'foo = "1"', - })).rejects.toThrow("Can't read from file"); + })); + await result.rejects.toThrow("Can't read from file"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("handles formats", async() => { - await expect(exec({ + let result = expect(exec({ args: ["--format"], - })).rejects.toThrow("option '--format ' argument missing"); + })); + await result.rejects.toThrow("option '--format ' argument missing"); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); - await expect(exec({ + result = expect(exec({ args: ["--format", "BAD_FORMAT"], - })).rejects.toThrow("option '--format ' argument 'BAD_FORMAT' is invalid. Allowed choices are amd, bare, commonjs, es, globals, umd."); + })); + await result.rejects.toThrow("option '--format ' argument 'BAD_FORMAT' is invalid. Allowed choices are amd, bare, commonjs, es, globals, umd."); + await result.rejects.toThrow(expect.objectContaining({ code: 1 })); }); it("doesn't fail with optimize", async() => { @@ -297,10 +331,12 @@ Options: stdin: 'foo = "1"', })).resolves.toMatch(/deprecated/); - await expect(exec({ + const result = expect(exec({ args: ["-O"], stdin: 'foo = "1"', - })).rejects.toThrow("-O, --optimize