Skip to content

Commit

Permalink
Merge pull request #309 from hildjj/grammar-location
Browse files Browse the repository at this point in the history
GrammarLocation
  • Loading branch information
hildjj authored Feb 16, 2023
2 parents fbf9935 + 8590f0b commit 6381e6b
Show file tree
Hide file tree
Showing 18 changed files with 305 additions and 53 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ Released: TBD
editors, from @Mingun
- [#299](https://github.com/peggyjs/peggy/issues/299) Add example grammar for a
[SemVer.org](https://semver.org) semantic version string, from @dselman
- [[#307](https://github.com/peggyjs/peggy/issues/307)] Allow grammars to have
relative offsets into their source files (e.g. if embedded in another doc),
from @hildjj.
- [#308](https://github.com/peggyjs/peggy/pull/308) Add support for reading test data from stdin using `-T -`, from @hildjj.

### Bug Fixes
Expand Down
6 changes: 6 additions & 0 deletions docs/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,12 @@ <h2 id="locations">Locations</h2>
because that string will be used when getting formatted error representation
with <a href="#error-messages"><code>e.format()</code></a>.</p>

<p>For certain special cases, you can use an instance of the
<code>GrammarLocation</code> class as the <code>grammarSource</code>.
<code>GrammarLocation</code> allows you to specify the offset of the grammar
source in another file, such as when that grammar is embedded in a larger
document.</p>

<p>If <code>source</code> is <code>null</code> or <code>undefined</code> it doesn't appear in the formatted messages.
The default value for <code>source</code> is <code>undefined</code>.</p>

Expand Down
2 changes: 1 addition & 1 deletion docs/js/benchmark-bundle.min.js

Large diffs are not rendered by default.

18 changes: 13 additions & 5 deletions docs/js/examples.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/js/test-bundle.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/vendor/peggy/peggy.min.js

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions lib/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ function processOptions(options, defaults) {
return processedOptions;
}

function isSourceMapCapable(target) {
if (typeof target === "string") {
return target.length > 0;
}
return target && (typeof target.offset === "function");
}

const compiler = {
// AST node visitor builder. Useful mainly for plugins which manipulate the
// AST.
Expand Down Expand Up @@ -97,9 +104,8 @@ const compiler = {
// grammarSource is required
if (((options.output === "source-and-map")
|| (options.output === "source-with-inline-map"))
&& ((typeof options.grammarSource !== "string")
|| (options.grammarSource.length === 0))) {
throw new Error("Must provide grammarSource (as a string) in order to generate source maps");
&& !isSourceMapCapable(options.grammarSource)) {
throw new Error("Must provide grammarSource (as a string or GrammarLocation) in order to generate source maps");
}

const session = new Session(options);
Expand Down
49 changes: 30 additions & 19 deletions lib/compiler/passes/generate-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Stack = require("../stack");
const VERSION = require("../../version");
const { stringEscape, regexpClassEscape } = require("../utils");
const { SourceNode } = require("source-map-generator");
const GrammarLocation = require("../../grammar-location");

/**
* Converts source text from the grammar into the `source-map` object
Expand All @@ -19,22 +20,23 @@ const { SourceNode } = require("source-map-generator");
* Code will be splitted by lines if necessary
*/
function toSourceNode(code, location, name) {
const line = location.start.line;
const start = GrammarLocation.offsetStart(location);
const line = start.line;
// `source-map` columns are 0-based, peggy columns is 1-based
const column = location.start.column - 1;
const column = start.column - 1;
const lines = code.split("\n");

if (lines.length === 1) {
return new SourceNode(
line, column, location.source, code, name
line, column, String(location.source), code, name
);
}

return new SourceNode(
null, null, location.source, lines.map((l, i) => new SourceNode(
null, null, String(location.source), lines.map((l, i) => new SourceNode(
line + i,
i === 0 ? column : 0,
location.source,
String(location.source),
i === lines.length - 1 ? l : [l, "\n"],
name
))
Expand All @@ -59,16 +61,17 @@ function wrapInSourceNode(prefix, chunk, location, suffix, name) {
// 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, [
const end = GrammarLocation.offsetEnd(location);
return new SourceNode(null, null, String(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,
end.line,
// `source-map` columns are 0-based, peggy columns is 1-based
location.end.column - 1,
location.source,
end.column - 1,
String(location.source),
suffix
),
]);
Expand Down Expand Up @@ -223,7 +226,7 @@ function generateJS(ast, options) {
"peg$tracer.trace({",
" type: \"rule.enter\",",
" rule: " + ruleNameCode + ",",
" location: peg$computeLocation(startPos, startPos)",
" location: peg$computeLocation(startPos, startPos, true)",
"});",
""
);
Expand All @@ -246,13 +249,13 @@ function generateJS(ast, options) {
" type: \"rule.match\",",
" rule: " + ruleNameCode + ",",
" result: cached.result,",
" location: peg$computeLocation(startPos, peg$currPos)",
" location: peg$computeLocation(startPos, peg$currPos, true)",
" });",
"} else {",
" peg$tracer.trace({",
" type: \"rule.fail\",",
" rule: " + ruleNameCode + ",",
" location: peg$computeLocation(startPos, startPos)",
" location: peg$computeLocation(startPos, startPos, true)",
" });",
"}",
""
Expand Down Expand Up @@ -287,13 +290,13 @@ function generateJS(ast, options) {
" type: \"rule.match\",",
" rule: " + ruleNameCode + ",",
" result: " + resultCode + ",",
" location: peg$computeLocation(startPos, peg$currPos)",
" location: peg$computeLocation(startPos, peg$currPos, true)",
" });",
"} else {",
" peg$tracer.trace({",
" type: \"rule.fail\",",
" rule: " + ruleNameCode + ",",
" location: peg$computeLocation(startPos, startPos)",
" location: peg$computeLocation(startPos, startPos, true)",
" });",
"}"
);
Expand Down Expand Up @@ -707,16 +710,19 @@ function generateJS(ast, options) {
" }",
" }",
" var s = this.location.start;",
" var loc = this.location.source + \":\" + s.line + \":\" + s.column;",
" var offset_s = (this.location.source && (typeof this.location.source.offset === \"function\"))",
" ? this.location.source.offset(s)",
" : s;",
" var loc = this.location.source + \":\" + offset_s.line + \":\" + offset_s.column;",
" if (src) {",
" var e = this.location.end;",
" var filler = peg$padEnd(\"\", s.line.toString().length, ' ');",
" var filler = peg$padEnd(\"\", offset_s.line.toString().length, ' ');",
" var line = src[s.line - 1];",
" var last = s.line === e.line ? e.column : line.length + 1;",
" var hatLen = (last - s.column) || 1;",
" str += \"\\n --> \" + loc + \"\\n\"",
" + filler + \" |\\n\"",
" + s.line + \" | \" + line + \"\\n\"",
" + offset_s.line + \" | \" + line + \"\\n\"",
" + filler + \" | \" + peg$padEnd(\"\", s.column - 1, ' ')",
" + peg$padEnd(\"\", hatLen, \"^\");",
" } else {",
Expand Down Expand Up @@ -1034,11 +1040,11 @@ function generateJS(ast, options) {
" }",
" }",
"",
" function peg$computeLocation(startPos, endPos) {",
" function peg$computeLocation(startPos, endPos, offset) {",
" var startPosDetails = peg$computePosDetails(startPos);",
" var endPosDetails = peg$computePosDetails(endPos);",
"",
" return {",
" var res = {",
" source: peg$source,",
" start: {",
" offset: startPos,",
Expand All @@ -1051,6 +1057,11 @@ function generateJS(ast, options) {
" column: endPosDetails.column",
" }",
" };",
" if (offset && peg$source && (typeof peg$source.offset === \"function\")) {",
" res.start = peg$source.offset(res.start);",
" res.end = peg$source.offset(res.end);",
" }",
" return res;",
" }",
"",
" function peg$fail(expected) {",
Expand Down
15 changes: 9 additions & 6 deletions lib/compiler/stack.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

const { SourceNode } = require("source-map-generator");
const GrammarLocation = require("../grammar-location.js");

/** Utility class that helps generating code for C-like languages. */
class Stack {
Expand Down Expand Up @@ -46,10 +47,11 @@ class Stack {
}

sourceNode(location, chunks, name) {
const start = GrammarLocation.offsetStart(location);
return new SourceNode(
location.start.line,
location.start.column ? location.start.column - 1 : null,
location.source,
start.line,
start.column ? start.column - 1 : null,
String(location.source),
chunks,
name
);
Expand Down Expand Up @@ -272,10 +274,11 @@ class Stack {
: chunk + "\n"
);
if (chunks.length) {
const start = GrammarLocation.offsetStart(location);
parts.push(new SourceNode(
location.start.line,
location.start.column - 1,
location.source,
start.line,
start.column - 1,
String(location.source),
chunks
));
}
Expand Down
13 changes: 9 additions & 4 deletions lib/grammar-error.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict";

const GrammarLocation = require("./grammar-location");

// See: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
// This is roughly what typescript generates, it's not called after super(), where it's needed.
// istanbul ignore next This is a special black magic that cannot be covered everywhere
Expand Down Expand Up @@ -98,6 +100,7 @@ class GrammarError extends Error {
let str = "";
const src = srcLines.find(({ source }) => source === location.source);
const s = location.start;
const offset_s = GrammarLocation.offsetStart(location);
if (src) {
const e = location.end;
const line = src.text[s.line - 1];
Expand All @@ -107,12 +110,12 @@ class GrammarError extends Error {
str += `\nnote: ${message}`;
}
str += `
--> ${location.source}:${s.line}:${s.column}
--> ${location.source}:${offset_s.line}:${offset_s.column}
${"".padEnd(indent)} |
${s.line.toString().padStart(indent)} | ${line}
${offset_s.line.toString().padStart(indent)} | ${line}
${"".padEnd(indent)} | ${"".padEnd(s.column - 1)}${"".padEnd(hatLen, "^")}`;
} else {
str += `\n at ${location.source}:${s.line}:${s.column}`;
str += `\n at ${location.source}:${offset_s.line}:${offset_s.column}`;
if (message) {
str += `: ${message}`;
}
Expand All @@ -135,7 +138,9 @@ ${"".padEnd(indent)} | ${"".padEnd(s.column - 1)}${"".padEnd(hatLen, "^")}`;
let maxLine;
if (location) {
maxLine = diagnostics.reduce(
(t, { location }) => Math.max(t, location.start.line),
(t, { location }) => Math.max(
t, GrammarLocation.offsetStart(location).line
),
location.start.line
);
} else {
Expand Down
Loading

0 comments on commit 6381e6b

Please sign in to comment.