Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"regjsgen.js"
],
"scripts": {
"test": "node tests/tests.js",
"test": "node tests/tests.js && node tests/error-tests.js && node tests/export-tests.js",
"coverage": "nyc --reporter=lcov --reporter=text-summary npm run test",
"update-fixtures": "node tests/update-fixtures.js"
},
Expand Down
2 changes: 1 addition & 1 deletion regjsgen.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@
result += '?<!';
break;
default:
throw Error('Invalid behaviour: ' + node.behaviour);
throw Error('Invalid behaviour: ' + node.behavior);
}

result += generateSequence(generate, node.body);
Expand Down
184 changes: 184 additions & 0 deletions tests/error-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
var assert = require('assert');
var generate = require('../regjsgen').generate;

// Node factories.
function value(kind, codePoint) {
return { 'type': 'value', 'kind': kind, 'codePoint': codePoint };
}

function symbol(char) {
return value('symbol', char.codePointAt(0));
}

function identifier(name) {
return { 'type': 'identifier', 'value': name };
}

function group(behavior, body) {
return { 'type': 'group', 'behavior': behavior, 'body': body || [symbol('a')] };
}

function quantifier(body, min, max) {
return { 'type': 'quantifier', 'min': min, 'max': max, 'greedy': true, 'body': [body] };
}

function testThrows(description, node, expected) {
assert.throws(function() {
generate(node);
}, expected);
console.log('PASSED TEST: %s', description);
}

function testGenerates(description, node, expected) {
assert.strictEqual(generate(node), expected);
console.log('PASSED TEST: %s', description);
}

/*--------------------------------------------------------------------------*/

// Unknown node types.
testThrows('unknown node type', { 'type': 'unknown' }, /^Error: Invalid node type: unknown$/);
testThrows('inherited property as node type', { 'type': 'toString' }, /^Error: Invalid node type: toString$/);
testThrows('node without a type', {}, /^Error: Invalid node type: undefined$/);

// Type assertions with a single expected type.
testThrows(
'group name that is not an identifier',
{ 'type': 'group', 'behavior': 'normal', 'name': symbol('a'), 'body': [] },
/^Error: Invalid node type: value; expected type: identifier$/
);
testThrows(
'reference name that is not an identifier',
{ 'type': 'reference', 'name': symbol('a') },
/^Error: Invalid node type: value; expected type: identifier$/
);
testThrows(
'class strings containing a non-`classString` node',
{ 'type': 'classStrings', 'strings': [symbol('a')] },
/^Error: Invalid node type: value; expected type: classString$/
);

// Type assertions with multiple expected types.
testThrows(
'quantifier applied to a disjunction',
quantifier({ 'type': 'disjunction', 'body': [] }, 0, null),
/^Error: Invalid node type: disjunction; expected types: /
);
testThrows(
'alternative containing a nested alternative',
{ 'type': 'alternative', 'body': [symbol('a'), { 'type': 'alternative', 'body': [] }] },
/^Error: Invalid node type: alternative; expected types: /
);
testThrows(
'character class containing a group',
{ 'type': 'characterClass', 'negative': false, 'kind': 'union', 'body': [group('normal')] },
/^Error: Invalid node type: group; expected types: /
);
// The compiled type assertion regex is cached; ensure the cached copy still rejects.
testThrows(
'quantifier applied to a quantifier (cached type assertion)',
quantifier(quantifier(symbol('a'), 0, null), 0, null),
/^Error: Invalid node type: quantifier; expected types: /
);

// Anchors.
testThrows('anchor with an invalid kind', { 'type': 'anchor', 'kind': 'middle' }, /^Error: Invalid assertion$/);

// Character class ranges.
testThrows(
'character class range with a nested range as the minimum',
{
'type': 'characterClassRange',
'min': { 'type': 'characterClassRange', 'min': symbol('a'), 'max': symbol('b') },
'max': symbol('z')
},
/^Error: Invalid character class range$/
);
testThrows(
'character class range with a nested range as the maximum',
{
'type': 'characterClassRange',
'min': symbol('a'),
'max': { 'type': 'characterClassRange', 'min': symbol('y'), 'max': symbol('z') }
},
/^Error: Invalid character class range$/
);

// Groups.
testThrows('group with an invalid behavior', group('sideways'), /^Error: Invalid behaviour: sideways$/);

// Backreferences.
testThrows('reference without an index or name', { 'type': 'reference' }, /^Error: Unknown reference type$/);
testThrows('reference with a zero index', { 'type': 'reference', 'matchIndex': 0 }, /^Error: Unknown reference type$/);

// Values.
testThrows('value without a code point', { 'type': 'value', 'kind': 'symbol' }, /^Error: Invalid code point: undefined$/);
testThrows('value with a string code point', value('symbol', '97'), /^Error: Invalid code point: 97$/);
testThrows('value with an unsupported kind', value('emoji', 97), /^Error: Unsupported node kind: emoji$/);
testThrows('single escape with an invalid code point', value('singleEscape', 0x61), /^Error: Invalid code point: 97$/);

// Invalid code points passed to `fromCodePoint`.
testThrows('symbol with a `NaN` code point', value('symbol', NaN), /^RangeError: Invalid code point: NaN$/);
testThrows('symbol with an infinite code point', value('symbol', Infinity), /^RangeError: Invalid code point: Infinity$/);
testThrows('symbol with a negative code point', value('symbol', -1), /^RangeError: Invalid code point: -1$/);
testThrows('symbol with a code point above U+10FFFF', value('symbol', 0x110000), /^RangeError: Invalid code point: 1114112$/);
testThrows('symbol with a non-integer code point', value('symbol', 97.5), /^RangeError: Invalid code point: 97.5$/);
testThrows('identifier escape with a negative code point', value('identifier', -1), /^RangeError: Invalid code point: -1$/);

/*--------------------------------------------------------------------------*/

// Boundary values that should still be generated.
testGenerates('symbol with the maximum code point', value('symbol', 0x10FFFF), '􏿿');
testGenerates('symbol with code point zero', value('symbol', 0), '\0');
testGenerates('unicode code point escape with the maximum code point', value('unicodeCodePointEscape', 0x10FFFF), '\\u{10FFFF}');
testGenerates('quantifier with `min` and `max` of zero', quantifier(symbol('a'), 0, 0), 'a{0}');
testGenerates('quantifier with `max` of `undefined`', quantifier(symbol('a'), 2, undefined), 'a{2,}');
testGenerates(
'lazy quantifier',
{ 'type': 'quantifier', 'min': 0, 'max': 1, 'greedy': false, 'body': [symbol('a')] },
'a??'
);
testGenerates(
'modifier group with only disabled flags',
{ 'type': 'group', 'behavior': 'ignore', 'modifierFlags': { 'enabling': '', 'disabling': 'ims' }, 'body': [symbol('a')] },
'(?-ims:a)'
);
testGenerates(
'modifier group with only enabled flags',
{ 'type': 'group', 'behavior': 'ignore', 'modifierFlags': { 'enabling': 'ims', 'disabling': '' }, 'body': [symbol('a')] },
'(?ims:a)'
);
testGenerates(
'named group and named backreference',
{
'type': 'alternative',
'body': [
{ 'type': 'group', 'behavior': 'normal', 'name': identifier('foo'), 'body': [symbol('a')] },
{ 'type': 'reference', 'name': identifier('foo') }
]
},
'(?<foo>a)\\k<foo>'
);
testGenerates(
'null escape followed by a digit in a character class',
{ 'type': 'characterClass', 'negative': false, 'kind': 'union', 'body': [value('null', 0), symbol('1')] },
'[\\0001]'
);
testGenerates(
'null escape followed by a digit in a class string',
{
'type': 'classStrings',
'strings': [{ 'type': 'classString', 'characters': [value('null', 0), symbol('9')] }]
},
'\\q{\\0009}'
);
testGenerates(
'null escape followed by a non-digit',
{ 'type': 'alternative', 'body': [value('null', 0), symbol('a')] },
'\\0a'
);
testGenerates(
'null escape at the end of a sequence',
{ 'type': 'alternative', 'body': [symbol('1'), value('null', 0)] },
'1\\0'
);
165 changes: 165 additions & 0 deletions tests/export-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
var assert = require('assert');
var Module = require('module');
var path = require('path');

var filename = path.join(__dirname, '..', 'regjsgen.js');
var realGlobal = global;

