Skip to main content
Module

x/ohm_js/test/test-errors.js

A library and language for building parsers, interpreters, compilers, etc.
Go to Latest
File
'use strict';
// --------------------------------------------------------------------// Imports// --------------------------------------------------------------------
const test = require('ava');const dedent = require('dedent');const fs = require('fs');
const ohm = require('..');
// --------------------------------------------------------------------// Helpers// --------------------------------------------------------------------
function makeRuleWithBody(expr) { return ohm.grammar(`G { start = ${expr}}`);}
// --------------------------------------------------------------------// Tests// --------------------------------------------------------------------
test('match failure', t => { const g = ohm.grammar('G { start = "a" "b" "c" "d" }');
let e = g.match('ab'); t.is(e.failed(), true); t.is(e.succeeded(), false); t.is(e.message, ['Line 1, col 3:', '> 1 | ab', ' ^', 'Expected "c"'].join('\n')); t.is(e.shortMessage, 'Line 1, col 3: expected "c"'); t.is(e.getRightmostFailurePosition(), 2);
e = g.match('abcde'); t.is(e.failed(), true); t.is(e.succeeded(), false); t.is( e.message, ['Line 1, col 5:', '> 1 | abcde', ' ^', 'Expected end of input'].join('\n') ); t.is(e.shortMessage, 'Line 1, col 5: expected end of input'); t.is(e.getRightmostFailurePosition(), 4);
const m = g.match('abcd'); t.is(m.succeeded(), true, 'succeeded() is true for root CST node'); t.is(m.failed(), false, 'failed() is false for root CST node');});
test('undeclared rules', t => { t.throws(() => makeRuleWithBody('undeclaredRule'), { message: /Rule undeclaredRule is not declared in grammar G/, }); const g = makeRuleWithBody('digit'); t.throws(() => g.match('hello world', 'x'), { message: /Rule x is not declared in grammar G/, });});
test('many expressions with nullable operands', t => { t.throws(() => makeRuleWithBody('("a"*)*'), { message: /Nullable expression "a"\* is not allowed inside '\*'/, }); t.throws(() => makeRuleWithBody('("a"?)*'), { message: /Nullable expression "a"\? is not allowed inside '\*'/, }); t.throws(() => makeRuleWithBody('("a"*)+'), { message: /Nullable expression "a"\* is not allowed inside '\+'/, }); t.throws(() => makeRuleWithBody('("a"?)+'), { message: /Nullable expression "a"\? is not allowed inside '\+'/, });
try { makeRuleWithBody('("a"?)*'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 1, col 14: > 1 | G { start = ("a"?)*} ^~~~ Nullable expression "a"? is not allowed inside '*' (possible infinite loop) ` ); }
try { makeRuleWithBody('("a"?)+'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 1, col 14: > 1 | G { start = ("a"?)+} ^~~~ Nullable expression "a"? is not allowed inside '+' (possible infinite loop) ` ); }
t.throws( () => ohm.grammar('G { x = y+ y = undeclaredRule }'), {message: /Rule undeclaredRule is not declared in grammar G/}, 'undeclared rule prevents ManyExprHasNullableOperand check' );
// Dynamic checks for infinite loops. These are needed because our static checks for nullable // expressions inside kleene +s and *s don't catch cases where the expression is or contains one // or more of the rule's parameters.
const g1 = ohm.grammar( 'G { plus<e> = e+ star<e> = e* inf1 = star<""> inf2 = plus<"a"*> }' ); try { g1.match('', 'inf1'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 1, col 29: > 1 | G { plus<e> = e+ star<e> = e* inf1 = star<""> inf2 = plus<"a"*> } ^ Nullable expression "" is not allowed inside '*' (possible infinite loop) Application stack (most recent application last): inf1 star<""> ` ); } try { g1.match('', 'inf2'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 1, col 15: > 1 | G { plus<e> = e+ star<e> = e* inf1 = star<""> inf2 = plus<"a"*> } ^ Nullable expression "a"* is not allowed inside '+' (possible infinite loop) Application stack (most recent application last): inf2 plus<"a"*> ` ); }
const g2 = ohm.grammar('G { Start = ListOf<"a"?, ""> }'); try { g2.match('whatever'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 25, col 13: 24 | NonemptyListOf<elem, sep> > 25 | = elem (sep elem)* ^~~~~~~~ 26 | Nullable expression ("" "a"?) is not allowed inside '*' (possible infinite loop) Application stack (most recent application last): Start ListOf<"a"?,""> NonemptyListOf<"a"?,""> ` ); }});
test('errors from ohm.grammar()', t => { const source = 'G {}\nG2 <: G {}'; try { ohm.grammar(source); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 2, col 1: 1 | G {} > 2 | G2 <: G {} ^ Found more than one grammar definition -- use ohm.grammars() instead. ` ); } t.throws(() => ohm.grammar(''), {message: /Missing grammar/}); t.throws(() => ohm.grammar(' \t\n'), {message: /Missing grammar/});
try { ohm.grammar('G {'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 1, col 4: > 1 | G { ^ Expected "}" ` ); }});
test('unrecognized escape sequences', t => { function testBadEscapeSequence(bes) { try { ohm.grammar('G { start = "hello' + bes + 'world" }'); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, dedent` Line 1, col 19: > 1 | G { start = "hello${bes}world" } ^ Expected "\"" ` ); } } testBadEscapeSequence('\\$'); testBadEscapeSequence('\\!'); testBadEscapeSequence('\\w');});
test('failures are memoized', t => { const g = ohm.grammar(` G { S = ~A "b" -- c1 | A -- c2 A = "a" } `); const e = g.match(''); t.is(e.failed(), true); t.is( e.message, dedent` Line 1, col 1: > 1 | ^ Expected "a" or "b" ` );});
test('multiple MatchResults from the same Matcher', t => { const g = ohm.grammar(fs.readFileSync('test/arithmetic.ohm')); const m = g.matcher(); const r1 = m.replaceInputRange(0, 0, '(1').match(); const r2 = m.replaceInputRange(0, 2, '1+').match(); t.is( r1.message, dedent` Line 1, col 3: > 1 | (1 ^ Expected ")" ` ); t.is( r2.message, dedent` Line 1, col 3: > 1 | 1+ ^ Expected a number or "(" ` );});
test('non-fluffy failures subsume fluffy failures, etc.', t => { const g = ohm.grammar(fs.readFileSync('test/arithmetic.ohm')); const r = g.match('(1'); const failures = r.getRightmostFailures(); t.is(failures.length, 5); t.is(failures[0].getText(), ')'); t.is(failures[0].type, 'string'); t.is(failures[1].getText(), '-'); t.is(failures[1].type, 'string'); t.is(failures[2].getText(), '+'); t.is(failures[2].type, 'string'); t.is(failures[3].getText(), '/'); t.is(failures[3].type, 'string'); t.is(failures[4].getText(), '*'); t.is(failures[4].type, 'string');});
test('memo recs that do not contain the necessary info are deleted properly', t => { const g = ohm.grammar(fs.readFileSync('test/arithmetic.ohm')); const r = g.match('1+2*#3/4'); const failures = r.getRightmostFailures(); t.is(failures.length, 2);});
test('trailing space should not influence the result', t => { const g = ohm.grammar(fs.readFileSync('test/arithmetic.ohm')); const r = g.match('(1 '); const failures = r.getRightmostFailures().filter(failure => !failure.isFluffy()); t.is(failures.length, 1); t.is(failures[0].getText(), ')'); t.is(failures[0].type, 'string');});
test('method name displayed on abstract function failure', t => { const g = ohm.ohmGrammar.superGrammar; const param = g.rules.NonemptyListOf.body.factors[0]; try { param.toFailure(); t.fail('Expected an exception to be thrown'); } catch (e) { t.is( e.message, 'this method toFailure is abstract! (it has no implementation in class Param)' ); }});
test('errors for Not-of-<PExpr>', t => { const notAltG = ohm.grammar('G { start = ~("b" | "c") "d" }'); let r = notAltG.match('b'); t.is(r.failed(), true); t.is(typeof r.message, 'string'); // implicitly requires that r.message not throw t.truthy( /Expected not \("b" or "c"\)/.exec(r.message), 'reasonable failure report for Not-of-Alt' );
const notParamG = ohm.grammar('G {\n' + ' S = Not<"a">\n' + ' Not<elem> = ~elem\n' + '}'); r = notParamG.match('a'); t.is(r.failed(), true); t.is(typeof r.message, 'string'); t.truthy(/Expected not "a"/.exec(r.message), 'reasonable failure report for Not-of-Param');
const notLookaheadG = ohm.grammar('G { start = ~(&"a") "b" }'); r = notLookaheadG.match('a'); t.is(r.failed(), true); t.is(typeof r.message, 'string'); t.truthy( /Expected not "a"/.exec(r.message), 'reasonable failure report for Not-of-Lookahead' );
const notSeqG = ohm.grammar('G { start = ~("a" "b") "c" }'); r = notSeqG.match('ab'); t.is(r.failed(), true); t.is(typeof r.message, 'string'); t.truthy( /Expected not \("a" "b"\)/.exec(r.message), 'reasonable failure report for Not-of-Seq' );
const notIterG = ohm.grammar('G { start = ~("a"*) "b" }'); r = notIterG.match('a'); t.is(r.failed(), true); t.is(typeof r.message, 'string'); t.truthy( /Expected not \("a"\*\)/.exec(r.message), 'reasonable failure report for Not-of-Iter' );});
test('complex match failure', t => { const g = ohm.grammar(` G { start = term* term = rule1 | rule2 | rule3 | rule4 rule1 = int | float rule2 = "#" alnum* rule3 = (~("$" | "_" | "#" | space+ | "\\"") any)+ rule4 = space+ int = digit+ float = int ("." digit+) } `); const r = g.match('fail?"'); t.is(r.failed(), true); t.truthy(/Expected /.exec(r.message), 'Should have a message failure');});
// https://github.com/harc/ohm/pull/357test('wrongNumberOfArguments includes the interval', t => { const message = dedent` Line 4, col 13: 3 | a = alnum > 4 | b = a<x> ^~~~ 5 | } Wrong number of arguments for rule a (expected 0, got 1) `; t.throws( () => { ohm.grammar(` Test { a = alnum b = a<x> } `); }, {message} );});