PEGO

Lesson 7 of 10

Testing and Linting a Grammar

You have been checking grammars by running them on a few inputs. This lesson turns those checks into a suite that you can rerun, and adds two tools that find mistakes without any input: the linter and the formatter.

What You Will Build

A fixture suite for the configuration grammar, a lint run that finds a real mistake, and a formatted grammar file.

Before You Start

Complete Building Typed Trees and Reporting Errors: you need config.pego and robust.pego.

A Fixture Suite

A suite is a JSON file next to the grammar. Each case has an input and the outcome you expect: accepted for a clean match, rejected for a syntax error, and recovered for a match that needed #recover. Save as suite.json:

{
  "version": 1,
  "grammar": "config.pego",
  "backends": ["closure", "bytecode"],
  "equivalence": true,
  "cases": [
    {"name": "empty file", "input": "", "expect": {"outcome": "accepted"}},
    {"name": "typical file", "input": "host = \"localhost\"\nport = 8080\n", "expect": {"outcome": "accepted"}},
    {"name": "comment only", "input": "# nothing here\n", "expect": {"outcome": "accepted"}},
    {"name": "double equals", "input": "port == 8080\n", "expect": {"outcome": "rejected"}},
    {"name": "unterminated string", "input": "name = \"oops", "expect": {"outcome": "rejected"}}
  ]
}
pego test -f suite.json
PASS empty file [closure]
PASS empty file [bytecode]
PASS typical file [closure]
PASS typical file [bytecode]
PASS comment only [closure]
PASS comment only [bytecode]
PASS double equals [closure]
PASS double equals [bytecode]
PASS unterminated string [closure]
PASS unterminated string [bytecode]
PASS empty file [backend equivalence: closure, bytecode]
PASS typical file [backend equivalence: closure, bytecode]
PASS comment only [backend equivalence: closure, bytecode]
PASS double equals [backend equivalence: closure, bytecode]
PASS unterminated string [backend equivalence: closure, bytecode]
all fixtures passed

Each case runs on both backends, and "equivalence": true also requires that the two backends produce the same tree and errors. A failing case prints FAIL with what differed and the command exits with status 1, so the suite works in a continuous integration job as it is. Cases can also pin the tree, the root range and the positions of errors; pego test -update refreshes those expectations from the observed results.

Linting

pego lint examines a grammar without running it. Save a grammar with two mistakes as buggy.pego:

@start
rule main = "a" ws op ws "b" $$
rule op = "<" / "<="
rule ws = " "*
rule unused = "x"
pego lint -g buggy.pego
buggy.pego:3:17: error: alternative 2 (`"<="`) can never match: alternative 1 (`"<"` at 3:11) matches first wherever it could [shadowed-alternative]
	fix: move it before alternative 1
buggy.pego:5:1: warning: rule unused is never used: main does not call it, directly or indirectly [unreachable-rule]
	fix: remove the rule, or call it from a rule that main reaches
pego: lint: 1 error, 1 warning, 0 hints

The first finding is the mistake of the previous lessons in disguise: "<" matches the start of "<=", so the second alternative is unreachable. Each finding has a position, a severity, the name of the check in brackets and a suggested fix. The command exits with status 1 if there is an error, and with -strict also for a warning. The grammars of this tutorial are clean:

pego lint -g config.pego
pego lint -g robust.pego

A run with no findings prints nothing and exits with status 0.

Formatting

pego fmt rewrites a grammar into the canonical layout and keeps comments. -l lists the files whose formatting differs, -w rewrites them in place and -check lists them and exits with status 1, which suits CI:

pego fmt -l config.pego robust.pego
pego fmt -w config.pego

Next Steps

Shipping a Parser turns the grammar into code that other programs can use. The guides Testing Grammars and Linting Grammars cover properties, golden files, fuzzing and every check, and the cookbook has recipes for testing hand-written code against a grammar and linting in CI.