@cucumber/cucumber-expressions
Advanced tools
Comparing version 10.3.0 to 11.0.0
@@ -14,10 +14,15 @@ import ParameterTypeRegistry from './ParameterTypeRegistry'; | ||
constructor(expression: string, parameterTypeRegistry: ParameterTypeRegistry); | ||
private processEscapes; | ||
private processOptional; | ||
private processAlternation; | ||
private processParameters; | ||
private rewriteToRegex; | ||
private static escapeRegex; | ||
private rewriteOptional; | ||
private rewriteAlternation; | ||
private rewriteAlternative; | ||
private rewriteParameter; | ||
private rewriteExpression; | ||
private assertNotEmpty; | ||
private assertNoParameters; | ||
private assertNoOptionals; | ||
match(text: string): ReadonlyArray<Argument<any>>; | ||
get regexp(): RegExp; | ||
get source(): string; | ||
private checkNoParameterType; | ||
} |
"use strict"; | ||
var __values = (this && this.__values) || function(o) { | ||
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; | ||
if (m) return m.call(o); | ||
if (o && typeof o.length === "number") return { | ||
next: function () { | ||
if (o && i >= o.length) o = void 0; | ||
return { value: o && o[i++], done: !o }; | ||
} | ||
}; | ||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -17,19 +6,8 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var ParameterType_1 = __importDefault(require("./ParameterType")); | ||
var TreeRegexp_1 = __importDefault(require("./TreeRegexp")); | ||
var Argument_1 = __importDefault(require("./Argument")); | ||
var Errors_1 = require("./Errors"); | ||
// RegExps with the g flag are stateful in JavaScript. In order to be able | ||
// to reuse them we have to wrap them in a function. | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test | ||
// Does not include (){} characters because they have special meaning | ||
var ESCAPE_REGEXP = function () { return /([\\^[$.|?*+])/g; }; | ||
var PARAMETER_REGEXP = function () { return /(\\\\)?{([^}]*)}/g; }; | ||
var OPTIONAL_REGEXP = function () { return /(\\\\)?\(([^)]+)\)/g; }; | ||
var ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = function () { | ||
return /([^\s^/]+)((\/[^\s^/]+)+)/g; | ||
}; | ||
var DOUBLE_ESCAPE = '\\\\'; | ||
var PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE = 'Parameter types cannot be alternative: '; | ||
var PARAMETER_TYPES_CANNOT_BE_OPTIONAL = 'Parameter types cannot be optional: '; | ||
var CucumberExpressionParser_1 = __importDefault(require("./CucumberExpressionParser")); | ||
var Ast_1 = require("./Ast"); | ||
var ESCAPE_PATTERN = function () { return /([\\^[({$.|?*+})\]])/g; }; | ||
var CucumberExpression = /** @class */ (function () { | ||
@@ -44,66 +22,97 @@ /** | ||
this.parameterTypes = []; | ||
var expr = this.processEscapes(expression); | ||
expr = this.processOptional(expr); | ||
expr = this.processAlternation(expr); | ||
expr = this.processParameters(expr, parameterTypeRegistry); | ||
expr = "^" + expr + "$"; | ||
this.treeRegexp = new TreeRegexp_1.default(expr); | ||
var parser = new CucumberExpressionParser_1.default(); | ||
var ast = parser.parse(expression); | ||
var pattern = this.rewriteToRegex(ast); | ||
this.treeRegexp = new TreeRegexp_1.default(pattern); | ||
} | ||
CucumberExpression.prototype.processEscapes = function (expression) { | ||
return expression.replace(ESCAPE_REGEXP(), '\\$1'); | ||
CucumberExpression.prototype.rewriteToRegex = function (node) { | ||
switch (node.type) { | ||
case Ast_1.NodeType.text: | ||
return CucumberExpression.escapeRegex(node.text()); | ||
case Ast_1.NodeType.optional: | ||
return this.rewriteOptional(node); | ||
case Ast_1.NodeType.alternation: | ||
return this.rewriteAlternation(node); | ||
case Ast_1.NodeType.alternative: | ||
return this.rewriteAlternative(node); | ||
case Ast_1.NodeType.parameter: | ||
return this.rewriteParameter(node); | ||
case Ast_1.NodeType.expression: | ||
return this.rewriteExpression(node); | ||
default: | ||
// Can't happen as long as the switch case is exhaustive | ||
throw new Error(node.type); | ||
} | ||
}; | ||
CucumberExpression.prototype.processOptional = function (expression) { | ||
CucumberExpression.escapeRegex = function (expression) { | ||
return expression.replace(ESCAPE_PATTERN(), '\\$1'); | ||
}; | ||
CucumberExpression.prototype.rewriteOptional = function (node) { | ||
var _this = this; | ||
return expression.replace(OPTIONAL_REGEXP(), function (match, p1, p2) { | ||
if (p1 === DOUBLE_ESCAPE) { | ||
return "\\(" + p2 + "\\)"; | ||
} | ||
_this.checkNoParameterType(p2, PARAMETER_TYPES_CANNOT_BE_OPTIONAL); | ||
return "(?:" + p2 + ")?"; | ||
this.assertNoParameters(node, function (astNode) { | ||
return Errors_1.createParameterIsNotAllowedInOptional(astNode, _this.expression); | ||
}); | ||
this.assertNoOptionals(node, function (astNode) { | ||
return Errors_1.createOptionalIsNotAllowedInOptional(astNode, _this.expression); | ||
}); | ||
this.assertNotEmpty(node, function (astNode) { | ||
return Errors_1.createOptionalMayNotBeEmpty(astNode, _this.expression); | ||
}); | ||
var regex = node.nodes.map(function (node) { return _this.rewriteToRegex(node); }).join(''); | ||
return "(?:" + regex + ")?"; | ||
}; | ||
CucumberExpression.prototype.processAlternation = function (expression) { | ||
CucumberExpression.prototype.rewriteAlternation = function (node) { | ||
var _this = this; | ||
return expression.replace(ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP(), function (match) { | ||
var e_1, _a; | ||
// replace \/ with / | ||
// replace / with | | ||
var replacement = match.replace(/\//g, '|').replace(/\\\|/g, '/'); | ||
if (replacement.indexOf('|') !== -1) { | ||
try { | ||
for (var _b = __values(replacement.split(/\|/)), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var part = _c.value; | ||
_this.checkNoParameterType(part, PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE); | ||
} | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
return "(?:" + replacement + ")"; | ||
// Make sure the alternative parts aren't empty and don't contain parameter types | ||
node.nodes.forEach(function (alternative) { | ||
if (alternative.nodes.length == 0) { | ||
throw Errors_1.createAlternativeMayNotBeEmpty(alternative, _this.expression); | ||
} | ||
else { | ||
return replacement; | ||
} | ||
_this.assertNotEmpty(alternative, function (astNode) { | ||
return Errors_1.createAlternativeMayNotExclusivelyContainOptionals(astNode, _this.expression); | ||
}); | ||
}); | ||
var regex = node.nodes.map(function (node) { return _this.rewriteToRegex(node); }).join('|'); | ||
return "(?:" + regex + ")"; | ||
}; | ||
CucumberExpression.prototype.processParameters = function (expression, parameterTypeRegistry) { | ||
CucumberExpression.prototype.rewriteAlternative = function (node) { | ||
var _this = this; | ||
return expression.replace(PARAMETER_REGEXP(), function (match, p1, p2) { | ||
if (p1 === DOUBLE_ESCAPE) { | ||
return "\\{" + p2 + "\\}"; | ||
} | ||
var typeName = p2; | ||
ParameterType_1.default.checkParameterTypeName(typeName); | ||
var parameterType = parameterTypeRegistry.lookupByTypeName(typeName); | ||
if (!parameterType) { | ||
throw new Errors_1.UndefinedParameterTypeError(typeName); | ||
} | ||
_this.parameterTypes.push(parameterType); | ||
return buildCaptureRegexp(parameterType.regexpStrings); | ||
}); | ||
return node.nodes.map(function (lastNode) { return _this.rewriteToRegex(lastNode); }).join(''); | ||
}; | ||
CucumberExpression.prototype.rewriteParameter = function (node) { | ||
var name = node.text(); | ||
var parameterType = this.parameterTypeRegistry.lookupByTypeName(name); | ||
if (!parameterType) { | ||
throw Errors_1.createUndefinedParameterType(node, this.expression, name); | ||
} | ||
this.parameterTypes.push(parameterType); | ||
var regexps = parameterType.regexpStrings; | ||
if (regexps.length == 1) { | ||
return "(" + regexps[0] + ")"; | ||
} | ||
return "((?:" + regexps.join(')|(?:') + "))"; | ||
}; | ||
CucumberExpression.prototype.rewriteExpression = function (node) { | ||
var _this = this; | ||
var regex = node.nodes.map(function (node) { return _this.rewriteToRegex(node); }).join(''); | ||
return "^" + regex + "$"; | ||
}; | ||
CucumberExpression.prototype.assertNotEmpty = function (node, createNodeWasNotEmptyException) { | ||
var textNodes = node.nodes.filter(function (astNode) { return Ast_1.NodeType.text == astNode.type; }); | ||
if (textNodes.length == 0) { | ||
throw createNodeWasNotEmptyException(node); | ||
} | ||
}; | ||
CucumberExpression.prototype.assertNoParameters = function (node, createNodeContainedAParameterError) { | ||
var parameterNodes = node.nodes.filter(function (astNode) { return Ast_1.NodeType.parameter == astNode.type; }); | ||
if (parameterNodes.length > 0) { | ||
throw createNodeContainedAParameterError(parameterNodes[0]); | ||
} | ||
}; | ||
CucumberExpression.prototype.assertNoOptionals = function (node, createNodeContainedAnOptionalError) { | ||
var parameterNodes = node.nodes.filter(function (astNode) { return Ast_1.NodeType.optional == astNode.type; }); | ||
if (parameterNodes.length > 0) { | ||
throw createNodeContainedAnOptionalError(parameterNodes[0]); | ||
} | ||
}; | ||
CucumberExpression.prototype.match = function (text) { | ||
@@ -126,19 +135,5 @@ return Argument_1.default.build(this.treeRegexp, text, this.parameterTypes); | ||
}); | ||
CucumberExpression.prototype.checkNoParameterType = function (s, message) { | ||
if (s.match(PARAMETER_REGEXP())) { | ||
throw new Errors_1.CucumberExpressionError(message + this.source); | ||
} | ||
}; | ||
return CucumberExpression; | ||
}()); | ||
exports.default = CucumberExpression; | ||
function buildCaptureRegexp(regexps) { | ||
if (regexps.length === 1) { | ||
return "(" + regexps[0] + ")"; | ||
} | ||
var captureGroups = regexps.map(function (group) { | ||
return "(?:" + group + ")"; | ||
}); | ||
return "(" + captureGroups.join('|') + ")"; | ||
} | ||
//# sourceMappingURL=CucumberExpression.js.map |
import ParameterType from './ParameterType'; | ||
import GeneratedExpression from './GeneratedExpression'; | ||
declare class CucumberExpressionError extends Error { | ||
import { Node, Token, TokenType } from './Ast'; | ||
export declare class CucumberExpressionError extends Error { | ||
} | ||
declare class AmbiguousParameterTypeError extends CucumberExpressionError { | ||
export declare function createAlternativeMayNotExclusivelyContainOptionals(node: Node, expression: string): CucumberExpressionError; | ||
export declare function createAlternativeMayNotBeEmpty(node: Node, expression: string): CucumberExpressionError; | ||
export declare function createOptionalMayNotBeEmpty(node: Node, expression: string): CucumberExpressionError; | ||
export declare function createParameterIsNotAllowedInOptional(node: Node, expression: string): CucumberExpressionError; | ||
export declare function createOptionalIsNotAllowedInOptional(node: Node, expression: string): CucumberExpressionError; | ||
export declare function createTheEndOfLIneCanNotBeEscaped(expression: string): CucumberExpressionError; | ||
export declare function createMissingEndToken(expression: string, beginToken: TokenType, endToken: TokenType, current: Token): CucumberExpressionError; | ||
export declare function createAlternationNotAllowedInOptional(expression: string, current: Token): CucumberExpressionError; | ||
export declare function createCantEscaped(expression: string, index: number): CucumberExpressionError; | ||
export declare function createInvalidParameterTypeName(typeName: string): CucumberExpressionError; | ||
export declare function createInvalidParameterTypeNameInNode(token: Token, expression: string): CucumberExpressionError; | ||
export declare class AmbiguousParameterTypeError extends CucumberExpressionError { | ||
static forConstructor(keyName: string, keyValue: string, parameterTypes: ReadonlyArray<ParameterType<any>>, generatedExpressions: ReadonlyArray<GeneratedExpression>): AmbiguousParameterTypeError; | ||
@@ -11,6 +23,6 @@ static forRegExp(parameterTypeRegexp: string, expressionRegexp: RegExp, parameterTypes: ReadonlyArray<ParameterType<any>>, generatedExpressions: ReadonlyArray<GeneratedExpression>): AmbiguousParameterTypeError; | ||
} | ||
declare class UndefinedParameterTypeError extends CucumberExpressionError { | ||
export declare class UndefinedParameterTypeError extends CucumberExpressionError { | ||
readonly undefinedParameterTypeName: string; | ||
constructor(undefinedParameterTypeName: string); | ||
constructor(undefinedParameterTypeName: string, message: string); | ||
} | ||
export { AmbiguousParameterTypeError, UndefinedParameterTypeError, CucumberExpressionError, }; | ||
export declare function createUndefinedParameterType(node: Node, expression: string, undefinedParameterTypeName: string): UndefinedParameterTypeError; |
@@ -6,3 +6,3 @@ "use strict"; | ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || | ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; | ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; | ||
return extendStatics(d, b); | ||
@@ -17,3 +17,4 @@ }; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.CucumberExpressionError = exports.UndefinedParameterTypeError = exports.AmbiguousParameterTypeError = void 0; | ||
exports.createUndefinedParameterType = exports.UndefinedParameterTypeError = exports.AmbiguousParameterTypeError = exports.createInvalidParameterTypeNameInNode = exports.createInvalidParameterTypeName = exports.createCantEscaped = exports.createAlternationNotAllowedInOptional = exports.createMissingEndToken = exports.createTheEndOfLIneCanNotBeEscaped = exports.createOptionalIsNotAllowedInOptional = exports.createParameterIsNotAllowedInOptional = exports.createOptionalMayNotBeEmpty = exports.createAlternativeMayNotBeEmpty = exports.createAlternativeMayNotExclusivelyContainOptionals = exports.CucumberExpressionError = void 0; | ||
var Ast_1 = require("./Ast"); | ||
var CucumberExpressionError = /** @class */ (function (_super) { | ||
@@ -27,2 +28,71 @@ __extends(CucumberExpressionError, _super); | ||
exports.CucumberExpressionError = CucumberExpressionError; | ||
function createAlternativeMayNotExclusivelyContainOptionals(node, expression) { | ||
return new CucumberExpressionError(message(node.start, expression, pointAtLocated(node), 'An alternative may not exclusively contain optionals', "If you did not mean to use an optional you can use '\\(' to escape the the '('")); | ||
} | ||
exports.createAlternativeMayNotExclusivelyContainOptionals = createAlternativeMayNotExclusivelyContainOptionals; | ||
function createAlternativeMayNotBeEmpty(node, expression) { | ||
return new CucumberExpressionError(message(node.start, expression, pointAtLocated(node), 'Alternative may not be empty', "If you did not mean to use an alternative you can use '\\/' to escape the the '/'")); | ||
} | ||
exports.createAlternativeMayNotBeEmpty = createAlternativeMayNotBeEmpty; | ||
function createOptionalMayNotBeEmpty(node, expression) { | ||
return new CucumberExpressionError(message(node.start, expression, pointAtLocated(node), 'An optional must contain some text', "If you did not mean to use an optional you can use '\\(' to escape the the '('")); | ||
} | ||
exports.createOptionalMayNotBeEmpty = createOptionalMayNotBeEmpty; | ||
function createParameterIsNotAllowedInOptional(node, expression) { | ||
return new CucumberExpressionError(message(node.start, expression, pointAtLocated(node), 'An optional may not contain a parameter type', "If you did not mean to use an parameter type you can use '\\{' to escape the the '{'")); | ||
} | ||
exports.createParameterIsNotAllowedInOptional = createParameterIsNotAllowedInOptional; | ||
function createOptionalIsNotAllowedInOptional(node, expression) { | ||
return new CucumberExpressionError(message(node.start, expression, pointAtLocated(node), 'An optional may not contain an other optional', "If you did not mean to use an optional type you can use '\\(' to escape the the '('. For more complicated expressions consider using a regular expression instead.")); | ||
} | ||
exports.createOptionalIsNotAllowedInOptional = createOptionalIsNotAllowedInOptional; | ||
function createTheEndOfLIneCanNotBeEscaped(expression) { | ||
var index = Array.from(expression).length - 1; | ||
return new CucumberExpressionError(message(index, expression, pointAt(index), 'The end of line can not be escaped', "You can use '\\\\' to escape the the '\\'")); | ||
} | ||
exports.createTheEndOfLIneCanNotBeEscaped = createTheEndOfLIneCanNotBeEscaped; | ||
function createMissingEndToken(expression, beginToken, endToken, current) { | ||
var beginSymbol = Ast_1.symbolOf(beginToken); | ||
var endSymbol = Ast_1.symbolOf(endToken); | ||
var purpose = Ast_1.purposeOf(beginToken); | ||
return new CucumberExpressionError(message(current.start, expression, pointAtLocated(current), "The '" + beginSymbol + "' does not have a matching '" + endSymbol + "'", "If you did not intend to use " + purpose + " you can use '\\" + beginSymbol + "' to escape the " + purpose)); | ||
} | ||
exports.createMissingEndToken = createMissingEndToken; | ||
function createAlternationNotAllowedInOptional(expression, current) { | ||
return new CucumberExpressionError(message(current.start, expression, pointAtLocated(current), 'An alternation can not be used inside an optional', "You can use '\\/' to escape the the '/'")); | ||
} | ||
exports.createAlternationNotAllowedInOptional = createAlternationNotAllowedInOptional; | ||
function createCantEscaped(expression, index) { | ||
return new CucumberExpressionError(message(index, expression, pointAt(index), "Only the characters '{', '}', '(', ')', '\\', '/' and whitespace can be escaped", "If you did mean to use an '\\' you can use '\\\\' to escape it")); | ||
} | ||
exports.createCantEscaped = createCantEscaped; | ||
function createInvalidParameterTypeName(typeName) { | ||
return new CucumberExpressionError("Illegal character in parameter name {" + typeName + "}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'"); | ||
} | ||
exports.createInvalidParameterTypeName = createInvalidParameterTypeName; | ||
function createInvalidParameterTypeNameInNode(token, expression) { | ||
return new CucumberExpressionError(message(token.start, expression, pointAtLocated(token), "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", 'Did you mean to use a regular expression?')); | ||
} | ||
exports.createInvalidParameterTypeNameInNode = createInvalidParameterTypeNameInNode; | ||
function message(index, expression, pointer, problem, solution) { | ||
return "This Cucumber Expression has a problem at column " + (index + 1) + ":\n\n" + expression + "\n" + pointer + "\n" + problem + ".\n" + solution; | ||
} | ||
function pointAt(index) { | ||
var pointer = []; | ||
for (var i = 0; i < index; i++) { | ||
pointer.push(' '); | ||
} | ||
pointer.push('^'); | ||
return pointer.join(''); | ||
} | ||
function pointAtLocated(node) { | ||
var pointer = [pointAt(node.start)]; | ||
if (node.start + 1 < node.end) { | ||
for (var i = node.start + 1; i < node.end - 1; i++) { | ||
pointer.push('-'); | ||
} | ||
pointer.push('^'); | ||
} | ||
return pointer.join(''); | ||
} | ||
var AmbiguousParameterTypeError = /** @class */ (function (_super) { | ||
@@ -50,4 +120,4 @@ __extends(AmbiguousParameterTypeError, _super); | ||
__extends(UndefinedParameterTypeError, _super); | ||
function UndefinedParameterTypeError(undefinedParameterTypeName) { | ||
var _this = _super.call(this, "Undefined parameter type {" + undefinedParameterTypeName + "}") || this; | ||
function UndefinedParameterTypeError(undefinedParameterTypeName, message) { | ||
var _this = _super.call(this, message) || this; | ||
_this.undefinedParameterTypeName = undefinedParameterTypeName; | ||
@@ -59,2 +129,6 @@ return _this; | ||
exports.UndefinedParameterTypeError = UndefinedParameterTypeError; | ||
function createUndefinedParameterType(node, expression, undefinedParameterTypeName) { | ||
return new UndefinedParameterTypeError(undefinedParameterTypeName, message(node.start, expression, pointAtLocated(node), "Undefined parameter type '" + undefinedParameterTypeName + "'", "Please register a ParameterType for '" + undefinedParameterTypeName + "'")); | ||
} | ||
exports.createUndefinedParameterType = createUndefinedParameterType; | ||
//# sourceMappingURL=Errors.js.map |
@@ -9,2 +9,3 @@ export default class ParameterType<T> { | ||
static checkParameterTypeName(typeName: string): void; | ||
static isValidParameterTypeName(typeName: string): boolean; | ||
regexpStrings: ReadonlyArray<string>; | ||
@@ -11,0 +12,0 @@ /** |
@@ -56,8 +56,10 @@ "use strict"; | ||
ParameterType.checkParameterTypeName = function (typeName) { | ||
var unescapedTypeName = typeName.replace(UNESCAPE_PATTERN(), '$2'); | ||
var match = unescapedTypeName.match(ILLEGAL_PARAMETER_NAME_PATTERN); | ||
if (match) { | ||
throw new Errors_1.CucumberExpressionError("Illegal character '" + match[1] + "' in parameter name {" + unescapedTypeName + "}"); | ||
if (!this.isValidParameterTypeName(typeName)) { | ||
throw Errors_1.createInvalidParameterTypeName(typeName); | ||
} | ||
}; | ||
ParameterType.isValidParameterTypeName = function (typeName) { | ||
var unescapedTypeName = typeName.replace(UNESCAPE_PATTERN(), '$2'); | ||
return !unescapedTypeName.match(ILLEGAL_PARAMETER_NAME_PATTERN); | ||
}; | ||
ParameterType.prototype.transform = function (thisObj, groupValues) { | ||
@@ -71,5 +73,3 @@ return this.transformFn.apply(thisObj, groupValues); | ||
var array = Array.isArray(regexps) ? regexps : [regexps]; | ||
return array.map(function (r) { | ||
return r instanceof RegExp ? regexpSource(r) : r; | ||
}); | ||
return array.map(function (r) { return (r instanceof RegExp ? regexpSource(r) : r); }); | ||
} | ||
@@ -76,0 +76,0 @@ function regexpSource(regexp) { |
@@ -17,3 +17,3 @@ "use strict"; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
@@ -20,0 +20,0 @@ return result; |
@@ -17,3 +17,3 @@ "use strict"; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
@@ -20,0 +20,0 @@ return result; |
"use strict"; | ||
var __values = (this && this.__values) || function(o) { | ||
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; | ||
if (m) return m.call(o); | ||
if (o && typeof o.length === "number") return { | ||
next: function () { | ||
if (o && i >= o.length) o = void 0; | ||
return { value: o && o[i++], done: !o }; | ||
} | ||
}; | ||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
@@ -21,4 +10,35 @@ return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
var ParameterType_1 = __importDefault(require("../src/ParameterType")); | ||
var fs_1 = __importDefault(require("fs")); | ||
// eslint-disable-next-line node/no-extraneous-import | ||
var js_yaml_1 = __importDefault(require("js-yaml")); // why? | ||
var Errors_1 = require("../src/Errors"); | ||
describe('CucumberExpression', function () { | ||
var e_1, _a; | ||
fs_1.default.readdirSync('testdata/expression').forEach(function (testcase) { | ||
var testCaseData = fs_1.default.readFileSync("testdata/expression/" + testcase, 'utf-8'); | ||
var expectation = js_yaml_1.default.safeLoad(testCaseData); | ||
it("" + testcase, function () { | ||
var parameterTypeRegistry = new ParameterTypeRegistry_1.default(); | ||
if (expectation.exception == undefined) { | ||
var expression = new CucumberExpression_1.default(expectation.expression, parameterTypeRegistry); | ||
var matches = expression.match(expectation.text); | ||
assert_1.default.deepStrictEqual(JSON.parse(JSON.stringify(matches ? matches.map(function (value) { return value.getValue(null); }) : null)), // Removes type information. | ||
JSON.parse(expectation.expected)); | ||
} | ||
else { | ||
assert_1.default.throws(function () { | ||
var expression = new CucumberExpression_1.default(expectation.expression, parameterTypeRegistry); | ||
expression.match(expectation.text); | ||
}, new Errors_1.CucumberExpressionError(expectation.exception)); | ||
} | ||
}); | ||
}); | ||
fs_1.default.readdirSync('testdata/regex').forEach(function (testcase) { | ||
var testCaseData = fs_1.default.readFileSync("testdata/regex/" + testcase, 'utf-8'); | ||
var expectation = js_yaml_1.default.safeLoad(testCaseData); | ||
it("" + testcase, function () { | ||
var parameterTypeRegistry = new ParameterTypeRegistry_1.default(); | ||
var expression = new CucumberExpression_1.default(expectation.expression, parameterTypeRegistry); | ||
assert_1.default.deepStrictEqual(expression.regexp.source, expectation.expected); | ||
}); | ||
}); | ||
it('documents match arguments', function () { | ||
@@ -33,65 +53,2 @@ var parameterTypeRegistry = new ParameterTypeRegistry_1.default(); | ||
}); | ||
it('matches word', function () { | ||
assert_1.default.deepStrictEqual(match('three {word} mice', 'three blind mice'), [ | ||
'blind', | ||
]); | ||
}); | ||
it('matches double quoted string', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', 'three "blind" mice'), [ | ||
'blind', | ||
]); | ||
}); | ||
it('matches multiple double quoted strings', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} and {string} mice', 'three "blind" and "crippled" mice'), ['blind', 'crippled']); | ||
}); | ||
it('matches single quoted string', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', "three 'blind' mice"), [ | ||
'blind', | ||
]); | ||
}); | ||
it('matches multiple single quoted strings', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} and {string} mice', "three 'blind' and 'crippled' mice"), ['blind', 'crippled']); | ||
}); | ||
it('does not match misquoted string', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', 'three "blind\' mice'), null); | ||
}); | ||
it('matches single quoted string with double quotes', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', 'three \'"blind"\' mice'), ['"blind"']); | ||
}); | ||
it('matches double quoted string with single quotes', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', 'three "\'blind\'" mice'), ["'blind'"]); | ||
}); | ||
it('matches double quoted string with escaped double quote', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', 'three "bl\\"nd" mice'), ['bl"nd']); | ||
}); | ||
it('matches single quoted string with escaped single quote', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', "three 'bl\\'nd' mice"), ["bl'nd"]); | ||
}); | ||
it('matches single quoted string with escaped single quote', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', "three 'bl\\'nd' mice"), ["bl'nd"]); | ||
}); | ||
it('matches single quoted empty string as empty string', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', "three '' mice"), ['']); | ||
}); | ||
it('matches double quoted empty string as empty string ', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} mice', 'three "" mice'), ['']); | ||
}); | ||
it('matches single quoted empty string as empty string, along with other strings', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} and {string} mice', "three '' and 'handsome' mice"), ['', 'handsome']); | ||
}); | ||
it('matches double quoted empty string as empty string, along with other strings', function () { | ||
assert_1.default.deepStrictEqual(match('three {string} and {string} mice', 'three "" and "handsome" mice'), ['', 'handsome']); | ||
}); | ||
it('matches escaped parenthesis', function () { | ||
assert_1.default.deepStrictEqual(match('three \\(exceptionally) {string} mice', 'three (exceptionally) "blind" mice'), ['blind']); | ||
}); | ||
it('matches escaped slash', function () { | ||
assert_1.default.deepStrictEqual(match('12\\/2020', '12/2020'), []); | ||
}); | ||
it('matches int', function () { | ||
assert_1.default.deepStrictEqual(match('{int}', '22'), [22]); | ||
}); | ||
it("doesn't match float as int", function () { | ||
assert_1.default.deepStrictEqual(match('{int}', '1.22'), null); | ||
}); | ||
it('matches float', function () { | ||
@@ -132,68 +89,2 @@ assert_1.default.deepStrictEqual(match('{float}', ''), null); | ||
}); | ||
it('matches anonymous', function () { | ||
assert_1.default.deepStrictEqual(match('{}', '0.22'), ['0.22']); | ||
}); | ||
it('throws unknown parameter type', function () { | ||
try { | ||
match('{unknown}', 'something'); | ||
assert_1.default.fail(); | ||
} | ||
catch (expected) { | ||
assert_1.default.strictEqual(expected.message, 'Undefined parameter type {unknown}'); | ||
} | ||
}); | ||
it('does not allow optional parameter types', function () { | ||
try { | ||
match('({int})', '3'); | ||
assert_1.default.fail(); | ||
} | ||
catch (expected) { | ||
assert_1.default.strictEqual(expected.message, 'Parameter types cannot be optional: ({int})'); | ||
} | ||
}); | ||
it('allows escaped optional parameter types', function () { | ||
assert_1.default.deepStrictEqual(match('\\({int})', '(3)'), [3]); | ||
}); | ||
it('does not allow text/parameter type alternation', function () { | ||
try { | ||
match('x/{int}', '3'); | ||
assert_1.default.fail(); | ||
} | ||
catch (expected) { | ||
assert_1.default.strictEqual(expected.message, 'Parameter types cannot be alternative: x/{int}'); | ||
} | ||
}); | ||
it('does not allow parameter type/text alternation', function () { | ||
try { | ||
match('{int}/x', '3'); | ||
assert_1.default.fail(); | ||
} | ||
catch (expected) { | ||
assert_1.default.strictEqual(expected.message, 'Parameter types cannot be alternative: {int}/x'); | ||
} | ||
}); | ||
var _loop_1 = function (c) { | ||
it("does not allow parameter type with " + c, function () { | ||
try { | ||
match("{" + c + "string}", 'something'); | ||
assert_1.default.fail(); | ||
} | ||
catch (expected) { | ||
assert_1.default.strictEqual(expected.message, "Illegal character '" + c + "' in parameter name {" + c + "string}"); | ||
} | ||
}); | ||
}; | ||
try { | ||
for (var _b = __values('[]()$.|?*+'.split('')), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var c = _c.value; | ||
_loop_1(c); | ||
} | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
it('exposes source', function () { | ||
@@ -234,28 +125,2 @@ var expr = 'I have {int} cuke(s)'; | ||
}); | ||
describe('escapes special characters', function () { | ||
var special = ['\\', '[', ']', '^', '$', '.', '|', '?', '*', '+']; | ||
special.forEach(function (character) { | ||
it("escapes " + character, function () { | ||
var expr = "I have {int} cuke(s) and " + character; | ||
var expression = new CucumberExpression_1.default(expr, new ParameterTypeRegistry_1.default()); | ||
var arg1 = expression.match("I have 800 cukes and " + character)[0]; | ||
assert_1.default.strictEqual(arg1.getValue(null), 800); | ||
}); | ||
}); | ||
it("escapes .", function () { | ||
var expr = "I have {int} cuke(s) and ."; | ||
var expression = new CucumberExpression_1.default(expr, new ParameterTypeRegistry_1.default()); | ||
assert_1.default.strictEqual(expression.match("I have 800 cukes and 3"), null); | ||
var arg1 = expression.match("I have 800 cukes and .")[0]; | ||
assert_1.default.strictEqual(arg1.getValue(null), 800); | ||
}); | ||
it("escapes |", function () { | ||
var expr = "I have {int} cuke(s) and a|b"; | ||
var expression = new CucumberExpression_1.default(expr, new ParameterTypeRegistry_1.default()); | ||
assert_1.default.strictEqual(expression.match("I have 800 cukes and a"), null); | ||
assert_1.default.strictEqual(expression.match("I have 800 cukes and b"), null); | ||
var arg1 = expression.match("I have 800 cukes and a|b")[0]; | ||
assert_1.default.strictEqual(arg1.getValue(null), 800); | ||
}); | ||
}); | ||
}); | ||
@@ -262,0 +127,0 @@ var match = function (expression, text) { |
@@ -81,3 +81,5 @@ 'use strict'; | ||
return new ParameterType_1.default('[string]', /.*/, String, function (s) { return s; }, false, true); | ||
}, { message: "Illegal character '[' in parameter name {[string]}" }); | ||
}, { | ||
message: "Illegal character in parameter name {[string]}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", | ||
}); | ||
}); | ||
@@ -84,0 +86,0 @@ it('matches parameters with custom parameter type', function () { |
@@ -41,3 +41,3 @@ "use strict"; | ||
var match = function (expressionText, text) { | ||
var m = /\/(.*)\//.exec(expressionText); | ||
var m = /^\/(.*)\/$/.exec(expressionText); | ||
var expression = m | ||
@@ -44,0 +44,0 @@ ? new RegularExpression_1.default(new RegExp(m[1]), new ParameterTypeRegistry_1.default()) |
@@ -17,3 +17,3 @@ "use strict"; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
@@ -42,8 +42,3 @@ return result; | ||
}); | ||
it('creates an UndefinedParameterTypeExpression', function () { | ||
assert.throws(function () { return expressionFactory.createExpression('{x}'); }, { | ||
message: 'Undefined parameter type {x}', | ||
}); | ||
}); | ||
}); | ||
//# sourceMappingURL=ExpressionFactoryTest.js.map |
@@ -17,3 +17,3 @@ "use strict"; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
@@ -20,0 +20,0 @@ return result; |
@@ -5,3 +5,3 @@ I have {int} cuke(s) | ||
--- | ||
I have {int} cuke(s) and some \[]^$.|?*+ | ||
I have {int} cuke(s) and some \\[]^$.|?*+ | ||
I have 1 cuke and some \[]^$.|?*+ | ||
@@ -41,1 +41,5 @@ [1] | ||
[3.5,42] | ||
--- | ||
I select the {int}st/nd/rd/th Cucumber | ||
I select the 3rd Cucumber | ||
[3] |
{ | ||
"name": "@cucumber/cucumber-expressions", | ||
"version": "10.3.0", | ||
"version": "11.0.0", | ||
"description": "Cucumber Expressions - a simpler alternative to Regular Expressions", | ||
@@ -12,3 +12,3 @@ "main": "dist/src/index.js", | ||
"coverage": "nyc --reporter=html --reporter=text mocha", | ||
"build": "tsc && webpack", | ||
"build": "tsc", | ||
"prepublishOnly": "npm run build" | ||
@@ -33,20 +33,17 @@ }, | ||
"devDependencies": { | ||
"@types/mocha": "^8.0.1", | ||
"@types/node": "^14.0.27", | ||
"@typescript-eslint/eslint-plugin": "^3.8.0", | ||
"@typescript-eslint/parser": "^3.8.0", | ||
"eslint": "^7.6.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"@types/js-yaml": "^3.12.5", | ||
"@types/mocha": "^8.2.0", | ||
"@types/node": "^14.14.12", | ||
"@typescript-eslint/eslint-plugin": "^4.9.1", | ||
"@typescript-eslint/parser": "^4.9.1", | ||
"eslint": "^7.15.0", | ||
"eslint-config-prettier": "^7.0.0", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"eslint-plugin-react": "^7.20.5", | ||
"json-schema": "^0.2.5", | ||
"mocha": "^8.1.1", | ||
"eslint-plugin-prettier": "^3.2.0", | ||
"eslint-plugin-react": "^7.21.5", | ||
"mocha": "^8.2.1", | ||
"nyc": "^15.1.0", | ||
"prettier": "^2.0.5", | ||
"ts-loader": "^8.0.2", | ||
"ts-node": "^8.10.2", | ||
"typescript": "^3.9.7", | ||
"webpack": "^4.44.1", | ||
"webpack-cli": "^3.3.12" | ||
"prettier": "^2.2.1", | ||
"ts-node": "^9.1.1", | ||
"typescript": "^4.1.2" | ||
}, | ||
@@ -53,0 +50,0 @@ "files": [ |
@@ -5,21 +5,17 @@ import ParameterTypeRegistry from './ParameterTypeRegistry' | ||
import Argument from './Argument' | ||
import { CucumberExpressionError, UndefinedParameterTypeError } from './Errors' | ||
import { | ||
createAlternativeMayNotBeEmpty, | ||
createAlternativeMayNotExclusivelyContainOptionals, | ||
createOptionalIsNotAllowedInOptional, | ||
createOptionalMayNotBeEmpty, | ||
createParameterIsNotAllowedInOptional, | ||
createUndefinedParameterType, | ||
CucumberExpressionError, | ||
} from './Errors' | ||
import Expression from './Expression' | ||
import CucumberExpressionParser from './CucumberExpressionParser' | ||
import { Node, NodeType } from './Ast' | ||
// RegExps with the g flag are stateful in JavaScript. In order to be able | ||
// to reuse them we have to wrap them in a function. | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test | ||
const ESCAPE_PATTERN = () => /([\\^[({$.|?*+})\]])/g | ||
// Does not include (){} characters because they have special meaning | ||
const ESCAPE_REGEXP = () => /([\\^[$.|?*+])/g | ||
const PARAMETER_REGEXP = () => /(\\\\)?{([^}]*)}/g | ||
const OPTIONAL_REGEXP = () => /(\\\\)?\(([^)]+)\)/g | ||
const ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = () => | ||
/([^\s^/]+)((\/[^\s^/]+)+)/g | ||
const DOUBLE_ESCAPE = '\\\\' | ||
const PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE = | ||
'Parameter types cannot be alternative: ' | ||
const PARAMETER_TYPES_CANNOT_BE_OPTIONAL = | ||
'Parameter types cannot be optional: ' | ||
export default class CucumberExpression implements Expression { | ||
@@ -37,65 +33,125 @@ private readonly parameterTypes: Array<ParameterType<any>> = [] | ||
) { | ||
let expr = this.processEscapes(expression) | ||
expr = this.processOptional(expr) | ||
expr = this.processAlternation(expr) | ||
expr = this.processParameters(expr, parameterTypeRegistry) | ||
expr = `^${expr}$` | ||
const parser = new CucumberExpressionParser() | ||
const ast = parser.parse(expression) | ||
const pattern = this.rewriteToRegex(ast) | ||
this.treeRegexp = new TreeRegexp(pattern) | ||
} | ||
this.treeRegexp = new TreeRegexp(expr) | ||
private rewriteToRegex(node: Node): string { | ||
switch (node.type) { | ||
case NodeType.text: | ||
return CucumberExpression.escapeRegex(node.text()) | ||
case NodeType.optional: | ||
return this.rewriteOptional(node) | ||
case NodeType.alternation: | ||
return this.rewriteAlternation(node) | ||
case NodeType.alternative: | ||
return this.rewriteAlternative(node) | ||
case NodeType.parameter: | ||
return this.rewriteParameter(node) | ||
case NodeType.expression: | ||
return this.rewriteExpression(node) | ||
default: | ||
// Can't happen as long as the switch case is exhaustive | ||
throw new Error(node.type) | ||
} | ||
} | ||
private processEscapes(expression: string) { | ||
return expression.replace(ESCAPE_REGEXP(), '\\$1') | ||
private static escapeRegex(expression: string) { | ||
return expression.replace(ESCAPE_PATTERN(), '\\$1') | ||
} | ||
private processOptional(expression: string) { | ||
return expression.replace(OPTIONAL_REGEXP(), (match, p1, p2) => { | ||
if (p1 === DOUBLE_ESCAPE) { | ||
return `\\(${p2}\\)` | ||
private rewriteOptional(node: Node): string { | ||
this.assertNoParameters(node, (astNode) => | ||
createParameterIsNotAllowedInOptional(astNode, this.expression) | ||
) | ||
this.assertNoOptionals(node, (astNode) => | ||
createOptionalIsNotAllowedInOptional(astNode, this.expression) | ||
) | ||
this.assertNotEmpty(node, (astNode) => | ||
createOptionalMayNotBeEmpty(astNode, this.expression) | ||
) | ||
const regex = node.nodes.map((node) => this.rewriteToRegex(node)).join('') | ||
return `(?:${regex})?` | ||
} | ||
private rewriteAlternation(node: Node) { | ||
// Make sure the alternative parts aren't empty and don't contain parameter types | ||
node.nodes.forEach((alternative) => { | ||
if (alternative.nodes.length == 0) { | ||
throw createAlternativeMayNotBeEmpty(alternative, this.expression) | ||
} | ||
this.checkNoParameterType(p2, PARAMETER_TYPES_CANNOT_BE_OPTIONAL) | ||
return `(?:${p2})?` | ||
this.assertNotEmpty(alternative, (astNode) => | ||
createAlternativeMayNotExclusivelyContainOptionals( | ||
astNode, | ||
this.expression | ||
) | ||
) | ||
}) | ||
const regex = node.nodes.map((node) => this.rewriteToRegex(node)).join('|') | ||
return `(?:${regex})` | ||
} | ||
private processAlternation(expression: string) { | ||
return expression.replace( | ||
ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP(), | ||
(match) => { | ||
// replace \/ with / | ||
// replace / with | | ||
const replacement = match.replace(/\//g, '|').replace(/\\\|/g, '/') | ||
if (replacement.indexOf('|') !== -1) { | ||
for (const part of replacement.split(/\|/)) { | ||
this.checkNoParameterType( | ||
part, | ||
PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE | ||
) | ||
} | ||
return `(?:${replacement})` | ||
} else { | ||
return replacement | ||
} | ||
} | ||
private rewriteAlternative(node: Node) { | ||
return node.nodes.map((lastNode) => this.rewriteToRegex(lastNode)).join('') | ||
} | ||
private rewriteParameter(node: Node) { | ||
const name = node.text() | ||
const parameterType = this.parameterTypeRegistry.lookupByTypeName(name) | ||
if (!parameterType) { | ||
throw createUndefinedParameterType(node, this.expression, name) | ||
} | ||
this.parameterTypes.push(parameterType) | ||
const regexps = parameterType.regexpStrings | ||
if (regexps.length == 1) { | ||
return `(${regexps[0]})` | ||
} | ||
return `((?:${regexps.join(')|(?:')}))` | ||
} | ||
private rewriteExpression(node: Node) { | ||
const regex = node.nodes.map((node) => this.rewriteToRegex(node)).join('') | ||
return `^${regex}$` | ||
} | ||
private assertNotEmpty( | ||
node: Node, | ||
createNodeWasNotEmptyException: (astNode: Node) => CucumberExpressionError | ||
) { | ||
const textNodes = node.nodes.filter( | ||
(astNode) => NodeType.text == astNode.type | ||
) | ||
if (textNodes.length == 0) { | ||
throw createNodeWasNotEmptyException(node) | ||
} | ||
} | ||
private processParameters( | ||
expression: string, | ||
parameterTypeRegistry: ParameterTypeRegistry | ||
private assertNoParameters( | ||
node: Node, | ||
createNodeContainedAParameterError: ( | ||
astNode: Node | ||
) => CucumberExpressionError | ||
) { | ||
return expression.replace(PARAMETER_REGEXP(), (match, p1, p2) => { | ||
if (p1 === DOUBLE_ESCAPE) { | ||
return `\\{${p2}\\}` | ||
} | ||
const parameterNodes = node.nodes.filter( | ||
(astNode) => NodeType.parameter == astNode.type | ||
) | ||
if (parameterNodes.length > 0) { | ||
throw createNodeContainedAParameterError(parameterNodes[0]) | ||
} | ||
} | ||
const typeName = p2 | ||
ParameterType.checkParameterTypeName(typeName) | ||
const parameterType = parameterTypeRegistry.lookupByTypeName(typeName) | ||
if (!parameterType) { | ||
throw new UndefinedParameterTypeError(typeName) | ||
} | ||
this.parameterTypes.push(parameterType) | ||
return buildCaptureRegexp(parameterType.regexpStrings) | ||
}) | ||
private assertNoOptionals( | ||
node: Node, | ||
createNodeContainedAnOptionalError: ( | ||
astNode: Node | ||
) => CucumberExpressionError | ||
) { | ||
const parameterNodes = node.nodes.filter( | ||
(astNode) => NodeType.optional == astNode.type | ||
) | ||
if (parameterNodes.length > 0) { | ||
throw createNodeContainedAnOptionalError(parameterNodes[0]) | ||
} | ||
} | ||
@@ -114,20 +170,2 @@ | ||
} | ||
private checkNoParameterType(s: string, message: string) { | ||
if (s.match(PARAMETER_REGEXP())) { | ||
throw new CucumberExpressionError(message + this.source) | ||
} | ||
} | ||
} | ||
function buildCaptureRegexp(regexps: ReadonlyArray<string>) { | ||
if (regexps.length === 1) { | ||
return `(${regexps[0]})` | ||
} | ||
const captureGroups = regexps.map((group) => { | ||
return `(?:${group})` | ||
}) | ||
return `(${captureGroups.join('|')})` | ||
} |
import ParameterType from './ParameterType' | ||
import GeneratedExpression from './GeneratedExpression' | ||
import { Located, Node, purposeOf, symbolOf, Token, TokenType } from './Ast' | ||
class CucumberExpressionError extends Error {} | ||
export class CucumberExpressionError extends Error {} | ||
class AmbiguousParameterTypeError extends CucumberExpressionError { | ||
export function createAlternativeMayNotExclusivelyContainOptionals( | ||
node: Node, | ||
expression: string | ||
): CucumberExpressionError { | ||
return new CucumberExpressionError( | ||
message( | ||
node.start, | ||
expression, | ||
pointAtLocated(node), | ||
'An alternative may not exclusively contain optionals', | ||
"If you did not mean to use an optional you can use '\\(' to escape the the '('" | ||
) | ||
) | ||
} | ||
export function createAlternativeMayNotBeEmpty( | ||
node: Node, | ||
expression: string | ||
): CucumberExpressionError { | ||
return new CucumberExpressionError( | ||
message( | ||
node.start, | ||
expression, | ||
pointAtLocated(node), | ||
'Alternative may not be empty', | ||
"If you did not mean to use an alternative you can use '\\/' to escape the the '/'" | ||
) | ||
) | ||
} | ||
export function createOptionalMayNotBeEmpty( | ||
node: Node, | ||
expression: string | ||
): CucumberExpressionError { | ||
return new CucumberExpressionError( | ||
message( | ||
node.start, | ||
expression, | ||
pointAtLocated(node), | ||
'An optional must contain some text', | ||
"If you did not mean to use an optional you can use '\\(' to escape the the '('" | ||
) | ||
) | ||
} | ||
export function createParameterIsNotAllowedInOptional( | ||
node: Node, | ||
expression: string | ||
): CucumberExpressionError { | ||
return new CucumberExpressionError( | ||
message( | ||
node.start, | ||
expression, | ||
pointAtLocated(node), | ||
'An optional may not contain a parameter type', | ||
"If you did not mean to use an parameter type you can use '\\{' to escape the the '{'" | ||
) | ||
) | ||
} | ||
export function createOptionalIsNotAllowedInOptional( | ||
node: Node, | ||
expression: string | ||
): CucumberExpressionError { | ||
return new CucumberExpressionError( | ||
message( | ||
node.start, | ||
expression, | ||
pointAtLocated(node), | ||
'An optional may not contain an other optional', | ||
"If you did not mean to use an optional type you can use '\\(' to escape the the '('. For more complicated expressions consider using a regular expression instead." | ||
) | ||
) | ||
} | ||
export function createTheEndOfLIneCanNotBeEscaped( | ||
expression: string | ||
): CucumberExpressionError { | ||
const index = Array.from(expression).length - 1 | ||
return new CucumberExpressionError( | ||
message( | ||
index, | ||
expression, | ||
pointAt(index), | ||
'The end of line can not be escaped', | ||
"You can use '\\\\' to escape the the '\\'" | ||
) | ||
) | ||
} | ||
export function createMissingEndToken( | ||
expression: string, | ||
beginToken: TokenType, | ||
endToken: TokenType, | ||
current: Token | ||
) { | ||
const beginSymbol = symbolOf(beginToken) | ||
const endSymbol = symbolOf(endToken) | ||
const purpose = purposeOf(beginToken) | ||
return new CucumberExpressionError( | ||
message( | ||
current.start, | ||
expression, | ||
pointAtLocated(current), | ||
`The '${beginSymbol}' does not have a matching '${endSymbol}'`, | ||
`If you did not intend to use ${purpose} you can use '\\${beginSymbol}' to escape the ${purpose}` | ||
) | ||
) | ||
} | ||
export function createAlternationNotAllowedInOptional( | ||
expression: string, | ||
current: Token | ||
) { | ||
return new CucumberExpressionError( | ||
message( | ||
current.start, | ||
expression, | ||
pointAtLocated(current), | ||
'An alternation can not be used inside an optional', | ||
"You can use '\\/' to escape the the '/'" | ||
) | ||
) | ||
} | ||
export function createCantEscaped(expression: string, index: number) { | ||
return new CucumberExpressionError( | ||
message( | ||
index, | ||
expression, | ||
pointAt(index), | ||
"Only the characters '{', '}', '(', ')', '\\', '/' and whitespace can be escaped", | ||
"If you did mean to use an '\\' you can use '\\\\' to escape it" | ||
) | ||
) | ||
} | ||
export function createInvalidParameterTypeName(typeName: string) { | ||
return new CucumberExpressionError( | ||
`Illegal character in parameter name {${typeName}}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'` | ||
) | ||
} | ||
export function createInvalidParameterTypeNameInNode( | ||
token: Token, | ||
expression: string | ||
) { | ||
return new CucumberExpressionError( | ||
message( | ||
token.start, | ||
expression, | ||
pointAtLocated(token), | ||
"Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", | ||
'Did you mean to use a regular expression?' | ||
) | ||
) | ||
} | ||
function message( | ||
index: number, | ||
expression: string, | ||
pointer: any, | ||
problem: string, | ||
solution: string | ||
): string { | ||
return `This Cucumber Expression has a problem at column ${index + 1}: | ||
${expression} | ||
${pointer} | ||
${problem}. | ||
${solution}` | ||
} | ||
function pointAt(index: number): string { | ||
const pointer: Array<string> = [] | ||
for (let i = 0; i < index; i++) { | ||
pointer.push(' ') | ||
} | ||
pointer.push('^') | ||
return pointer.join('') | ||
} | ||
function pointAtLocated(node: Located): string { | ||
const pointer = [pointAt(node.start)] | ||
if (node.start + 1 < node.end) { | ||
for (let i = node.start + 1; i < node.end - 1; i++) { | ||
pointer.push('-') | ||
} | ||
pointer.push('^') | ||
} | ||
return pointer.join('') | ||
} | ||
export class AmbiguousParameterTypeError extends CucumberExpressionError { | ||
public static forConstructor( | ||
@@ -52,12 +243,26 @@ keyName: string, | ||
class UndefinedParameterTypeError extends CucumberExpressionError { | ||
constructor(public readonly undefinedParameterTypeName: string) { | ||
super(`Undefined parameter type {${undefinedParameterTypeName}}`) | ||
export class UndefinedParameterTypeError extends CucumberExpressionError { | ||
constructor( | ||
public readonly undefinedParameterTypeName: string, | ||
message: string | ||
) { | ||
super(message) | ||
} | ||
} | ||
export { | ||
AmbiguousParameterTypeError, | ||
UndefinedParameterTypeError, | ||
CucumberExpressionError, | ||
export function createUndefinedParameterType( | ||
node: Node, | ||
expression: string, | ||
undefinedParameterTypeName: string | ||
) { | ||
return new UndefinedParameterTypeError( | ||
undefinedParameterTypeName, | ||
message( | ||
node.start, | ||
expression, | ||
pointAtLocated(node), | ||
`Undefined parameter type '${undefinedParameterTypeName}'`, | ||
`Please register a ParameterType for '${undefinedParameterTypeName}'` | ||
) | ||
) | ||
} |
@@ -1,2 +0,5 @@ | ||
import { CucumberExpressionError } from './Errors' | ||
import { | ||
createInvalidParameterTypeName, | ||
CucumberExpressionError, | ||
} from './Errors' | ||
@@ -7,3 +10,3 @@ const ILLEGAL_PARAMETER_NAME_PATTERN = /([[\]()$.|?*+])/ | ||
export default class ParameterType<T> { | ||
private transformFn: (...match: ReadonlyArray<string>) => T | ||
private transformFn: (...match: readonly string[]) => T | ||
@@ -21,11 +24,12 @@ public static compare(pt1: ParameterType<any>, pt2: ParameterType<any>) { | ||
public static checkParameterTypeName(typeName: string) { | ||
const unescapedTypeName = typeName.replace(UNESCAPE_PATTERN(), '$2') | ||
const match = unescapedTypeName.match(ILLEGAL_PARAMETER_NAME_PATTERN) | ||
if (match) { | ||
throw new CucumberExpressionError( | ||
`Illegal character '${match[1]}' in parameter name {${unescapedTypeName}}` | ||
) | ||
if (!this.isValidParameterTypeName(typeName)) { | ||
throw createInvalidParameterTypeName(typeName) | ||
} | ||
} | ||
public static isValidParameterTypeName(typeName: string) { | ||
const unescapedTypeName = typeName.replace(UNESCAPE_PATTERN(), '$2') | ||
return !unescapedTypeName.match(ILLEGAL_PARAMETER_NAME_PATTERN) | ||
} | ||
public regexpStrings: ReadonlyArray<string> | ||
@@ -72,9 +76,9 @@ | ||
type StringOrRegexp = string | RegExp | ||
function stringArray( | ||
regexps: ReadonlyArray<RegExp> | ReadonlyArray<string> | RegExp | string | ||
regexps: readonly StringOrRegexp[] | StringOrRegexp | ||
): string[] { | ||
const array = Array.isArray(regexps) ? regexps : [regexps] | ||
return array.map((r: RegExp | string) => | ||
r instanceof RegExp ? regexpSource(r) : r | ||
) | ||
return array.map((r) => (r instanceof RegExp ? regexpSource(r) : r)) | ||
} | ||
@@ -81,0 +85,0 @@ |
@@ -5,4 +5,62 @@ import assert from 'assert' | ||
import ParameterType from '../src/ParameterType' | ||
import fs from 'fs' | ||
// eslint-disable-next-line node/no-extraneous-import | ||
import yaml from 'js-yaml' // why? | ||
import { CucumberExpressionError } from '../src/Errors' | ||
interface Expectation { | ||
expression: string | ||
text: string | ||
expected?: string | ||
exception?: string | ||
} | ||
describe('CucumberExpression', () => { | ||
fs.readdirSync('testdata/expression').forEach((testcase) => { | ||
const testCaseData = fs.readFileSync( | ||
`testdata/expression/${testcase}`, | ||
'utf-8' | ||
) | ||
const expectation = yaml.safeLoad(testCaseData) as Expectation | ||
it(`${testcase}`, () => { | ||
const parameterTypeRegistry = new ParameterTypeRegistry() | ||
if (expectation.exception == undefined) { | ||
const expression = new CucumberExpression( | ||
expectation.expression, | ||
parameterTypeRegistry | ||
) | ||
const matches = expression.match(expectation.text) | ||
assert.deepStrictEqual( | ||
JSON.parse( | ||
JSON.stringify( | ||
matches ? matches.map((value) => value.getValue(null)) : null | ||
) | ||
), // Removes type information. | ||
JSON.parse(expectation.expected) | ||
) | ||
} else { | ||
assert.throws(() => { | ||
const expression = new CucumberExpression( | ||
expectation.expression, | ||
parameterTypeRegistry | ||
) | ||
expression.match(expectation.text) | ||
}, new CucumberExpressionError(expectation.exception)) | ||
} | ||
}) | ||
}) | ||
fs.readdirSync('testdata/regex').forEach((testcase) => { | ||
const testCaseData = fs.readFileSync(`testdata/regex/${testcase}`, 'utf-8') | ||
const expectation = yaml.safeLoad(testCaseData) as Expectation | ||
it(`${testcase}`, () => { | ||
const parameterTypeRegistry = new ParameterTypeRegistry() | ||
const expression = new CucumberExpression( | ||
expectation.expression, | ||
parameterTypeRegistry | ||
) | ||
assert.deepStrictEqual(expression.regexp.source, expectation.expected) | ||
}) | ||
}) | ||
it('documents match arguments', () => { | ||
@@ -19,126 +77,2 @@ const parameterTypeRegistry = new ParameterTypeRegistry() | ||
it('matches word', () => { | ||
assert.deepStrictEqual(match('three {word} mice', 'three blind mice'), [ | ||
'blind', | ||
]) | ||
}) | ||
it('matches double quoted string', () => { | ||
assert.deepStrictEqual(match('three {string} mice', 'three "blind" mice'), [ | ||
'blind', | ||
]) | ||
}) | ||
it('matches multiple double quoted strings', () => { | ||
assert.deepStrictEqual( | ||
match( | ||
'three {string} and {string} mice', | ||
'three "blind" and "crippled" mice' | ||
), | ||
['blind', 'crippled'] | ||
) | ||
}) | ||
it('matches single quoted string', () => { | ||
assert.deepStrictEqual(match('three {string} mice', "three 'blind' mice"), [ | ||
'blind', | ||
]) | ||
}) | ||
it('matches multiple single quoted strings', () => { | ||
assert.deepStrictEqual( | ||
match( | ||
'three {string} and {string} mice', | ||
"three 'blind' and 'crippled' mice" | ||
), | ||
['blind', 'crippled'] | ||
) | ||
}) | ||
it('does not match misquoted string', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} mice', 'three "blind\' mice'), | ||
null | ||
) | ||
}) | ||
it('matches single quoted string with double quotes', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} mice', 'three \'"blind"\' mice'), | ||
['"blind"'] | ||
) | ||
}) | ||
it('matches double quoted string with single quotes', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} mice', 'three "\'blind\'" mice'), | ||
["'blind'"] | ||
) | ||
}) | ||
it('matches double quoted string with escaped double quote', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} mice', 'three "bl\\"nd" mice'), | ||
['bl"nd'] | ||
) | ||
}) | ||
it('matches single quoted string with escaped single quote', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} mice', "three 'bl\\'nd' mice"), | ||
["bl'nd"] | ||
) | ||
}) | ||
it('matches single quoted string with escaped single quote', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} mice', "three 'bl\\'nd' mice"), | ||
["bl'nd"] | ||
) | ||
}) | ||
it('matches single quoted empty string as empty string', () => { | ||
assert.deepStrictEqual(match('three {string} mice', "three '' mice"), ['']) | ||
}) | ||
it('matches double quoted empty string as empty string ', () => { | ||
assert.deepStrictEqual(match('three {string} mice', 'three "" mice'), ['']) | ||
}) | ||
it('matches single quoted empty string as empty string, along with other strings', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} and {string} mice', "three '' and 'handsome' mice"), | ||
['', 'handsome'] | ||
) | ||
}) | ||
it('matches double quoted empty string as empty string, along with other strings', () => { | ||
assert.deepStrictEqual( | ||
match('three {string} and {string} mice', 'three "" and "handsome" mice'), | ||
['', 'handsome'] | ||
) | ||
}) | ||
it('matches escaped parenthesis', () => { | ||
assert.deepStrictEqual( | ||
match( | ||
'three \\(exceptionally) {string} mice', | ||
'three (exceptionally) "blind" mice' | ||
), | ||
['blind'] | ||
) | ||
}) | ||
it('matches escaped slash', () => { | ||
assert.deepStrictEqual(match('12\\/2020', '12/2020'), []) | ||
}) | ||
it('matches int', () => { | ||
assert.deepStrictEqual(match('{int}', '22'), [22]) | ||
}) | ||
it("doesn't match float as int", () => { | ||
assert.deepStrictEqual(match('{int}', '1.22'), null) | ||
}) | ||
it('matches float', () => { | ||
@@ -183,69 +117,2 @@ assert.deepStrictEqual(match('{float}', ''), null) | ||
it('matches anonymous', () => { | ||
assert.deepStrictEqual(match('{}', '0.22'), ['0.22']) | ||
}) | ||
it('throws unknown parameter type', () => { | ||
try { | ||
match('{unknown}', 'something') | ||
assert.fail() | ||
} catch (expected) { | ||
assert.strictEqual(expected.message, 'Undefined parameter type {unknown}') | ||
} | ||
}) | ||
it('does not allow optional parameter types', () => { | ||
try { | ||
match('({int})', '3') | ||
assert.fail() | ||
} catch (expected) { | ||
assert.strictEqual( | ||
expected.message, | ||
'Parameter types cannot be optional: ({int})' | ||
) | ||
} | ||
}) | ||
it('allows escaped optional parameter types', () => { | ||
assert.deepStrictEqual(match('\\({int})', '(3)'), [3]) | ||
}) | ||
it('does not allow text/parameter type alternation', () => { | ||
try { | ||
match('x/{int}', '3') | ||
assert.fail() | ||
} catch (expected) { | ||
assert.strictEqual( | ||
expected.message, | ||
'Parameter types cannot be alternative: x/{int}' | ||
) | ||
} | ||
}) | ||
it('does not allow parameter type/text alternation', () => { | ||
try { | ||
match('{int}/x', '3') | ||
assert.fail() | ||
} catch (expected) { | ||
assert.strictEqual( | ||
expected.message, | ||
'Parameter types cannot be alternative: {int}/x' | ||
) | ||
} | ||
}) | ||
for (const c of '[]()$.|?*+'.split('')) { | ||
it(`does not allow parameter type with ${c}`, () => { | ||
try { | ||
match(`{${c}string}`, 'something') | ||
assert.fail() | ||
} catch (expected) { | ||
assert.strictEqual( | ||
expected.message, | ||
`Illegal character '${c}' in parameter name {${c}string}` | ||
) | ||
} | ||
}) | ||
} | ||
it('exposes source', () => { | ||
@@ -320,40 +187,2 @@ const expr = 'I have {int} cuke(s)' | ||
}) | ||
describe('escapes special characters', () => { | ||
const special = ['\\', '[', ']', '^', '$', '.', '|', '?', '*', '+'] | ||
special.forEach((character) => { | ||
it(`escapes ${character}`, () => { | ||
const expr = `I have {int} cuke(s) and ${character}` | ||
const expression = new CucumberExpression( | ||
expr, | ||
new ParameterTypeRegistry() | ||
) | ||
const arg1 = expression.match(`I have 800 cukes and ${character}`)[0] | ||
assert.strictEqual(arg1.getValue(null), 800) | ||
}) | ||
}) | ||
it(`escapes .`, () => { | ||
const expr = `I have {int} cuke(s) and .` | ||
const expression = new CucumberExpression( | ||
expr, | ||
new ParameterTypeRegistry() | ||
) | ||
assert.strictEqual(expression.match(`I have 800 cukes and 3`), null) | ||
const arg1 = expression.match(`I have 800 cukes and .`)[0] | ||
assert.strictEqual(arg1.getValue(null), 800) | ||
}) | ||
it(`escapes |`, () => { | ||
const expr = `I have {int} cuke(s) and a|b` | ||
const expression = new CucumberExpression( | ||
expr, | ||
new ParameterTypeRegistry() | ||
) | ||
assert.strictEqual(expression.match(`I have 800 cukes and a`), null) | ||
assert.strictEqual(expression.match(`I have 800 cukes and b`), null) | ||
const arg1 = expression.match(`I have 800 cukes and a|b`)[0] | ||
assert.strictEqual(arg1.getValue(null), 800) | ||
}) | ||
}) | ||
}) | ||
@@ -360,0 +189,0 @@ |
@@ -45,3 +45,6 @@ 'use strict' | ||
new ParameterType('[string]', /.*/, String, (s) => s, false, true), | ||
{ message: "Illegal character '[' in parameter name {[string]}" } | ||
{ | ||
message: | ||
"Illegal character in parameter name {[string]}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", | ||
} | ||
) | ||
@@ -48,0 +51,0 @@ }) |
@@ -9,3 +9,3 @@ import fs from 'fs' | ||
const match = (expressionText: string, text: string) => { | ||
const m = /\/(.*)\//.exec(expressionText) | ||
const m = /^\/(.*)\/$/.exec(expressionText) | ||
const expression = m | ||
@@ -12,0 +12,0 @@ ? new RegularExpression(new RegExp(m[1]), new ParameterTypeRegistry()) |
@@ -26,8 +26,2 @@ import * as assert from 'assert' | ||
}) | ||
it('creates an UndefinedParameterTypeExpression', () => { | ||
assert.throws(() => expressionFactory.createExpression('{x}'), { | ||
message: 'Undefined parameter type {x}', | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
15
266
487643
9183
4