Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

regexplain

A zero-dependency Node library and CLI that parses a regular expression into a typed AST, renders it as a deterministic railroad-diagram SVG, and flags catastrophic-backtracking risk from nested quantifiers or overlapping alternation.

So far this ships four pieces: a recursive-descent parser that turns a pattern string into a plain-object AST without using the platform's own RegExp engine to interpret the pattern, a renderer that turns that AST into a deterministic railroad-diagram SVG, a static analysis pass that scores a parsed pattern's catastrophic-backtracking risk, and a library function plus CLI that tie the three together.

Install

npm install

No runtime dependencies — everything is built on the Node standard library.

To use the regexplain command from anywhere, link it globally:

npm link

Usage

import { parse, parseRegExp } from './src/index.js';

const ast = parse('(foo|bar)+baz{2,4}[^x-z]?', 'gi');
// ast.type === 'Pattern'
// ast.flags === 'gi'
// ast.body is a Sequence of the top-level terms

const fromNative = parseRegExp(/\d{3}-\d{4}/);

parse(source, flags) accepts a pattern body (no surrounding slashes) and an optional flag string, and returns a Pattern AST node. parseRegExp(regexp) is a convenience wrapper that takes a native RegExp instance.

The parser understands:

  • literals and escape sequences (\n, \t, \xNN, \uNNNN, \u{NNNN})
  • groups: capturing, non-capturing (?:), named (?<name>), lookahead (?=, ?!) and lookbehind (?<=, ?<!)
  • quantifiers: *, +, ?, {n}, {n,}, {n,m}, and their lazy (?) variants
  • alternation (|)
  • character classes, including ranges, negation, and nested class escapes (\d, \w, \s)
  • anchors (^, $) and boundaries (\b, \B)
  • backreferences, both numbered (\1) and named (\k<name>)

Invalid patterns (unbalanced groups, out-of-order quantifier ranges, quantifiers with nothing to repeat, unknown flags) raise a RegexSyntaxError with the offending position.

See src/ast.js for the full set of node shapes.

Rendering a railroad diagram

import { parse, renderSVG } from './src/index.js';

const svg = renderSVG(parse('(foo|bar)+baz{2,4}[^x-z]?', 'gi'));
// svg is a complete, self-contained `<svg>...</svg>` string

renderSVG(patternNode, options) takes the Pattern AST node produced by parse() (not a raw pattern string) and returns an SVG document as a string. It accepts no dependencies beyond the Node standard library: the layout is computed by a small recursive algorithm that walks the AST once, and every shape is written out as plain SVG markup (<rect>, <line>, <text>, <circle>).

Rendering is deterministic and side-effect free: the same AST always produces byte-identical SVG, and no network or filesystem access happens during rendering. options.padding (default 16) controls the outer margin in pixels.

The diagram follows standard railroad-diagram conventions:

  • literals, ., anchors, character-class escapes, character classes and backreferences are drawn as labelled boxes
  • alternation (|) branches into parallel horizontal tracks that rejoin
  • quantifiers (*, +, ?, {n,m}) draw a loop-back path above the repeated element when it can repeat, and a bypass path below it when it is optional; non-default bounds (e.g. {2,4}) and lazy quantifiers are labelled
  • groups are drawn as a dashed box around their contents, labelled with their kind (group 1, non-capturing, lookahead ?=, ...)

Any text taken from the pattern itself (literal characters, character-class contents, group names) is XML-escaped before being written into the SVG, so a pattern containing <, &, or quote characters cannot break out of the generated markup.

Scoring backtracking risk

import { parse, analyzeBacktrackingRisk } from './src/index.js';

const result = analyzeBacktrackingRisk(parse('(a+)+'));
// result.severity === 'critical'
// result.score is a 0-100 aggregate
// result.findings is an array of { type, severity, message }

analyzeBacktrackingRisk(patternNode) takes the Pattern AST node produced by parse() and returns { score, severity, findings }. It is a static heuristic over the AST, not a simulator: it never runs the pattern against input, so it can only flag shapes known to cause trouble in a backtracking engine, with a plain-language reason for each one. It looks for two of the most common causes of catastrophic or high-order polynomial backtracking:

  • nested repetition - a quantifier whose repeated body contains another quantifier that can also repeat, e.g. (a+)+ or (a*)*. Flagged as critical when both quantifiers are unbounded, high when only one is, and medium when both are bounded (e.g. (a{2,3}){2,3}).
  • ambiguous alternation under repetition - a repeated group whose branches can match the same input, e.g. (a|a)* or (a|ab)+, so the engine must try every matching branch on each repetition before it can rule a position out.

severity is the single worst finding (none, low, medium, high, or critical); score sums each finding's weight, capped at 100, for ranking patterns against each other. Because it approximates rather than exactly resolves what a construct can match, negated character classes, \D/\W/\S, and backreferences are all treated as "could match anything" when checking for overlap - this favours flagging a pattern that turns out to be safe over missing one that is not.

Putting it together: analyse()

import { analyse } from './src/index.js';

const result = analyse('(a+)+');
// result.ast    - the Pattern AST node
// result.risk   - the analyzeBacktrackingRisk() result
// result.svg    - the rendered railroad-diagram SVG string
// result.report - a plain-text summary of the risk findings

analyse(input, options) parses input (a pattern body string, or a native RegExp instance), renders it, and scores its backtracking risk in one call. options.flags sets the flag letters when input is a string (a RegExp's own .flags are used instead); options.svg is forwarded to renderSVG().

CLI: regexplain check <pattern>

$ npx regexplain check '(a+)+'
pattern: (a+)+
flags: (no flags)
risk: critical (score 40/100)
1 finding(s):
  [critical] nested-quantifier: A "+" quantifier repeats inside the body of an outer "+" quantifier - ...
diagram written to /path/to/regexplain.svg

<pattern> may be a bare pattern body ('\d{3}-\d{4}') or a full regex literal including flags ('/\d{3}-\d{4}/g'). Options:

  • --flags <letters> — flag letters to use (ignored for a /.../flags literal)
  • --out <path> — where to write the SVG (default regexplain.svg)
  • --no-svg — skip writing the SVG file
  • -h, --help — show usage

Exits non-zero with a message on stderr if the pattern cannot be parsed.

Golden-file tests

test/golden.test.mjs runs a fixed corpus of patterns (test/fixtures/corpus.mjs, covering literals, every group kind, quantifiers, character classes, anchors, backreferences, and both flagged risk shapes) through analyse() and compares the resulting SVG and plain-text report byte-for-byte against checked-in snapshots under test/fixtures/golden/. This catches unintended drift in the renderer's markup or the risk scorer's wording that per-feature unit tests wouldn't notice because they only assert on small substrings.

After an intentional change to the renderer or risk scorer, regenerate the snapshots and review the diff before committing it:

npm run golden:update

Status

Built autonomously and gated on passing tests: every change here only ships after npm test passes.

About

A zero-dependency Node library and CLI that parses a regular expression into an AST, renders it as a deterministic railroad-diagram SVG, and flags catastrophic-backtracking risk from nested…

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages