Repository: peggyjs/peggy
Pull request #605 (2025-04-23 20:56:06)

Ensure that warnings and infos are printed better by the CLI.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66a4adc..f382e10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -108,6 +108,9 @@ Released: TBD (Not before 2025-05-01)
   with `--format es -t foo` will still not work in Deno.
 - Fix a bug with named NEVER_MATCH expressions.
   [#454](https://github.com/peggyjs/peggy/pull/454)
+- Warnings from grammar parsing are now presented more fully to the user in
+  the CLI.  Info messages from grammar parsing are presented the same if in
+  verbose mode. [#605](https://github.com/peggyjs/peggy/pull/605)
 
 ### Documentation
 
diff --git a/bin/peggy-cli.js b/bin/peggy-cli.js
index 09644c9..71c57e8 100644
--- a/bin/peggy-cli.js
+++ b/bin/peggy-cli.js
@@ -15,6 +15,7 @@ const fs = require("fs");
 const path = require("path");
 const peggy = require("../lib/peg.js");
 const util = require("util");
+const GrammarError = require("../lib/grammar-error.js");
 
 exports.CommanderError = CommanderError;
 exports.InvalidArgumentError = InvalidArgumentError;
@@ -189,10 +190,6 @@ class PeggyCLI extends Command {
           parserOptions,
           progOptions,
         } = await refineOptions(this, inputFiles, opts);
-        if (progOptions.verbose) {
-          parserOptions.info = (pass, msg) => PeggyCLI.print(this.std.err, `INFO(${pass}): ${msg}`);
-        }
-        parserOptions.warning = (pass, msg) => PeggyCLI.print(this.std.err, `WARN(${pass}): ${msg}`);
         this.parserOptions = parserOptions;
         this.progOptions = progOptions;
         this.verbose("PARSER OPTIONS:", parserOptions);
@@ -528,6 +525,26 @@ declare function ParseFunction<Options extends ParseOptions<StartRuleNames>>(
       errorText = "parsing grammar";
       this.verbose("CLI", errorText);
 
+      if (this.progOptions.verbose) {
+        this.parserOptions.info = (pass, msg, loc, diag) => {
+          if (loc) {
+            const e = new GrammarError(`INFO(${pass}): ${msg}`, loc, diag);
+            PeggyCLI.print(this.std.err, e.format(sources));
+          } else {
+            PeggyCLI.print(this.std.err, `INFO(${pass}): ${msg}`);
+          }
+        };
+      }
+
+      this.parserOptions.warning = (pass, msg, loc, diag) => {
+        if (loc) {
+          const e = new GrammarError(`WARN(${pass}): ${msg}`, loc, diag);
+          PeggyCLI.print(this.std.err, e.format(sources));
+        } else {
+          PeggyCLI.print(this.std.err, `WARN(${pass}): ${msg}`);
+        }
+      };
+
       const source = peggy.generate(
         sources,
         this.parserOptions
diff --git a/bin/tsconfig.json b/bin/tsconfig.json
index fa70ddf..667c38b 100644
--- a/bin/tsconfig.json
+++ b/bin/tsconfig.json
@@ -1,6 +1,11 @@
 {
   "extends": "../tsconfig-base.json",
-  "include": ["**/*.ts", "**/*.js"],
+  "include": [
+    "**/*.ts",
+    "**/*.js",
+    "../lib/grammar-error.js",
+    "../lib/grammar-location.js"
+  ],
   "compilerOptions": {
     "checkJs": true,
     "target": "ES2021"
diff --git a/lib/grammar-error.js b/lib/grammar-error.js
index 750c7ef..826af08 100644
--- a/lib/grammar-error.js
+++ b/lib/grammar-error.js
@@ -1,30 +1,19 @@
+// @ts-check
 "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
-const setProtoOf = Object.setPrototypeOf
-  || ({ __proto__: [] } instanceof Array
-      && function(d, b) {
-        // eslint-disable-next-line no-proto -- Backward-compatibility
-        d.__proto__ = b;
-      })
-  || function(d, b) {
-    for (const p in b) {
-      if (Object.prototype.hasOwnProperty.call(b, p)) {
-        d[p] = b[p];
-      }
-    }
-  };
-
 // Thrown when the grammar contains an error.
 /** @type {import("./peg").GrammarError} */
-class GrammarError extends Error {
+class GrammarError extends SyntaxError {
+  /**
+   *
+   * @param {string} message
+   * @param {PEG.LocationRange} [location]
+   * @param {PEG.DiagnosticNote[]} [diagnostics]
+   */
   constructor(message, location, diagnostics) {
     super(message);
-    setProtoOf(this, GrammarError.prototype);
     this.name = "GrammarError";
     this.location = location;
     if (diagnostics === undefined) {
@@ -34,7 +23,10 @@ class GrammarError extends Error {
     // All problems if this error is thrown by the plugin and not at stage
     // checking phase
     this.stage = null;
-    this.problems = [["error", message, location, diagnostics]];
+    this.problems = [
+      /** @type {PEG.Problem} */
+      (["error", message, location, diagnostics]),
+    ];
   }
 
   toString() {
@@ -131,8 +123,8 @@ ${"".padEnd(indent)} | ${"".padEnd(s.column - 1)}${"".padEnd(hatLen, "^")}`;
      *
      * @param {import("./peg").Severity} severity Importance of the message
      * @param {string} message Test message of the problem
-     * @param {import("./peg").LocationRange?} location Location of the problem in the source
-     * @param {import("./peg").DiagnosticNote[]} diagnostics Additional notes about the problem
+     * @param {import("./peg").LocationRange} [location] Location of the problem in the source
+     * @param {import("./peg").DiagnosticNote[]} [diagnostics] Additional notes about the problem
      * @returns {string}
      */
     function formatProblem(severity, message, location, diagnostics = []) {
diff --git a/lib/peg.d.ts b/lib/peg.d.ts
index 24ecf3d..ee9e60f 100644
--- a/lib/peg.d.ts
+++ b/lib/peg.d.ts
@@ -443,17 +443,17 @@ type Severity = "error" | "info" | "warning";
 
 type Problem = [
   /** Problem severity. */
-  Severity,
+  severity: Severity,
   /** Diagnostic message. */
-  string,
+  message: string,
   /** Location where message is generated, if applicable. */
-  LocationRange?,
+  location: LocationRange?,
   /** List of additional messages with their locations, if applicable. */
-  DiagnosticNote[]?,
+  diagnostics: DiagnosticNote[]?,
 ];
 
 /** Thrown if the grammar contains a semantic error. */
-export class GrammarError extends Error {
+export class GrammarError extends SyntaxError {
   /** Location of the error in the source. */
   public location?: LocationRange;
 


Test patch:
diff --git a/test/cli/run.spec.ts b/test/cli/run.spec.ts
index 4f802f9..df9cd65 100644
--- a/test/cli/run.spec.ts
+++ b/test/cli/run.spec.ts
@@ -1266,6 +1266,20 @@ bar = '2'
       error: "Error running test",
     });
 
+    // INFO
+    const expectedInfo = `\
+error: INFO(transform): Removing unused rule: "bar"
+ --> stdin:2:1
+  |
+2 | bar='2'
+  | ^^^^^^^
+`.replace(/[()|^]/g, c => `\\${c}`);
+    await exec({
+      args: ["-t", "1", "--verbose"],
+      stdin: "foo='1'\nbar='2'\n",
+      expected: new RegExp(expectedInfo),
+    });
+
     // Abusing template literals to ensure we have a trailing space.
     await exec({
       args: ["-t", ""],
@@ -1355,6 +1369,21 @@ error: Rule "unknownRule" is not defined
     });
   });
 
+  it("handles grammar warnings", async() => {
+    await exec({
+      args: ["-t", "b"],
+      stdin: "foo=('b'*)|1..2|",
+      expected: `\
+error: WARN(check): An expression may not consume any input and may always match 2 times
+ --> stdin:1:5
+  |
+1 | foo=('b'*)|1..2|
+  |     ^^^^^^^^^^^^
+[ [ 'b' ], [] ]
+`,
+    });
+  });
+
   it("handles imports", async() => {
     const lib = path.join(__dirname, "fixtures", "lib.peggy");
     const imps = path.join(__dirname, "fixtures", "imports_peggy.peggy");