// Loads a fresh copy of `regjsgen.js` through the module system (so that
// coverage instrumentation still applies), with the given `exports` object.
function load(exports) {
var mod = new Module(filename, module);
mod.filename = filename;
mod.paths = Module._nodeModulePaths(path.dirname(filename));
if (exports) {
mod.exports = exports;
}
mod.load(filename);
return mod;
}

// Temporarily sets properties on the global object while running `callback`.
function withGlobals(values, callback) {
var names = Object.keys(values),
saved = {};

names.forEach(function(name) {
saved[name] = Object.getOwnPropertyDescriptor(realGlobal, name);
realGlobal[name] = values[name];
});

try {
return callback();
} finally {
names.forEach(function(name) {
if (saved[name]) {
Object.defineProperty(realGlobal, name, saved[name]);
} else {
delete realGlobal[name];
}
});
}
}

function assertIsRegjsgen(regjsgen) {
assert.ok(regjsgen, 'regjsgen should be exported');
assert.strictEqual(typeof regjsgen.generate, 'function');
assert.strictEqual(regjsgen.generate({ 'type': 'dot' }), '.');
}

function test(description, callback) {
callback();
console.log('PASSED TEST: %s', description);
}

// Marking `exports` and `module` as DOM nodes disables CommonJS detection.
function domNodeExports() {
return { 'nodeType': 1 };
}

// Returns a no-op AMD `define`, used to make the module export to its root.
function defineAmd() {
function define() {}
define.amd = {};
return define;
}

/*--------------------------------------------------------------------------*/

test('exports `generate` via CommonJS', function() {
var mod = load();
assertIsRegjsgen(mod.exports);
assert.strictEqual(realGlobal.regjsgen, undefined);
});

test('defines an anonymous AMD module', function() {
var factory;
function define(fn) {
factory = fn;
}
define.amd = {};

withGlobals({ 'define': define }, function() {
var mod = load();
assert.strictEqual(typeof factory, 'function');
assertIsRegjsgen(factory());
// The global object is also assigned to when using AMD.
assert.strictEqual(realGlobal.regjsgen, factory());
// CommonJS exports are skipped when AMD is detected.
assert.strictEqual(mod.exports.generate, undefined);
delete realGlobal.regjsgen;
});
});

test('ignores `define` without `define.amd`', function() {
var called = false;
function define() {
called = true;
}

withGlobals({ 'define': define }, function() {
var mod = load();
assert.strictEqual(called, false);
assertIsRegjsgen(mod.exports);
});
});

test('ignores `define` with a falsy `define.amd`', function() {
var called = false;
function define() {
called = true;
}
define.amd = null;

withGlobals({ 'define': define }, function() {
var mod = load();
assert.strictEqual(called, false);
assertIsRegjsgen(mod.exports);
});
});

test('exports to `this` when CommonJS is unavailable', function() {
var exports = domNodeExports();
load(exports);
assertIsRegjsgen(exports.regjsgen);
assert.strictEqual(exports.generate, undefined);
});

test('exports to `window` when available', function() {
var window = {};
withGlobals({ 'window': window }, function() {
load(domNodeExports());
});
assertIsRegjsgen(window.regjsgen);
});

test('exports to a `global` that references itself via `window`', function() {
var fakeGlobal = {};
fakeGlobal.window = fakeGlobal;

var exports = withGlobals({ 'global': fakeGlobal, 'define': defineAmd() }, function() {
return load().exports;
});
assertIsRegjsgen(fakeGlobal.regjsgen);
assert.strictEqual(exports.regjsgen, undefined);
});

test('exports to a `global` that references itself via `self`', function() {
var fakeGlobal = {};
fakeGlobal.self = fakeGlobal;

withGlobals({ 'global': fakeGlobal, 'define': defineAmd() }, function() {
load();
});
assertIsRegjsgen(fakeGlobal.regjsgen);
});

test('ignores a `global` that does not reference itself', function() {
var fakeGlobal = {};

var exports = withGlobals({ 'global': fakeGlobal, 'define': defineAmd() }, function() {
return load().exports;
});
assert.strictEqual(fakeGlobal.regjsgen, undefined);
assertIsRegjsgen(exports.regjsgen);
});