diff --git a/cucumber-expressions/CHANGELOG.md b/cucumber-expressions/CHANGELOG.md index bd0e6a9b7e..6404d2c44c 100644 --- a/cucumber-expressions/CHANGELOG.md +++ b/cucumber-expressions/CHANGELOG.md @@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ---- ## [Unreleased] +This is a major release of Cucumber Expressions. +Cucumber Expressions now has a formal grammar. + +This grammar is implemented in a hand-written recursive-descent parser. +The new grammar and parser handles edge cases better. + +Most existing expressions should still parse in the same way, but you may +come across edge cases where the expressions are parsed differently. + +This work was a heroic effort by @mpkorstanje who has been working on and off +on this for over a year!! + ### Added ### Changed -### Deprecated +* Some expressions that were valid in previous versions may now be invalid +* Some expressions that were invalid in previous versions may now be valid ### Removed @@ -19,6 +32,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +* [Go, Java, JavaScript, Ruby] New handwritten parser, which fixes several long-standing bugs. + ([#601](https://github.com/cucumber/cucumber/issues/601) + [#726](https://github.com/cucumber/cucumber/issues/726) + [#767](https://github.com/cucumber/cucumber/issues/767) + [#770](https://github.com/cucumber/cucumber/issues/770) + [#771](https://github.com/cucumber/cucumber/pull/771) + [mpkorstanje]) * [Go] Support for Go 1.15 ## [10.3.0] - 2020-08-07 diff --git a/cucumber-expressions/Makefile b/cucumber-expressions/Makefile index 551e68e27a..3e7523e10c 100644 --- a/cucumber-expressions/Makefile +++ b/cucumber-expressions/Makefile @@ -1 +1,2 @@ +LANGUAGES ?= go javascript ruby java include default.mk diff --git a/cucumber-expressions/README.md b/cucumber-expressions/README.md index bfaef63d31..6b6f9ea3d3 100644 --- a/cucumber-expressions/README.md +++ b/cucumber-expressions/README.md @@ -1,9 +1,71 @@ -See [website docs](https://cucumber.io/docs/cucumber/cucumber-expressions/) for more details. +See [website docs](https://cucumber.io/docs/cucumber/cucumber-expressions/) +for more details. + +## Grammar ## + +A Cucumber Expression has the following AST: + +``` +cucumber-expression := ( alternation | optional | parameter | text )* +alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary) +left-boundary := whitespace | } | ^ +right-boundary := whitespace | { | $ +alternative: = optional | parameter | text +optional := '(' + option* + ')' +option := optional | parameter | text +parameter := '{' + name* + '}' +name := whitespace | . +text := whitespace | ')' | '}' | . +``` + +The AST is constructed from the following tokens: +``` +escape := '\' +token := whitespace | '(' | ')' | '{' | '}' | '/' | . +. := any non-reserved codepoint +``` + +Note: + * While `parameter` is allowed to appear as part of `alternative` and + `option` in the AST, such an AST is not a valid a Cucumber Expression. + * While `optional` is allowed to appear as part of `option` in the AST, + such an AST is not a valid a Cucumber Expression. + * ASTs with empty alternatives or alternatives that only + contain an optional are valid ASTs but invalid Cucumber Expressions. + * All escaped tokens (tokens starting with a backslash) are rewritten to their + unescaped equivalent after parsing. + +### Production Rules + +The AST can be rewritten into a regular expression by the following production +rules: + +``` +cucumber-expression -> '^' + rewrite(node[0]) + ... + rewrite(node[n-1]) + '$' +alternation -> '(?:' + rewrite(node[0]) +'|' + ... +'|' + rewerite(node[n-1]) + ')' +alternative -> rewrite(node[0]) + ... + rewrite(node[n-1]) +optional -> '(?:' + rewrite(node[0]) + ... + rewrite(node[n-1]) + ')?' +parameter -> { + parameter_name := node[0].text + ... + node[n-1].text + parameter_pattern := parameter_type_registry[parameter_name] + '((?:' + parameter_pattern[0] + ')|(?:' ... + ')|(?:' + parameter_pattern[n-1] + '))' +} +text -> { + escape_regex := escape '^', `$`, `[`, `]`, `(`, `)` `\`, `{`, `}`, `.`, `|`, `?`, `*`, `+` + escape_regex(token.text) +} +``` ## Acknowledgements The Cucumber Expression syntax is inspired by similar expression syntaxes in -other BDD tools, such as [Turnip](https://github.com/jnicklas/turnip), [Behat](https://github.com/Behat/Behat) and [Behave](https://github.com/behave/behave). +other BDD tools, such as [Turnip](https://github.com/jnicklas/turnip), +[Behat](https://github.com/Behat/Behat) and +[Behave](https://github.com/behave/behave). Big thanks to Jonas Nicklas, Konstantin Kudryashov and Jens Engel for implementing those libraries. + +The [Tiny-Compiler-Parser tutorial](https://blog.klipse.tech/javascript/2017/02/08/tiny-compiler-parser.html) +by [Yehonathan Sharvit](https://github.com/viebel) inspired the design of the +Cucumber expression parser. diff --git a/cucumber-expressions/examples.txt b/cucumber-expressions/examples.txt index 35b7bd17a9..3a8288d34d 100644 --- a/cucumber-expressions/examples.txt +++ b/cucumber-expressions/examples.txt @@ -2,7 +2,7 @@ I have {int} cuke(s) I have 22 cukes [22] --- -I have {int} cuke(s) and some \[]^$.|?*+ +I have {int} cuke(s) and some \\[]^$.|?*+ I have 1 cuke and some \[]^$.|?*+ [1] --- @@ -37,3 +37,7 @@ a purchase for $33 Some ${float} of cukes at {int}° Celsius Some $3.50 of cukes at 42° Celsius [3.5,42] +--- +I select the {int}st/nd/rd/th Cucumber +I select the 3rd Cucumber +[3] diff --git a/cucumber-expressions/go/.rsync b/cucumber-expressions/go/.rsync index 1c4037d8da..1831697da5 100644 --- a/cucumber-expressions/go/.rsync +++ b/cucumber-expressions/go/.rsync @@ -2,3 +2,4 @@ ../../.templates/github/ .github/ ../../.templates/go/ . ../examples.txt examples.txt +../testdata . diff --git a/cucumber-expressions/go/ast.go b/cucumber-expressions/go/ast.go new file mode 100644 index 0000000000..e1ff4c5088 --- /dev/null +++ b/cucumber-expressions/go/ast.go @@ -0,0 +1,147 @@ +package cucumberexpressions + +import ( + "strings" + "unicode" +) + +const escapeCharacter rune = '\\' +const alternationCharacter rune = '/' +const beginParameterCharacter rune = '{' +const endParameterCharacter rune = '}' +const beginOptionalCharacter rune = '(' +const endOptionalCharacter rune = ')' + +type nodeType string + +const ( + textNode nodeType = "TEXT_NODE" + optionalNode nodeType = "OPTIONAL_NODE" + alternationNode nodeType = "ALTERNATION_NODE" + alternativeNode nodeType = "ALTERNATIVE_NODE" + parameterNode nodeType = "PARAMETER_NODE" + expressionNode nodeType = "EXPRESSION_NODE" +) + +type node struct { + NodeType nodeType `json:"type"` + Start int `json:"start"` + End int `json:"end"` + Token string `json:"token"` + Nodes []node `json:"nodes"` +} + +func (node node) text() string { + builder := strings.Builder{} + builder.WriteString(node.Token) + + if node.Nodes == nil { + return builder.String() + } + + for _, c := range node.Nodes { + builder.WriteString(c.text()) + } + return builder.String() +} + +type tokenType string + +const ( + startOfLine tokenType = "START_OF_LINE" + endOfLine tokenType = "END_OF_LINE" + whiteSpace tokenType = "WHITE_SPACE" + beginOptional tokenType = "BEGIN_OPTIONAL" + endOptional tokenType = "END_OPTIONAL" + beginParameter tokenType = "BEGIN_PARAMETER" + endParameter tokenType = "END_PARAMETER" + alternation tokenType = "ALTERNATION" + text tokenType = "TEXT" +) + +type token struct { + Text string `json:"text"` + TokenType tokenType `json:"type"` + Start int `json:"start"` + End int `json:"end"` +} + +var nullNode = node{textNode, -1, -1, "", nil} + +func isEscapeCharacter(r rune) bool { + return r == escapeCharacter +} + +func canEscape(r rune) bool { + if unicode.Is(unicode.White_Space, r) { + return true + } + switch r { + case escapeCharacter: + return true + case alternationCharacter: + return true + case beginParameterCharacter: + return true + case endParameterCharacter: + return true + case beginOptionalCharacter: + return true + case endOptionalCharacter: + return true + } + return false +} + +func typeOf(r rune) (tokenType, error) { + if unicode.Is(unicode.White_Space, r) { + return whiteSpace, nil + } + switch r { + case alternationCharacter: + return alternation, nil + case beginParameterCharacter: + return beginParameter, nil + case endParameterCharacter: + return endParameter, nil + case beginOptionalCharacter: + return beginOptional, nil + case endOptionalCharacter: + return endOptional, nil + } + return text, nil +} + +func symbol(tokenType tokenType) string { + switch tokenType { + case beginOptional: + return string(beginOptionalCharacter) + case endOptional: + return string(endOptionalCharacter) + case beginParameter: + return string(beginParameterCharacter) + case endParameter: + return string(endParameterCharacter) + case alternation: + return string(alternationCharacter) + } + + return "" +} + +func purpose(tokenType tokenType) string { + switch tokenType { + case beginOptional: + return "optional text" + case endOptional: + return "optional text" + case beginParameter: + return "a parameter" + case endParameter: + return "optional text" + case alternation: + return "alternation" + } + + return "" +} diff --git a/cucumber-expressions/go/cucumber_expression.go b/cucumber-expressions/go/cucumber_expression.go index f3c09c19a0..626384b4ff 100644 --- a/cucumber-expressions/go/cucumber_expression.go +++ b/cucumber-expressions/go/cucumber_expression.go @@ -7,11 +7,7 @@ import ( "strings" ) -var ESCAPE_REGEXP = regexp.MustCompile(`([\\^[$.|?*+])`) -var PARAMETER_REGEXP = regexp.MustCompile(`(\\\\\\\\)?{([^}]*)}`) -var OPTIONAL_REGEXP = regexp.MustCompile(`(\\\\\\\\)?\([^)]+\)`) -var ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = regexp.MustCompile(`([^\s^/]+)((/[^\s^/]+)+)`) -var DOUBLE_ESCAPE = `\\\\` +var escapeRegexp = regexp.MustCompile(`([\\^\[({$.|?*+})\]])`) type CucumberExpression struct { source string @@ -23,143 +19,206 @@ type CucumberExpression struct { func NewCucumberExpression(expression string, parameterTypeRegistry *ParameterTypeRegistry) (Expression, error) { result := &CucumberExpression{source: expression, parameterTypeRegistry: parameterTypeRegistry} - expression = result.processEscapes(expression) - - expression, err := result.processOptional(expression) + ast, err := parse(expression) if err != nil { return nil, err } - expression, err = result.processAlteration(expression) + pattern, err := result.rewriteNodeToRegex(ast) if err != nil { return nil, err } + result.treeRegexp = NewTreeRegexp(regexp.MustCompile(pattern)) + return result, nil +} + +func (c *CucumberExpression) rewriteNodeToRegex(node node) (string, error) { + switch node.NodeType { + case textNode: + return c.processEscapes(node.Token), nil + case optionalNode: + return c.rewriteOptional(node) + case alternationNode: + return c.rewriteAlternation(node) + case alternativeNode: + return c.rewriteAlternative(node) + case parameterNode: + return c.rewriteParameter(node) + case expressionNode: + return c.rewriteExpression(node) + default: + // Can't happen as long as the switch case is exhaustive + return "", NewCucumberExpressionError(fmt.Sprintf("Could not rewrite %s", c.source)) + } +} - expression, err = result.processParameters(expression, parameterTypeRegistry) +func (c *CucumberExpression) processEscapes(expression string) string { + return escapeRegexp.ReplaceAllString(expression, `\$1`) +} + +func (c *CucumberExpression) rewriteOptional(node node) (string, error) { + err := c.assertNoParameters(node, c.createParameterIsNotAllowedInOptional()) if err != nil { - return nil, err + return "", err } + err = c.assertNoOptionals(node, c.createOptionalIsNotAllowedInOptional()) + if err != nil { + return "", err + } + err = c.assertNotEmpty(node, c.createOptionalMayNotBeEmpty()) + if err != nil { + return "", err + } + return c.rewriteNodesToRegex(node.Nodes, "", "(?:", ")?") +} - expression = "^" + expression + "$" +func (c *CucumberExpression) createParameterIsNotAllowedInOptional() func(node) error { + return func(node node) error { + return createParameterIsNotAllowedInOptional(node, c.source) + } +} - result.treeRegexp = NewTreeRegexp(regexp.MustCompile(expression)) - return result, nil +func (c *CucumberExpression) createOptionalIsNotAllowedInOptional() func(node) error { + return func(node node) error { + return createOptionalIsNotAllowedInOptional(node, c.source) + } } -func (c *CucumberExpression) Match(text string, typeHints ...reflect.Type) ([]*Argument, error) { - parameterTypes := make([]*ParameterType, len(c.parameterTypes)) - copy(parameterTypes, c.parameterTypes) - for i := 0; i < len(parameterTypes); i++ { - if parameterTypes[i].isAnonymous() { - typeHint := hintOrDefault(i, typeHints...) - parameterType, err := parameterTypes[i].deAnonymize(typeHint, c.objectMapperTransformer(typeHint)) - if err != nil { - return nil, err - } - parameterTypes[i] = parameterType +func (c *CucumberExpression) createOptionalMayNotBeEmpty() func(node) error { + return func(node node) error { + return createOptionalMayNotBeEmpty(node, c.source) + } +} + +func (c *CucumberExpression) rewriteAlternation(node node) (string, error) { + // Make sure the alternative parts aren't empty and don't contain parameter types + for _, alternative := range node.Nodes { + if len(alternative.Nodes) == 0 { + return "", createAlternativeMayNotBeEmpty(alternative, c.source) + } + err := c.assertNotEmpty(alternative, c.createAlternativeMayNotExclusivelyContainOptionals()) + if err != nil { + return "", err } } - return BuildArguments(c.treeRegexp, text, parameterTypes), nil + return c.rewriteNodesToRegex(node.Nodes, "|", "(?:", ")") } -func hintOrDefault(i int, typeHints ...reflect.Type) reflect.Type { - typeHint := reflect.TypeOf("") - if i < len(typeHints) { - typeHint = typeHints[i] +func (c *CucumberExpression) createAlternativeMayNotExclusivelyContainOptionals() func(node) error { + return func(node node) error { + return createAlternativeMayNotExclusivelyContainOptionals(node, c.source) } - return typeHint } -func (c *CucumberExpression) Regexp() *regexp.Regexp { - return c.treeRegexp.Regexp() +func (c *CucumberExpression) rewriteAlternative(node node) (string, error) { + return c.rewriteNodesToRegex(node.Nodes, "", "", "") } -func (c *CucumberExpression) Source() string { - return c.source +func (c *CucumberExpression) rewriteParameter(node node) (string, error) { + buildCaptureRegexp := func(regexps []*regexp.Regexp) string { + if len(regexps) == 1 { + return fmt.Sprintf("(%s)", regexps[0].String()) + } + + captureGroups := make([]string, len(regexps)) + for i, r := range regexps { + captureGroups[i] = fmt.Sprintf("(?:%s)", r.String()) + } + + return fmt.Sprintf("(%s)", strings.Join(captureGroups, "|")) + } + typeName := node.text() + parameterType := c.parameterTypeRegistry.LookupByTypeName(typeName) + if parameterType == nil { + return "", createUndefinedParameterType(node, c.source, typeName) + } + c.parameterTypes = append(c.parameterTypes, parameterType) + return buildCaptureRegexp(parameterType.regexps), nil } -func (c *CucumberExpression) processEscapes(expression string) string { - return ESCAPE_REGEXP.ReplaceAllString(expression, `\$1`) +func (c *CucumberExpression) rewriteExpression(node node) (string, error) { + return c.rewriteNodesToRegex(node.Nodes, "", "^", "$") } -func (c *CucumberExpression) processOptional(expression string) (string, error) { - var err error - result := OPTIONAL_REGEXP.ReplaceAllStringFunc(expression, func(match string) string { - if strings.HasPrefix(match, DOUBLE_ESCAPE) { - return fmt.Sprintf(`\(%s\)`, match[5:len(match)-1]) - } - if PARAMETER_REGEXP.MatchString(match) { - err = NewCucumberExpressionError(fmt.Sprintf("Parameter types cannot be optional: %s", c.source)) - return match +func (c *CucumberExpression) rewriteNodesToRegex(nodes []node, delimiter string, prefix string, suffix string) (string, error) { + builder := strings.Builder{} + builder.WriteString(prefix) + for i, node := range nodes { + if i > 0 { + builder.WriteString(delimiter) } - return fmt.Sprintf("(?:%s)?", match[1:len(match)-1]) - }) - return result, err -} - -func (c *CucumberExpression) processAlteration(expression string) (string, error) { - var err error - result := ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP.ReplaceAllStringFunc(expression, func(match string) string { - // replace \/ with / - // replace / with | - replacement := strings.Replace(match, "/", "|", -1) - replacement = strings.Replace(replacement, `\\\\|`, "/", -1) - - if strings.Contains(replacement, "|") { - parts := strings.Split(replacement, ":") - for _, part := range parts { - if PARAMETER_REGEXP.MatchString(part) { - err = NewCucumberExpressionError(fmt.Sprintf("Parameter types cannot be alternative: %s", c.source)) - return match - } - } - return fmt.Sprintf("(?:%s)", replacement) + s, err := c.rewriteNodeToRegex(node) + if err != nil { + return s, err } - - return replacement - }) - return result, err + builder.WriteString(s) + } + builder.WriteString(suffix) + return builder.String(), nil } -func (c *CucumberExpression) processParameters(expression string, parameterTypeRegistry *ParameterTypeRegistry) (string, error) { - var err error - result := PARAMETER_REGEXP.ReplaceAllStringFunc(expression, func(match string) string { - if strings.HasPrefix(match, DOUBLE_ESCAPE) { - return fmt.Sprintf(`\{%s\}`, match[5:len(match)-1]) +func (c *CucumberExpression) assertNotEmpty(node node, createNodeWasNotEmptyError func(node) error) error { + for _, node := range node.Nodes { + if node.NodeType == textNode { + return nil } + } + return createNodeWasNotEmptyError(node) +} - typeName := match[1 : len(match)-1] - err = CheckParameterTypeName(typeName) - if err != nil { - return "" +func (c *CucumberExpression) assertNoParameters(node node, createParameterIsNotAllowedInOptionalError func(node) error) error { + for _, node := range node.Nodes { + if node.NodeType == parameterNode { + return createParameterIsNotAllowedInOptionalError(node) } - parameterType := parameterTypeRegistry.LookupByTypeName(typeName) - if parameterType == nil { - err = NewUndefinedParameterTypeError(typeName) - return match + } + return nil +} + +func (c *CucumberExpression) assertNoOptionals(node node, createOptionalIsNotAllowedInOptionalError func(node) error) error { + for _, node := range node.Nodes { + if node.NodeType == optionalNode { + return createOptionalIsNotAllowedInOptionalError(node) } - c.parameterTypes = append(c.parameterTypes, parameterType) - return buildCaptureRegexp(parameterType.regexps) - }) - return result, err + } + return nil } -func buildCaptureRegexp(regexps []*regexp.Regexp) string { - if len(regexps) == 1 { - return fmt.Sprintf("(%s)", regexps[0].String()) +func (c *CucumberExpression) Match(text string, typeHints ...reflect.Type) ([]*Argument, error) { + hintOrDefault := func(i int, typeHints ...reflect.Type) reflect.Type { + typeHint := reflect.TypeOf("") + if i < len(typeHints) { + typeHint = typeHints[i] + } + return typeHint } - captureGroups := make([]string, len(regexps)) - for i, r := range regexps { - captureGroups[i] = fmt.Sprintf("(?:%s)", r.String()) + parameterTypes := make([]*ParameterType, len(c.parameterTypes)) + copy(parameterTypes, c.parameterTypes) + for i := 0; i < len(parameterTypes); i++ { + if parameterTypes[i].isAnonymous() { + typeHint := hintOrDefault(i, typeHints...) + parameterType, err := parameterTypes[i].deAnonymize(typeHint, c.objectMapperTransformer(typeHint)) + if err != nil { + return nil, err + } + parameterTypes[i] = parameterType + } } + return BuildArguments(c.treeRegexp, text, parameterTypes), nil +} + +func (c *CucumberExpression) Regexp() *regexp.Regexp { + return c.treeRegexp.Regexp() +} - return fmt.Sprintf("(%s)", strings.Join(captureGroups, "|")) +func (c *CucumberExpression) Source() string { + return c.source } -func (r *CucumberExpression) objectMapperTransformer(typeHint reflect.Type) func(args ...*string) interface{} { +func (c *CucumberExpression) objectMapperTransformer(typeHint reflect.Type) func(args ...*string) interface{} { return func(args ...*string) interface{} { - i, err := r.parameterTypeRegistry.defaultTransformer.Transform(*args[0], typeHint) + i, err := c.parameterTypeRegistry.defaultTransformer.Transform(*args[0], typeHint) if err != nil { panic(err) } diff --git a/cucumber-expressions/go/cucumber_expression_generator.go b/cucumber-expressions/go/cucumber_expression_generator.go index cb899ba4dc..605dc125d4 100644 --- a/cucumber-expressions/go/cucumber_expression_generator.go +++ b/cucumber-expressions/go/cucumber_expression_generator.go @@ -16,6 +16,13 @@ func NewCucumberExpressionGenerator(parameterTypeRegistry *ParameterTypeRegistry } func (c *CucumberExpressionGenerator) GenerateExpressions(text string) []*GeneratedExpression { + escape := func(s string) string { + result := strings.Replace(s, "%", "%%", -1) + result = strings.Replace(result, `(`, `\(`, -1) + result = strings.Replace(result, `{`, `\{`, -1) + return strings.Replace(result, `/`, `\/`, -1) + } + parameterTypeCombinations := [][]*ParameterType{} parameterTypeMatchers := c.createParameterTypeMatchers(text) expressionTemplate := "" @@ -93,10 +100,3 @@ func (c *CucumberExpressionGenerator) createParameterTypeMatchers2(parameterType } return result } - -func escape(s string) string { - result := strings.Replace(s, "%", "%%", -1) - result = strings.Replace(result, `(`, `\(`, -1) - result = strings.Replace(result, `{`, `\{`, -1) - return strings.Replace(result, `/`, `\/`, -1) -} diff --git a/cucumber-expressions/go/cucumber_expression_parser.go b/cucumber-expressions/go/cucumber_expression_parser.go new file mode 100644 index 0000000000..e2982fa40b --- /dev/null +++ b/cucumber-expressions/go/cucumber_expression_parser.go @@ -0,0 +1,290 @@ +package cucumberexpressions + +/* + * text := whitespace | ')' | '}' | . + */ +var textParser = func(expression string, tokens []token, current int) (int, node, error) { + token := tokens[current] + switch token.TokenType { + case whiteSpace: + fallthrough + case text: + fallthrough + case endParameter: + fallthrough + case endOptional: + return 1, node{textNode, token.Start, token.End, token.Text, nil}, nil + case alternation: + return 0, nullNode, createAlternationNotAllowedInOptional(expression, token) + case beginParameter: + fallthrough + case startOfLine: + fallthrough + case endOfLine: + fallthrough + case beginOptional: + fallthrough + default: + // If configured correctly this will never happen + return 0, nullNode, nil + } +} + +/* + * name := whitespace | . + */ +var nameParser = func(expression string, tokens []token, current int) (int, node, error) { + token := tokens[current] + switch token.TokenType { + case whiteSpace: + fallthrough + case text: + return 1, node{textNode, token.Start, token.End, token.Text, nil}, nil + case beginParameter: + fallthrough + case endParameter: + fallthrough + case beginOptional: + fallthrough + case endOptional: + fallthrough + case alternation: + return 0, nullNode, createInvalidParameterTypeNameInNode(token, expression) + case startOfLine: + fallthrough + case endOfLine: + fallthrough + default: + // If configured correctly this will never happen + return 0, nullNode, nil + } +} + +/* + * parameter := '{' + name* + '}' + */ +var parameterParser = parseBetween( + parameterNode, + beginParameter, + endParameter, + []parser{nameParser}, +) + +/* + * optional := '(' + option* + ')' + * option := optional | parameter | text + */ +var optionalSubParsers = make([]parser, 3) +var optionalParser = parseBetween( + optionalNode, + beginOptional, + endOptional, + optionalSubParsers, +) + +func setParsers(parsers []parser, toSet ...parser) []parser { + for i, p := range toSet { + parsers[i] = p + } + return parsers +} + +var _ = setParsers(optionalSubParsers, optionalParser, parameterParser, textParser) + +// alternation := alternative* + ( '/' + alternative* )+ +var alternativeSeparatorParser = func(expression string, tokens []token, current int) (int, node, error) { + if !lookingAt(tokens, current, alternation) { + return 0, nullNode, nil + } + token := tokens[current] + return 1, node{alternativeNode, token.Start, token.End, token.Text, nil}, nil +} + +var alternativeParsers = []parser{ + alternativeSeparatorParser, + optionalParser, + parameterParser, + textParser, +} + +/* + * alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary) + * left-boundary := whitespace | } | ^ + * right-boundary := whitespace | { | $ + * alternative: = optional | parameter | text + */ +var alternationParser = func(expression string, tokens []token, current int) (int, node, error) { + previous := current - 1 + if !lookingAtAny(tokens, previous, startOfLine, whiteSpace, endParameter) { + return 0, nullNode, nil + } + + consumed, subAst, err := parseTokensUntil(expression, alternativeParsers, tokens, current, whiteSpace, endOfLine, beginParameter) + if err != nil { + return 0, nullNode, err + } + + var contains = func(s []node, nodeType nodeType) bool { + for _, a := range s { + if a.NodeType == nodeType { + return true + } + } + return false + } + subCurrent := current + consumed + if !contains(subAst, alternativeNode) { + return 0, nullNode, nil + } + + // Does not consume right hand boundary token + start := tokens[current].Start + end := tokens[subCurrent].Start + return consumed, node{alternationNode, start, end, "", splitAlternatives(start, end, subAst)}, nil +} + +/* + * cucumber-expression := ( alternation | optional | parameter | text )* + */ +var cucumberExpressionParser = parseBetween( + expressionNode, + startOfLine, + endOfLine, + []parser{alternationParser, optionalParser, parameterParser, textParser}, +) + +func parse(expression string) (node, error) { + tokens, err := tokenize(expression) + if err != nil { + return nullNode, err + } + consumed, ast, err := cucumberExpressionParser(expression, tokens, 0) + if err != nil { + return nullNode, err + } + if consumed != len(tokens) { + // Can't happen if configured properly + return nullNode, NewCucumberExpressionError("Could not parse" + expression) + } + return ast, nil +} + +type parser func(expression string, tokens []token, current int) (int, node, error) + +func parseBetween(nodeType nodeType, beginToken tokenType, endToken tokenType, parsers []parser) parser { + return func(expression string, tokens []token, current int) (int, node, error) { + if !lookingAt(tokens, current, beginToken) { + return 0, nullNode, nil + } + + subCurrent := current + 1 + consumed, subAst, err := parseTokensUntil(expression, parsers, tokens, subCurrent, endToken, endOfLine) + if err != nil { + return 0, nullNode, err + } + subCurrent += consumed + + // endToken not found + if !lookingAt(tokens, subCurrent, endToken) { + return 0, nullNode, createMissingEndToken(expression, beginToken, endToken, tokens[current]) + } + // consumes endToken + start := tokens[current].Start + end := tokens[subCurrent].End + return subCurrent + 1 - current, node{nodeType, start, end, "", subAst}, nil + } +} + +func parseTokensUntil(expresion string, parsers []parser, tokens []token, startAt int, endTokens ...tokenType) (int, []node, error) { + ast := make([]node, 0) + current := startAt + size := len(tokens) + for current < size { + if lookingAtAny(tokens, current, endTokens...) { + break + } + consumed, node, err := parseToken(expresion, parsers, tokens, current) + if err != nil { + return 0, nil, err + } + if consumed == 0 { + // If configured correctly this will never happen + // Keep to avoid infinite loops + return 0, nil, NewCucumberExpressionError("No eligible parsers") + } + current += consumed + ast = append(ast, node) + } + + return current - startAt, ast, nil +} + +func parseToken(expression string, parsers []parser, tokens []token, startAt int) (int, node, error) { + for _, parser := range parsers { + consumed, node, err := parser(expression, tokens, startAt) + if err != nil { + return 0, nullNode, err + } + if consumed != 0 { + return consumed, node, nil + } + } + // If configured correctly this will never happen + return 0, nullNode, NewCucumberExpressionError("No eligible parsers") +} + +func lookingAtAny(tokens []token, at int, tokenTypes ...tokenType) bool { + for _, tokenType := range tokenTypes { + if lookingAt(tokens, at, tokenType) { + return true + } + } + return false +} + +func lookingAt(tokens []token, at int, tokenType tokenType) bool { + size := len(tokens) + if at < 0 { + return tokenType == startOfLine + } + if at >= size { + return tokenType == endOfLine + } + return tokens[at].TokenType == tokenType +} + +func splitAlternatives(start int, end int, alternation []node) []node { + separators := make([]node, 0) + alternatives := make([][]node, 0) + alternative := make([]node, 0) + for _, n := range alternation { + if n.NodeType == alternativeNode { + separators = append(separators, n) + alternatives = append(alternatives, alternative) + alternative = make([]node, 0) + } else { + alternative = append(alternative, n) + } + } + alternatives = append(alternatives, alternative) + + return createAlternativeNodes(start, end, separators, alternatives) +} + +func createAlternativeNodes(start int, end int, separators []node, alternatives [][]node) []node { + nodes := make([]node, 0) + for i, n := range alternatives { + if i == 0 { + rightSeparator := separators[i] + nodes = append(nodes, node{alternativeNode, start, rightSeparator.Start, "", n}) + } else if i == len(alternatives)-1 { + leftSeparator := separators[i-1] + nodes = append(nodes, node{alternativeNode, leftSeparator.End, end, "", n}) + } else { + leftSeparator := separators[i-1] + rightSeparator := separators[i] + nodes = append(nodes, node{alternativeNode, leftSeparator.End, rightSeparator.Start, "", n}) + } + } + return nodes +} diff --git a/cucumber-expressions/go/cucumber_expression_parser_test.go b/cucumber-expressions/go/cucumber_expression_parser_test.go new file mode 100644 index 0000000000..8d2077f62c --- /dev/null +++ b/cucumber-expressions/go/cucumber_expression_parser_test.go @@ -0,0 +1,48 @@ +package cucumberexpressions + +import ( + "encoding/json" + "fmt" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "io/ioutil" + "testing" +) + +func TestCucumberExpressionParser(t *testing.T) { + var assertAst = func(t *testing.T, expected node, expression string) { + ast, err := parse(expression) + require.NoError(t, err) + require.Equal(t, expected, ast) + require.Equal(t, expected, ast) + } + var assertThrows = func(t *testing.T, expected string, expression string) { + _, err := parse(expression) + require.Error(t, err) + require.Equal(t, expected, err.Error()) + } + + directory := "testdata/ast/" + files, err := ioutil.ReadDir(directory) + require.NoError(t, err) + + for _, file := range files { + contents, err := ioutil.ReadFile(directory + file.Name()) + require.NoError(t, err) + t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { + var expectation expectation + err = yaml.Unmarshal(contents, &expectation) + require.NoError(t, err) + + if expectation.Exception == "" { + var node node + err = json.Unmarshal([]byte(expectation.Expected), &node) + require.NoError(t, err) + assertAst(t, node, expectation.Expression) + } else { + assertThrows(t, expectation.Exception, expectation.Expression) + } + }) + } + +} diff --git a/cucumber-expressions/go/cucumber_expression_regexp_test.go b/cucumber-expressions/go/cucumber_expression_regexp_test.go deleted file mode 100644 index 5f8c172d7f..0000000000 --- a/cucumber-expressions/go/cucumber_expression_regexp_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package cucumberexpressions - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCucumberExpressionRegExpTranslation(t *testing.T) { - t.Run("translates no arguments", func(t *testing.T) { - assertRegexp( - t, - "I have 10 cukes in my belly now", - "^I have 10 cukes in my belly now$", - ) - }) - - t.Run("translates alternation", func(t *testing.T) { - assertRegexp( - t, - "I had/have a great/nice/charming friend", - "^I (?:had|have) a (?:great|nice|charming) friend$", - ) - }) - - t.Run("translates alternation with non-alpha", func(t *testing.T) { - assertRegexp( - t, - "I said Alpha1/Beta1", - "^I said (?:Alpha1|Beta1)$", - ) - }) - - t.Run("translates parameters", func(t *testing.T) { - assertRegexp( - t, - "I have {float} cukes at {int} o'clock", - `^I have ([-+]?\d*\.?\d+) cukes at ((?:-?\d+)|(?:\d+)) o'clock$`, - ) - }) - - t.Run("translates parenthesis to non-capturing optional capture group", func(t *testing.T) { - assertRegexp( - t, - "I have many big(ish) cukes", - `^I have many big(?:ish)? cukes$`, - ) - }) - - t.Run("translates parenthesis with alpha unicode", func(t *testing.T) { - assertRegexp( - t, - "Привет, Мир(ы)!", - `^Привет, Мир(?:ы)?!$`, - ) - }) -} - -func assertRegexp(t *testing.T, expression string, expectedRegexp string) { - parameterTypeRegistry := NewParameterTypeRegistry() - generator, err := NewCucumberExpression(expression, parameterTypeRegistry) - require.NoError(t, err) - require.Equal(t, generator.Regexp().String(), expectedRegexp) -} diff --git a/cucumber-expressions/go/cucumber_expression_test.go b/cucumber-expressions/go/cucumber_expression_test.go index 250eb76051..122152cbdc 100644 --- a/cucumber-expressions/go/cucumber_expression_test.go +++ b/cucumber-expressions/go/cucumber_expression_test.go @@ -1,167 +1,103 @@ package cucumberexpressions import ( + "encoding/json" "fmt" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "io/ioutil" "reflect" "regexp" + "strings" "testing" ) func TestCucumberExpression(t *testing.T) { - t.Run("documents expression generation", func(t *testing.T) { - parameterTypeRegistry := NewParameterTypeRegistry() - - /// [capture-match-arguments] - expr := "I have {int} cuke(s)" - expression, err := NewCucumberExpression(expr, parameterTypeRegistry) - require.NoError(t, err) - args, err := expression.Match("I have 7 cukes") - require.NoError(t, err) - require.Equal(t, args[0].GetValue(), 7) - /// [capture-match-arguments] - }) - - t.Run("matches word", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {word} mice", "three blind mice"), - []interface{}{"blind"}, - ) - }) - - t.Run("matches double quoted string", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three "blind" mice`), - []interface{}{"blind"}, - ) - }) - - t.Run("matches multiple double quoted strings", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} and {string} mice", `three "blind" and "crippled" mice`), - []interface{}{"blind", "crippled"}, - ) - }) - - t.Run("matches single quoted string", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three 'blind' mice`), - []interface{}{"blind"}, - ) - }) - - t.Run("matches multiple single quoted strings", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} and {string} mice", `three 'blind' and 'crippled' mice`), - []interface{}{"blind", "crippled"}, - ) - }) - - t.Run("does not match misquoted string", func(t *testing.T) { - require.Nil( - t, - MatchCucumberExpression(t, "three {string} mice", `three "blind' mice`), - ) - }) - t.Run("matches single quoted strings with double quotes", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three '"blind"' mice`), - []interface{}{`"blind"`}, - ) - }) + t.Run("acceptance tests pass", func(t *testing.T) { - t.Run("matches double quoted strings with single quotes", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three "'blind'" mice`), - []interface{}{`'blind'`}, - ) - }) + assertMatches := func(t *testing.T, expected string, expr string, text string) { + parameterTypeRegistry := NewParameterTypeRegistry() + expression, err := NewCucumberExpression(expr, parameterTypeRegistry) + require.NoError(t, err) + args, err := expression.Match(text) + require.NoError(t, err) - t.Run("matches double quoted string with escaped double quote", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three "bl\"nd" mice`), - []interface{}{`bl\"nd`}, - ) - }) + values := strings.Builder{} + encoder := json.NewEncoder(&values) + encoder.SetEscapeHTML(false) + err = encoder.Encode(argumentValues(args)) + require.NoError(t, err) + require.Equal(t, expected, strings.TrimSuffix(values.String(), "\n")) + } - t.Run("matches single quoted string with escaped single quote", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three 'bl\'nd' mice`), - []interface{}{`bl\'nd`}, - ) - }) + assertThrows := func(t *testing.T, expected string, expr string, text string) { + parameterTypeRegistry := NewParameterTypeRegistry() + expression, err := NewCucumberExpression(expr, parameterTypeRegistry) - t.Run("matches single quoted empty string as empty string", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three '' mice`), - []interface{}{""}, - ) - }) + if err != nil { + require.Error(t, err) + require.Equal(t, expected, err.Error()) + } else { + _, err = expression.Match(text) + require.Error(t, err) + require.Equal(t, expected, err.Error()) + } + } - t.Run("matches double quoted empty string as empty string", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} mice", `three "" mice`), - []interface{}{""}, - ) - }) + directory := "testdata/expression/" + files, err := ioutil.ReadDir(directory) + require.NoError(t, err) - t.Run("matches single quoted empty string as empty string along with other strings", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} and {string} mice", `three '' and 'handsome' mice`), - []interface{}{"", "handsome"}, - ) - }) + for _, file := range files { + contents, err := ioutil.ReadFile(directory + file.Name()) + require.NoError(t, err) + t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { + var expectation expectation + err = yaml.Unmarshal(contents, &expectation) + require.NoError(t, err) + if expectation.Exception == "" { + assertMatches(t, expectation.Expected, expectation.Expression, expectation.Text) + } else { + assertThrows(t, expectation.Exception, expectation.Expression, expectation.Text) + } + }) + } - t.Run("matches double quoted empty string as empty string along with other strings", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three {string} and {string} mice", `three "" and "handsome" mice`), - []interface{}{"", "handsome"}, - ) - }) + assertRegex := func(t *testing.T, expected string, expr string) { + parameterTypeRegistry := NewParameterTypeRegistry() + expression, err := NewCucumberExpression(expr, parameterTypeRegistry) + require.NoError(t, err) + require.Equal(t, expected, expression.Regexp().String()) + } - t.Run("matches escaped parenthesis", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "three \\\\(exceptionally) {string} mice", `three (exceptionally) "blind" mice`), - []interface{}{"blind"}, - ) - }) + directory = "testdata/regex/" + files, err = ioutil.ReadDir(directory) + require.NoError(t, err) - t.Run("matches escaped slash", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "12\\\\/2020", `12/2020`), - []interface{}{}, - ) + for _, file := range files { + contents, err := ioutil.ReadFile(directory + file.Name()) + require.NoError(t, err) + t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { + var expectation expectation + err = yaml.Unmarshal(contents, &expectation) + require.NoError(t, err) + assertRegex(t, expectation.Expected, expectation.Expression) + }) + } }) - t.Run("matches int", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "{int}", "22"), - []interface{}{22}, - ) - }) + t.Run("documents expression generation", func(t *testing.T) { + parameterTypeRegistry := NewParameterTypeRegistry() - t.Run("doesn't match float as int", func(t *testing.T) { - require.Nil( - t, - MatchCucumberExpression(t, "{int}", "1.22"), - ) + /// [capture-match-arguments] + expr := "I have {int} cuke(s)" + expression, err := NewCucumberExpression(expr, parameterTypeRegistry) + require.NoError(t, err) + args, err := expression.Match("I have 7 cukes") + require.NoError(t, err) + require.Equal(t, args[0].GetValue(), 7) + /// [capture-match-arguments] }) t.Run("matches float", func(t *testing.T) { @@ -225,48 +161,6 @@ func TestCucumberExpression(t *testing.T) { ) }) - t.Run("does not allow parameter type with left bracket", func(t *testing.T) { - parameterTypeRegistry := NewParameterTypeRegistry() - _, err := NewCucumberExpression("{[string]}", parameterTypeRegistry) - require.Error(t, err) - require.Equal(t, err.Error(), "illegal character '[' in parameter name {[string]}") - }) - - t.Run("does not allow optional parameter types", func(t *testing.T) { - parameterTypeRegistry := NewParameterTypeRegistry() - _, err := NewCucumberExpression("({int})", parameterTypeRegistry) - require.Error(t, err) - require.Equal(t, "Parameter types cannot be optional: ({int})", err.Error()) - }) - - t.Run("allows escaped optional parameters", func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression(t, "\\\\({int})", `(3)`), - []interface{}{3}, - ) - }) - - t.Run("does not allow text/parameter type alternation", func(t *testing.T) { - parameterTypeRegistry := NewParameterTypeRegistry() - _, err := NewCucumberExpression("x/{int}", parameterTypeRegistry) - require.Error(t, err) - require.Equal(t, "Parameter types cannot be alternative: x/{int}", err.Error()) - }) - - t.Run("does not allow parameter type/text alternation", func(t *testing.T) { - parameterTypeRegistry := NewParameterTypeRegistry() - _, err := NewCucumberExpression("{int}/x", parameterTypeRegistry) - require.Error(t, err) - require.Equal(t, "Parameter types cannot be alternative: {int}/x", err.Error()) - }) - - t.Run("returns UndefinedParameterTypeExpression for unknown parameter", func(t *testing.T) { - parameterTypeRegistry := NewParameterTypeRegistry() - _, err := NewCucumberExpression("{unknown}", parameterTypeRegistry) - require.Error(t, err) - }) - t.Run("exposes source", func(t *testing.T) { expr := "I have {int} cuke(s)" parameterTypeRegistry := NewParameterTypeRegistry() @@ -275,51 +169,6 @@ func TestCucumberExpression(t *testing.T) { require.Equal(t, expression.Source(), expr) }) - t.Run("escapes special characters", func(t *testing.T) { - for _, char := range []string{"\\", "[", "]", "^", "$", ".", "|", "?", "*", "+"} { - t.Run(fmt.Sprintf("escapes %s", char), func(t *testing.T) { - require.Equal( - t, - MatchCucumberExpression( - t, - fmt.Sprintf("I have {int} cuke(s) and %s", char), - fmt.Sprintf("I have 800 cukes and %s", char), - ), - []interface{}{800}, - ) - }) - } - - t.Run("escapes .", func(t *testing.T) { - expr := "I have {int} cuke(s) and ." - parameterTypeRegistry := NewParameterTypeRegistry() - expression, err := NewCucumberExpression(expr, parameterTypeRegistry) - require.NoError(t, err) - args, err := expression.Match("I have 800 cukes and 3") - require.NoError(t, err) - require.Nil(t, args) - args, err = expression.Match("I have 800 cukes and .") - require.NoError(t, err) - require.NotNil(t, args) - }) - - t.Run("escapes |", func(t *testing.T) { - expr := "I have {int} cuke(s) and a|b" - parameterTypeRegistry := NewParameterTypeRegistry() - expression, err := NewCucumberExpression(expr, parameterTypeRegistry) - require.NoError(t, err) - args, err := expression.Match("I have 800 cukes and a") - require.NoError(t, err) - require.Nil(t, args) - args, err = expression.Match("I have 800 cukes and b") - require.NoError(t, err) - require.Nil(t, args) - args, err = expression.Match("I have 800 cukes and a|b") - require.NoError(t, err) - require.NotNil(t, args) - }) - }) - t.Run("unmatched optional groups have nil values", func(t *testing.T) { parameterTypeRegistry := NewParameterTypeRegistry() colorParameterType, err := NewParameterType( @@ -358,6 +207,10 @@ func MatchCucumberExpression(t *testing.T, expr string, text string, typeHints . require.NoError(t, err) args, err := expression.Match(text, typeHints...) require.NoError(t, err) + return argumentValues(args) +} + +func argumentValues(args []*Argument) []interface{} { if args == nil { return nil } diff --git a/cucumber-expressions/go/cucumber_expression_tokenizer.go b/cucumber-expressions/go/cucumber_expression_tokenizer.go new file mode 100644 index 0000000000..c99dd838b4 --- /dev/null +++ b/cucumber-expressions/go/cucumber_expression_tokenizer.go @@ -0,0 +1,84 @@ +package cucumberexpressions + +func tokenize(expression string) ([]token, error) { + + tokens := make([]token, 0) + + runes := []rune(expression) + + var buffer []rune + previousTokenType := startOfLine + treatAsText := false + escaped := 0 + bufferStartIndex := 0 + + convertBufferToToken := func(tokenType tokenType) token { + escapeTokens := 0 + if tokenType == text { + escapeTokens = escaped + escaped = 0 + } + consumedIndex := bufferStartIndex + len(buffer) + escapeTokens + t := token{string(buffer), tokenType, bufferStartIndex, consumedIndex} + buffer = []rune{} + bufferStartIndex = consumedIndex + return t + } + + tokenTypeOf := func(r rune, treatAsText bool) (tokenType, error) { + if !treatAsText { + return typeOf(r) + } + if canEscape(r) { + return text, nil + } + return startOfLine, createCantEscaped(expression, bufferStartIndex+len(buffer)+escaped) + } + + shouldCreateNewToken := func(currentTokenType tokenType, previousTokenType tokenType) bool { + if currentTokenType != previousTokenType { + return true + } + return currentTokenType != whiteSpace && currentTokenType != text + } + + if len(runes) == 0 { + tokens = append(tokens, token{"", startOfLine, 0, 0}) + } + + for _, r := range runes { + if !treatAsText && isEscapeCharacter(r) { + escaped++ + treatAsText = true + continue + } + + currentTokenType, err := tokenTypeOf(r, treatAsText) + if err != nil { + return nil, err + } + treatAsText = false + + if shouldCreateNewToken(currentTokenType, previousTokenType) { + token := convertBufferToToken(previousTokenType) + previousTokenType = currentTokenType + buffer = append(buffer, r) + tokens = append(tokens, token) + } else { + previousTokenType = currentTokenType + buffer = append(buffer, r) + } + } + + if len(buffer) > 0 { + token := convertBufferToToken(previousTokenType) + tokens = append(tokens, token) + } + + if treatAsText { + return nil, createTheEndOfLineCanNotBeEscaped(expression) + } + token := token{"", endOfLine, len(runes), len(runes)} + tokens = append(tokens, token) + return tokens, nil +} diff --git a/cucumber-expressions/go/cucumber_expression_tokenizer_test.go b/cucumber-expressions/go/cucumber_expression_tokenizer_test.go new file mode 100644 index 0000000000..f7a38470c0 --- /dev/null +++ b/cucumber-expressions/go/cucumber_expression_tokenizer_test.go @@ -0,0 +1,55 @@ +package cucumberexpressions + +import ( + "encoding/json" + "fmt" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + "io/ioutil" + "testing" +) + +type expectation struct { + Expression string `yaml:"expression"` + Text string `yaml:"text"` + Expected string `yaml:"expected"` + Exception string `yaml:"exception"` +} + +func TestCucumberExpressionTokenizer(t *testing.T) { + + directory := "testdata/tokens/" + files, err := ioutil.ReadDir(directory) + require.NoError(t, err) + + for _, file := range files { + contents, err := ioutil.ReadFile(directory + file.Name()) + require.NoError(t, err) + t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { + var expectation expectation + err = yaml.Unmarshal(contents, &expectation) + require.NoError(t, err) + + if expectation.Exception == "" { + var token []token + err = json.Unmarshal([]byte(expectation.Expected), &token) + require.NoError(t, err) + assertTokenizes(t, token, expectation.Expression) + } else { + assertThrows(t, expectation.Exception, expectation.Expression) + } + }) + } +} + +func assertTokenizes(t *testing.T, expected []token, expression string) { + tokens, err := tokenize(expression) + require.NoError(t, err) + require.Equal(t, expected, tokens) +} + +func assertThrows(t *testing.T, expected string, expression string) { + _, err := tokenize(expression) + require.Error(t, err) + require.Equal(t, expected, err.Error()) +} diff --git a/cucumber-expressions/go/custom_parameter_type_test.go b/cucumber-expressions/go/custom_parameter_type_test.go index b6f7df954a..6b7837dad7 100644 --- a/cucumber-expressions/go/custom_parameter_type_test.go +++ b/cucumber-expressions/go/custom_parameter_type_test.go @@ -42,7 +42,7 @@ func CreateParameterTypeRegistry(t *testing.T) *ParameterTypeRegistry { func TestCustomParameterTypes(t *testing.T) { t.Run("throws exception for illegal character in parameter type name", func(t *testing.T) { _, err := NewParameterType( - "[string]", + "{string}", []*regexp.Regexp{regexp.MustCompile(`.*`)}, "x", func(args ...*string) interface{} { @@ -53,7 +53,7 @@ func TestCustomParameterTypes(t *testing.T) { false, ) require.Error(t, err) - require.Equal(t, "illegal character '[' in parameter name {[string]}", err.Error()) + require.Equal(t, "Illegal character in parameter name {{string}}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", err.Error()) }) t.Run("CucumberExpression", func(t *testing.T) { diff --git a/cucumber-expressions/go/errors.go b/cucumber-expressions/go/errors.go index 754a444a6a..811e6421d6 100644 --- a/cucumber-expressions/go/errors.go +++ b/cucumber-expressions/go/errors.go @@ -3,6 +3,7 @@ package cucumberexpressions import ( "fmt" "strings" + "unicode/utf8" ) type CucumberExpressionError struct { @@ -17,6 +18,154 @@ func (e *CucumberExpressionError) Error() string { return e.s } +func createMissingEndToken(expression string, beginToken tokenType, endToken tokenType, current token) error { + return NewCucumberExpressionError(message( + current.Start, + expression, + pointAtToken(current), + "The '"+symbol(beginToken)+"' does not have a matching '"+symbol(endToken)+"'", + "If you did not intend to use "+purpose(beginToken)+" you can use '\\"+symbol(beginToken)+"' to escape the "+purpose(beginToken), + )) +} + +func createAlternationNotAllowedInOptional(expression string, current token) error { + return NewCucumberExpressionError(message( + current.Start, + expression, + pointAtToken(current), + "An alternation can not be used inside an optional", + "You can use '\\/' to escape the the '/'", + )) +} + +func createTheEndOfLineCanNotBeEscaped(expression string) error { + index := utf8.RuneCountInString(expression) - 1 + return NewCucumberExpressionError(message( + index, + expression, + pointAt(index), + "The end of line can not be escaped", + "You can use '\\\\' to escape the the '\\'", + )) +} + +func createOptionalMayNotBeEmpty(node node, expression string) error { + return NewCucumberExpressionError(message( + node.Start, + expression, + pointAtNode(node), + "An optional must contain some text", + "If you did not mean to use an optional you can use '\\(' to escape the the '('", + )) +} +func createParameterIsNotAllowedInOptional(node node, expression string) error { + return NewCucumberExpressionError(message( + node.Start, + expression, + pointAtNode(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 '{'", + )) +} +func createOptionalIsNotAllowedInOptional(node node, expression string) error { + return NewCucumberExpressionError(message( + node.Start, + expression, + pointAtNode(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.", + )) +} +func createAlternativeMayNotBeEmpty(node node, expression string) error { + return NewCucumberExpressionError(message( + node.Start, + expression, + pointAtNode(node), + "Alternative may not be empty", + "If you did not mean to use an alternative you can use '\\/' to escape the the '/'", + )) +} +func createAlternativeMayNotExclusivelyContainOptionals(node node, expression string) error { + return NewCucumberExpressionError(message( + node.Start, + expression, + pointAtNode(node), + "An alternative may not exclusively contain optionals", + "If you did not mean to use an optional you can use '\\(' to escape the the '('", + )) +} + +func createCantEscaped(expression string, index int) error { + return NewCucumberExpressionError(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", + )) +} + +func createInvalidParameterTypeName(typeName string) error { + return NewCucumberExpressionError("Illegal character in parameter name {" + typeName + "}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'") +} + +// Not very clear, but this message has to be language independent +func createInvalidParameterTypeNameInNode(token token, expression string) error { + return NewCucumberExpressionError(message( + token.Start, + expression, + pointAtToken(token), + "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", + "Did you mean to use a regular expression?", + )) +} + +func pointAt(index int) string { + pointer := strings.Builder{} + for i := 0; i < index; i++ { + pointer.WriteString(" ") + } + pointer.WriteString("^") + return pointer.String() +} + +func pointAtToken(node token) string { + pointer := strings.Builder{} + pointer.WriteString(pointAt(node.Start)) + if node.Start+1 < node.End { + for i := node.Start + 1; i < node.End-1; i++ { + pointer.WriteString("-") + } + pointer.WriteString("^") + } + return pointer.String() +} + +func pointAtNode(node node) string { + pointer := strings.Builder{} + pointer.WriteString(pointAt(node.Start)) + if node.Start+1 < node.End { + for i := node.Start + 1; i < node.End-1; i++ { + pointer.WriteString("-") + } + pointer.WriteString("^") + } + return pointer.String() +} + +func message(index int, expression string, pointer string, problem string, solution string) string { + return thisCucumberExpressionHasAProblemAt(index) + + "\n" + + expression + "\n" + + pointer + "\n" + + problem + ".\n" + + solution +} + +func thisCucumberExpressionHasAProblemAt(index int) string { + return fmt.Sprintf("This Cucumber Expression has a problem at column %d:\n", index+1) +} + type AmbiguousParameterTypeError struct { s string } @@ -60,10 +209,19 @@ type UndefinedParameterTypeError struct { s string } -func NewUndefinedParameterTypeError(typeName string) error { - return &UndefinedParameterTypeError{s: fmt.Sprintf("Undefined parameter type {%s}", typeName)} +func NewUndefinedParameterTypeError(message string) error { + return &UndefinedParameterTypeError{s: message} } func (e *UndefinedParameterTypeError) Error() string { return e.s } + +func createUndefinedParameterType(node node, expression string, undefinedParameterTypeName string) error { + return NewUndefinedParameterTypeError(message( + node.Start, + expression, + pointAtNode(node), + "Undefined parameter type '"+undefinedParameterTypeName+"'", + "Please register a ParameterType for '"+undefinedParameterTypeName+"'")) +} diff --git a/cucumber-expressions/go/examples.txt b/cucumber-expressions/go/examples.txt index 35b7bd17a9..3a8288d34d 100644 --- a/cucumber-expressions/go/examples.txt +++ b/cucumber-expressions/go/examples.txt @@ -2,7 +2,7 @@ I have {int} cuke(s) I have 22 cukes [22] --- -I have {int} cuke(s) and some \[]^$.|?*+ +I have {int} cuke(s) and some \\[]^$.|?*+ I have 1 cuke and some \[]^$.|?*+ [1] --- @@ -37,3 +37,7 @@ a purchase for $33 Some ${float} of cukes at {int}° Celsius Some $3.50 of cukes at 42° Celsius [3.5,42] +--- +I select the {int}st/nd/rd/th Cucumber +I select the 3rd Cucumber +[3] diff --git a/cucumber-expressions/go/go.mod b/cucumber-expressions/go/go.mod index 5739432f95..b32bc03ea3 100644 --- a/cucumber-expressions/go/go.mod +++ b/cucumber-expressions/go/go.mod @@ -6,6 +6,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/stretchr/testify v1.6.1 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) go 1.13 diff --git a/cucumber-expressions/go/parameter_type.go b/cucumber-expressions/go/parameter_type.go index 66b4e14130..ee4dbb6cd0 100644 --- a/cucumber-expressions/go/parameter_type.go +++ b/cucumber-expressions/go/parameter_type.go @@ -2,14 +2,12 @@ package cucumberexpressions import ( "errors" - "fmt" "reflect" "regexp" ) var HAS_FLAG_REGEXP = regexp.MustCompile(`\(\?[imsU-]+(:.*)?\)`) -var UNESCAPE_REGEXP = regexp.MustCompile(`(\\([\[$.|?*+\]]))`) -var ILLEGAL_PARAMETER_NAME_REGEXP = regexp.MustCompile(`([\[\]()$.|?*+])`) +var ILLEGAL_PARAMETER_NAME_REGEXP = regexp.MustCompile(`([{}()\\/])`) type ParameterType struct { name string @@ -22,14 +20,16 @@ type ParameterType struct { } func CheckParameterTypeName(typeName string) error { - unescapedTypeName := UNESCAPE_REGEXP.ReplaceAllString(typeName, "$2") - if ILLEGAL_PARAMETER_NAME_REGEXP.MatchString(typeName) { - c := ILLEGAL_PARAMETER_NAME_REGEXP.FindStringSubmatch(typeName)[0] - return fmt.Errorf("illegal character '%s' in parameter name {%s}", c, unescapedTypeName) + if !isValidParameterTypeName(typeName) { + return createInvalidParameterTypeName(typeName) } return nil } +func isValidParameterTypeName(typeName string) bool { + return !ILLEGAL_PARAMETER_NAME_REGEXP.MatchString(typeName) +} + func NewParameterType(name string, regexps []*regexp.Regexp, type1 string, transform func(...*string) interface{}, useForSnippets bool, preferForRegexpMatch bool, useRegexpMatchAsStrongTypeHint bool) (*ParameterType, error) { if transform == nil { transform = func(s ...*string) interface{} { diff --git a/cucumber-expressions/go/parameter_type_registry.go b/cucumber-expressions/go/parameter_type_registry.go index 155d60e16a..0371575cfb 100644 --- a/cucumber-expressions/go/parameter_type_registry.go +++ b/cucumber-expressions/go/parameter_type_registry.go @@ -5,6 +5,7 @@ import ( "reflect" "regexp" "sort" + "strings" ) var INTEGER_REGEXPS = []*regexp.Regexp{ @@ -98,14 +99,19 @@ func NewParameterTypeRegistry() *ParameterTypeRegistry { STRING_REGEXPS, "string", func(args ...*string) interface{} { - if args[0] == nil && args[1] != nil { - i, err := transformer.Transform(*args[1], reflect.String) - if err != nil { - panic(err) + matched := func(args []*string) string { + var value string + if args[0] == nil && args[1] != nil { + value = *args[1] + } else { + value = *args[0] } - return i + return value } - i, err := transformer.Transform(*args[0], reflect.String) + value := matched(args) + value = strings.ReplaceAll(value, "\\\"", "\"") + value = strings.ReplaceAll(value, "\\'", "'") + i, err := transformer.Transform(value, reflect.String) if err != nil { panic(err) } diff --git a/cucumber-expressions/go/testdata/ast/alternation-followed-by-optional.yaml b/cucumber-expressions/go/testdata/ast/alternation-followed-by-optional.yaml new file mode 100644 index 0000000000..0bfb53a534 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation-followed-by-optional.yaml @@ -0,0 +1,17 @@ +expression: three blind\ rat/cat(s) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 16, "token": "blind rat"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 17, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 17, "end": 20, "token": "cat"}, + {"type": "OPTIONAL_NODE", "start": 20, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": "s"} + ]} + ]} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/alternation-phrase.yaml b/cucumber-expressions/go/testdata/ast/alternation-phrase.yaml new file mode 100644 index 0000000000..9a243822d5 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation-phrase.yaml @@ -0,0 +1,16 @@ +expression: three hungry/blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 12, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 12, "token": "hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 13, "end": 18, "token": "blind"} + ]} + ]}, + {"type": "TEXT_NODE", "start": 18, "end": 19, "token": " "}, + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "mice"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/alternation-with-parameter.yaml b/cucumber-expressions/go/testdata/ast/alternation-with-parameter.yaml new file mode 100644 index 0000000000..c5daf32bd7 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation-with-parameter.yaml @@ -0,0 +1,27 @@ +expression: I select the {int}st/nd/rd/th +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "I"}, + {"type": "TEXT_NODE", "start": 1, "end": 2, "token": " "}, + {"type": "TEXT_NODE", "start": 2, "end": 8, "token": "select"}, + {"type": "TEXT_NODE", "start": 8, "end": 9, "token": " "}, + {"type": "TEXT_NODE", "start": 9, "end": 12, "token": "the"}, + {"type": "TEXT_NODE", "start": 12, "end": 13, "token": " "}, + {"type": "PARAMETER_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 14, "end": 17, "token": "int"} + ]}, + {"type": "ALTERNATION_NODE", "start": 18, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 18, "end": 20, "nodes": [ + {"type": "TEXT_NODE", "start": 18, "end": 20, "token": "st"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 21, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 23, "token": "nd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 24, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 24, "end": 26, "token": "rd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 27, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 27, "end": 29, "token": "th"} + ]} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/alternation-with-unused-end-optional.yaml b/cucumber-expressions/go/testdata/ast/alternation-with-unused-end-optional.yaml new file mode 100644 index 0000000000..842838b75f --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation-with-unused-end-optional.yaml @@ -0,0 +1,15 @@ +expression: three )blind\ mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 7, "token": ")"}, + {"type": "TEXT_NODE", "start": 7, "end": 18, "token": "blind mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 19, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/alternation-with-unused-start-optional.yaml b/cucumber-expressions/go/testdata/ast/alternation-with-unused-start-optional.yaml new file mode 100644 index 0000000000..e2f0584556 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation-with-unused-start-optional.yaml @@ -0,0 +1,8 @@ +expression: three blind\ mice/rats( +exception: |- + This Cucumber Expression has a problem at column 23: + + three blind\ mice/rats( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/go/testdata/ast/alternation-with-white-space.yaml b/cucumber-expressions/go/testdata/ast/alternation-with-white-space.yaml new file mode 100644 index 0000000000..eedd57dd21 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation-with-white-space.yaml @@ -0,0 +1,12 @@ +expression: '\ three\ hungry/blind\ mice\ ' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 15, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 15, "token": " three hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 16, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 16, "end": 29, "token": "blind mice "} + ]} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/alternation.yaml b/cucumber-expressions/go/testdata/ast/alternation.yaml new file mode 100644 index 0000000000..88df8325fe --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/alternation.yaml @@ -0,0 +1,12 @@ +expression: mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 4, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 4, "token": "mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 5, "end": 9, "nodes": [ + {"type": "TEXT_NODE", "start": 5, "end": 9, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/anonymous-parameter.yaml b/cucumber-expressions/go/testdata/ast/anonymous-parameter.yaml new file mode 100644 index 0000000000..2c4d339333 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/anonymous-parameter.yaml @@ -0,0 +1,5 @@ +expression: "{}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 2, "nodes": []} + ]} diff --git a/cucumber-expressions/go/testdata/ast/closing-brace.yaml b/cucumber-expressions/go/testdata/ast/closing-brace.yaml new file mode 100644 index 0000000000..1bafd9c6a8 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/closing-brace.yaml @@ -0,0 +1,5 @@ +expression: "}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "}"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/closing-parenthesis.yaml b/cucumber-expressions/go/testdata/ast/closing-parenthesis.yaml new file mode 100644 index 0000000000..23daf7bcd3 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/closing-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: ) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": ")"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/empty-alternation.yaml b/cucumber-expressions/go/testdata/ast/empty-alternation.yaml new file mode 100644 index 0000000000..6d810fc8f3 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/empty-alternation.yaml @@ -0,0 +1,8 @@ +expression: / +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/empty-alternations.yaml b/cucumber-expressions/go/testdata/ast/empty-alternations.yaml new file mode 100644 index 0000000000..f8d4dd4cf8 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/empty-alternations.yaml @@ -0,0 +1,9 @@ +expression: '//' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 2, "end": 2, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/empty-string.yaml b/cucumber-expressions/go/testdata/ast/empty-string.yaml new file mode 100644 index 0000000000..4d33c2dc76 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/empty-string.yaml @@ -0,0 +1,3 @@ +expression: "" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 0, "nodes": []} diff --git a/cucumber-expressions/go/testdata/ast/escaped-alternation.yaml b/cucumber-expressions/go/testdata/ast/escaped-alternation.yaml new file mode 100644 index 0000000000..3ed9c37674 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/escaped-alternation.yaml @@ -0,0 +1,5 @@ +expression: 'mice\/rats' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 10, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 10, "token": "mice/rats"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/escaped-backslash.yaml b/cucumber-expressions/go/testdata/ast/escaped-backslash.yaml new file mode 100644 index 0000000000..da2d008e1e --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/escaped-backslash.yaml @@ -0,0 +1,5 @@ +expression: '\\' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "\\"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/escaped-opening-parenthesis.yaml b/cucumber-expressions/go/testdata/ast/escaped-opening-parenthesis.yaml new file mode 100644 index 0000000000..afafc59eb8 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/escaped-opening-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: '\(' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "("} + ]} diff --git a/cucumber-expressions/go/testdata/ast/escaped-optional-followed-by-optional.yaml b/cucumber-expressions/go/testdata/ast/escaped-optional-followed-by-optional.yaml new file mode 100644 index 0000000000..1e4746291b --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/escaped-optional-followed-by-optional.yaml @@ -0,0 +1,15 @@ +expression: three \((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 8, "token": "("}, + {"type": "OPTIONAL_NODE", "start": 8, "end": 14, "nodes": [ + {"type": "TEXT_NODE", "start": 9, "end": 13, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 20, "token": "blind"}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": ")"}, + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": " "}, + {"type": "TEXT_NODE", "start": 22, "end": 26, "token": "mice"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/escaped-optional-phrase.yaml b/cucumber-expressions/go/testdata/ast/escaped-optional-phrase.yaml new file mode 100644 index 0000000000..832249e2a7 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/escaped-optional-phrase.yaml @@ -0,0 +1,10 @@ +expression: three \(blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 19, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 13, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": ")"}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 19, "token": "mice"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/escaped-optional.yaml b/cucumber-expressions/go/testdata/ast/escaped-optional.yaml new file mode 100644 index 0000000000..4c2b457d6f --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: '\(blind)' + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 7, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 7, "end": 8, "token": ")"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/opening-brace.yaml b/cucumber-expressions/go/testdata/ast/opening-brace.yaml new file mode 100644 index 0000000000..916a674a36 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/opening-brace.yaml @@ -0,0 +1,8 @@ +expression: '{' +exception: |- + This Cucumber Expression has a problem at column 1: + + { + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/go/testdata/ast/opening-parenthesis.yaml b/cucumber-expressions/go/testdata/ast/opening-parenthesis.yaml new file mode 100644 index 0000000000..929d6ae304 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/opening-parenthesis.yaml @@ -0,0 +1,8 @@ +expression: ( +exception: |- + This Cucumber Expression has a problem at column 1: + + ( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/go/testdata/ast/optional-containing-nested-optional.yaml b/cucumber-expressions/go/testdata/ast/optional-containing-nested-optional.yaml new file mode 100644 index 0000000000..0fdd55d46b --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/optional-containing-nested-optional.yaml @@ -0,0 +1,15 @@ +expression: three ((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 25, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 20, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 7, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 8, "end": 12, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 19, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": " "}, + {"type": "TEXT_NODE", "start": 21, "end": 25, "token": "mice"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/optional-phrase.yaml b/cucumber-expressions/go/testdata/ast/optional-phrase.yaml new file mode 100644 index 0000000000..0ef4fb3f95 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/optional-phrase.yaml @@ -0,0 +1,12 @@ +expression: three (blind) mice + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 7, "end": 12, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 18, "token": "mice"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/optional.yaml b/cucumber-expressions/go/testdata/ast/optional.yaml new file mode 100644 index 0000000000..6ce2b632e7 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/optional.yaml @@ -0,0 +1,7 @@ +expression: (blind) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 6, "token": "blind"} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/parameter.yaml b/cucumber-expressions/go/testdata/ast/parameter.yaml new file mode 100644 index 0000000000..92ac8c147e --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/parameter.yaml @@ -0,0 +1,7 @@ +expression: "{string}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 7, "token": "string"} + ]} + ]} diff --git a/cucumber-expressions/go/testdata/ast/phrase.yaml b/cucumber-expressions/go/testdata/ast/phrase.yaml new file mode 100644 index 0000000000..ba340d0122 --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/phrase.yaml @@ -0,0 +1,9 @@ +expression: three blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 11, "token": "blind"}, + {"type": "TEXT_NODE", "start": 11, "end": 12, "token": " "}, + {"type": "TEXT_NODE", "start": 12, "end": 16, "token": "mice"} + ]} diff --git a/cucumber-expressions/go/testdata/ast/unfinished-parameter.yaml b/cucumber-expressions/go/testdata/ast/unfinished-parameter.yaml new file mode 100644 index 0000000000..d02f9b4ccf --- /dev/null +++ b/cucumber-expressions/go/testdata/ast/unfinished-parameter.yaml @@ -0,0 +1,8 @@ +expression: "{string" +exception: |- + This Cucumber Expression has a problem at column 1: + + {string + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/go/testdata/expression/allows-escaped-optional-parameter-types.yaml b/cucumber-expressions/go/testdata/expression/allows-escaped-optional-parameter-types.yaml new file mode 100644 index 0000000000..a00b45acef --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/allows-escaped-optional-parameter-types.yaml @@ -0,0 +1,4 @@ +expression: \({int}) +text: (3) +expected: |- + [3] diff --git a/cucumber-expressions/go/testdata/expression/allows-parameter-type-in-alternation-1.yaml b/cucumber-expressions/go/testdata/expression/allows-parameter-type-in-alternation-1.yaml new file mode 100644 index 0000000000..bb1a6f21b1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/allows-parameter-type-in-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: i18n +expected: |- + [18] diff --git a/cucumber-expressions/go/testdata/expression/allows-parameter-type-in-alternation-2.yaml b/cucumber-expressions/go/testdata/expression/allows-parameter-type-in-alternation-2.yaml new file mode 100644 index 0000000000..cdddce7d84 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/allows-parameter-type-in-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: a11y +expected: |- + [11] diff --git a/cucumber-expressions/go/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml b/cucumber-expressions/go/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml new file mode 100644 index 0000000000..9e2ecdfbe1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml @@ -0,0 +1,5 @@ +expression: |- + {int}st/nd/rd/th +text: 3rd +expected: |- + [3] diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-in-optional.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-in-optional.yaml new file mode 100644 index 0000000000..b507e27220 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-in-optional.yaml @@ -0,0 +1,9 @@ +expression: three( brown/black) mice +text: three brown/black mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three( brown/black) mice + ^ + An alternation can not be used inside an optional. + You can use '\/' to escape the the '/' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml new file mode 100644 index 0000000000..b32540a4a9 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml @@ -0,0 +1,10 @@ +expression: |- + {int}/x +text: '3' +exception: |- + This Cucumber Expression has a problem at column 6: + + {int}/x + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml new file mode 100644 index 0000000000..a0aab0e5a9 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml @@ -0,0 +1,9 @@ +expression: three (brown)/black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (brown)/black mice + ^-----^ + An alternative may not exclusively contain optionals. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml new file mode 100644 index 0000000000..50250f00aa --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml @@ -0,0 +1,9 @@ +expression: x/{int} +text: '3' +exception: |- + This Cucumber Expression has a problem at column 3: + + x/{int} + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml new file mode 100644 index 0000000000..b724cfa77f --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml @@ -0,0 +1,9 @@ +expression: three brown//black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three brown//black mice + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-empty-optional.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-empty-optional.yaml new file mode 100644 index 0000000000..00e341af0b --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-empty-optional.yaml @@ -0,0 +1,9 @@ +expression: three () mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three () mice + ^^ + An optional must contain some text. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-nested-optional.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-nested-optional.yaml new file mode 100644 index 0000000000..017c3be25d --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-nested-optional.yaml @@ -0,0 +1,8 @@ +expression: "(a(b))" +exception: |- + This Cucumber Expression has a problem at column 3: + + (a(b)) + ^-^ + 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. diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-optional-parameter-types.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-optional-parameter-types.yaml new file mode 100644 index 0000000000..b88061e9b4 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-optional-parameter-types.yaml @@ -0,0 +1,9 @@ +expression: ({int}) +text: '3' +exception: |- + This Cucumber Expression has a problem at column 2: + + ({int}) + ^---^ + 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 '{' diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml new file mode 100644 index 0000000000..d1c89689e9 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml @@ -0,0 +1,10 @@ +expression: |- + {(string)} +text: something +exception: |- + This Cucumber Expression has a problem at column 2: + + {(string)} + ^ + Parameter names may not contain '{', '}', '(', ')', '\' or '/'. + Did you mean to use a regular expression? diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml new file mode 100644 index 0000000000..e033648972 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string\} mice +exception: |- + This Cucumber Expression has a problem at column 24: + + three (exceptionally\) {string\} mice + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml new file mode 100644 index 0000000000..7cb9c6d56a --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string} mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (exceptionally\) {string} mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml b/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml new file mode 100644 index 0000000000..029c4e63bd --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml @@ -0,0 +1,8 @@ +expression: three ((exceptionally\) strong) mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three ((exceptionally\) strong) mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/go/testdata/expression/does-not-match-misquoted-string.yaml b/cucumber-expressions/go/testdata/expression/does-not-match-misquoted-string.yaml new file mode 100644 index 0000000000..18023180af --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/does-not-match-misquoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind' mice +expected: |- + null diff --git a/cucumber-expressions/go/testdata/expression/doesnt-match-float-as-int.yaml b/cucumber-expressions/go/testdata/expression/doesnt-match-float-as-int.yaml new file mode 100644 index 0000000000..d66b586430 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/doesnt-match-float-as-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '1.22' +expected: |- + null diff --git a/cucumber-expressions/go/testdata/expression/matches-alternation.yaml b/cucumber-expressions/go/testdata/expression/matches-alternation.yaml new file mode 100644 index 0000000000..20a9b9a728 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-alternation.yaml @@ -0,0 +1,4 @@ +expression: mice/rats and rats\/mice +text: rats and rats/mice +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-anonymous-parameter-type.yaml b/cucumber-expressions/go/testdata/expression/matches-anonymous-parameter-type.yaml new file mode 100644 index 0000000000..fc954960df --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-anonymous-parameter-type.yaml @@ -0,0 +1,5 @@ +expression: |- + {} +text: '0.22' +expected: |- + ["0.22"] diff --git a/cucumber-expressions/go/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/go/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c3e1962e22 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "" and "handsome" mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/go/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/go/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..89315b62ef --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "" mice +expected: |- + [""] diff --git a/cucumber-expressions/go/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml b/cucumber-expressions/go/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml new file mode 100644 index 0000000000..fe30a044c1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "bl\"nd" mice +expected: |- + ["bl\"nd"] diff --git a/cucumber-expressions/go/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml b/cucumber-expressions/go/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml new file mode 100644 index 0000000000..25fcf304c1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "'blind'" mice +expected: |- + ["'blind'"] diff --git a/cucumber-expressions/go/testdata/expression/matches-double-quoted-string.yaml b/cucumber-expressions/go/testdata/expression/matches-double-quoted-string.yaml new file mode 100644 index 0000000000..8b0560f332 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-double-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-parenthesis.yaml b/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-parenthesis.yaml new file mode 100644 index 0000000000..902a084103 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-parenthesis.yaml @@ -0,0 +1,4 @@ +expression: three \\(exceptionally) \\{string} mice +text: three \exceptionally \"blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-slash-1.yaml b/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-slash-1.yaml new file mode 100644 index 0000000000..94e531eca7 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-slash-1.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: 12\ +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-slash-2.yaml b/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-slash-2.yaml new file mode 100644 index 0000000000..9c9c735b86 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-doubly-escaped-slash-2.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: '2020' +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-1.yaml b/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-1.yaml new file mode 100644 index 0000000000..171df89ee1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-1.yaml @@ -0,0 +1,4 @@ +expression: three \(exceptionally) \{string} mice +text: three (exceptionally) {string} mice +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-2.yaml b/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-2.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-2.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-3.yaml b/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-3.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-escaped-parenthesis-3.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/go/testdata/expression/matches-escaped-slash.yaml b/cucumber-expressions/go/testdata/expression/matches-escaped-slash.yaml new file mode 100644 index 0000000000..d8b3933399 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-escaped-slash.yaml @@ -0,0 +1,4 @@ +expression: 12\/2020 +text: 12/2020 +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-float-1.yaml b/cucumber-expressions/go/testdata/expression/matches-float-1.yaml new file mode 100644 index 0000000000..fe7e8b1869 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-float-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '0.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/go/testdata/expression/matches-float-2.yaml b/cucumber-expressions/go/testdata/expression/matches-float-2.yaml new file mode 100644 index 0000000000..c1e5894eac --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-float-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/go/testdata/expression/matches-int.yaml b/cucumber-expressions/go/testdata/expression/matches-int.yaml new file mode 100644 index 0000000000..bcd3763886 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '22' +expected: |- + [22] diff --git a/cucumber-expressions/go/testdata/expression/matches-multiple-double-quoted-strings.yaml b/cucumber-expressions/go/testdata/expression/matches-multiple-double-quoted-strings.yaml new file mode 100644 index 0000000000..6c74bc2350 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-multiple-double-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "blind" and "crippled" mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/go/testdata/expression/matches-multiple-single-quoted-strings.yaml b/cucumber-expressions/go/testdata/expression/matches-multiple-single-quoted-strings.yaml new file mode 100644 index 0000000000..5037821c14 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-multiple-single-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three 'blind' and 'crippled' mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-1.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-1.yaml new file mode 100644 index 0000000000..821776715b --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three brown mice +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-2.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-2.yaml new file mode 100644 index 0000000000..71b3a341f1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three rats +expected: |- + [] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml new file mode 100644 index 0000000000..2632f410ce --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 2 seconds? +expected: |- + [2] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml new file mode 100644 index 0000000000..7b30f667bc --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 1 second. +expected: |- + [1] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-1.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-1.yaml new file mode 100644 index 0000000000..6574bb4bdf --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 3 rats +expected: |- + [3] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-2.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-2.yaml new file mode 100644 index 0000000000..4eb0f0e1c6 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 2 mice +expected: |- + [2] diff --git a/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-3.yaml b/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-3.yaml new file mode 100644 index 0000000000..964fa6489e --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-optional-in-alternation-3.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 1 mouse +expected: |- + [1] diff --git a/cucumber-expressions/go/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/go/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c963dcf1c7 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three '' and 'handsome' mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/go/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/go/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..cff2a2d1ec --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '' mice +expected: |- + [""] diff --git a/cucumber-expressions/go/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml b/cucumber-expressions/go/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml new file mode 100644 index 0000000000..eb9ed537cd --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '"blind"' mice +expected: |- + ["\"blind\""] diff --git a/cucumber-expressions/go/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml b/cucumber-expressions/go/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml new file mode 100644 index 0000000000..4c2b0055b9 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'bl\'nd' mice +expected: |- + ["bl'nd"] diff --git a/cucumber-expressions/go/testdata/expression/matches-single-quoted-string.yaml b/cucumber-expressions/go/testdata/expression/matches-single-quoted-string.yaml new file mode 100644 index 0000000000..6c8f4652a5 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-single-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'blind' mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/go/testdata/expression/matches-word.yaml b/cucumber-expressions/go/testdata/expression/matches-word.yaml new file mode 100644 index 0000000000..358fd3afd1 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/matches-word.yaml @@ -0,0 +1,4 @@ +expression: three {word} mice +text: three blind mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/go/testdata/expression/throws-unknown-parameter-type.yaml b/cucumber-expressions/go/testdata/expression/throws-unknown-parameter-type.yaml new file mode 100644 index 0000000000..384e3a48c3 --- /dev/null +++ b/cucumber-expressions/go/testdata/expression/throws-unknown-parameter-type.yaml @@ -0,0 +1,10 @@ +expression: |- + {unknown} +text: something +exception: |- + This Cucumber Expression has a problem at column 1: + + {unknown} + ^-------^ + Undefined parameter type 'unknown'. + Please register a ParameterType for 'unknown' diff --git a/cucumber-expressions/go/testdata/regex/alternation-with-optional.yaml b/cucumber-expressions/go/testdata/regex/alternation-with-optional.yaml new file mode 100644 index 0000000000..73787b2b0a --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/alternation-with-optional.yaml @@ -0,0 +1,2 @@ +expression: "a/b(c)" +expected: ^(?:a|b(?:c)?)$ diff --git a/cucumber-expressions/go/testdata/regex/alternation.yaml b/cucumber-expressions/go/testdata/regex/alternation.yaml new file mode 100644 index 0000000000..1dc293fb62 --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/alternation.yaml @@ -0,0 +1,2 @@ +expression: "a/b c/d/e" +expected: ^(?:a|b) (?:c|d|e)$ diff --git a/cucumber-expressions/go/testdata/regex/empty.yaml b/cucumber-expressions/go/testdata/regex/empty.yaml new file mode 100644 index 0000000000..bb9a81906c --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/empty.yaml @@ -0,0 +1,2 @@ +expression: "" +expected: ^$ diff --git a/cucumber-expressions/go/testdata/regex/escape-regex-characters.yaml b/cucumber-expressions/go/testdata/regex/escape-regex-characters.yaml new file mode 100644 index 0000000000..c8ea8c549e --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/escape-regex-characters.yaml @@ -0,0 +1,2 @@ +expression: '^$[]\(\){}\\.|?*+' +expected: ^\^\$\[\]\(\)(.*)\\\.\|\?\*\+$ diff --git a/cucumber-expressions/go/testdata/regex/optional.yaml b/cucumber-expressions/go/testdata/regex/optional.yaml new file mode 100644 index 0000000000..7d6d84cc14 --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/optional.yaml @@ -0,0 +1,2 @@ +expression: "(a)" +expected: ^(?:a)?$ diff --git a/cucumber-expressions/go/testdata/regex/parameter.yaml b/cucumber-expressions/go/testdata/regex/parameter.yaml new file mode 100644 index 0000000000..f793b21c0f --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/parameter.yaml @@ -0,0 +1,2 @@ +expression: "{int}" +expected: ^((?:-?\d+)|(?:\d+))$ diff --git a/cucumber-expressions/go/testdata/regex/text.yaml b/cucumber-expressions/go/testdata/regex/text.yaml new file mode 100644 index 0000000000..2af3e41664 --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/text.yaml @@ -0,0 +1,2 @@ +expression: "a" +expected: ^a$ diff --git a/cucumber-expressions/go/testdata/regex/unicode.yaml b/cucumber-expressions/go/testdata/regex/unicode.yaml new file mode 100644 index 0000000000..f93fe35db1 --- /dev/null +++ b/cucumber-expressions/go/testdata/regex/unicode.yaml @@ -0,0 +1,2 @@ +expression: "Привет, Мир(ы)!" +expected: ^Привет, Мир(?:ы)?!$ diff --git a/cucumber-expressions/go/testdata/tokens/alternation-phrase.yaml b/cucumber-expressions/go/testdata/tokens/alternation-phrase.yaml new file mode 100644 index 0000000000..48b107f64e --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/alternation-phrase.yaml @@ -0,0 +1,13 @@ +expression: three blind/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "ALTERNATION", "start": 11, "end": 12, "text": "/"}, + {"type": "TEXT", "start": 12, "end": 19, "text": "cripple"}, + {"type": "WHITE_SPACE", "start": 19, "end": 20, "text": " "}, + {"type": "TEXT", "start": 20, "end": 24, "text": "mice"}, + {"type": "END_OF_LINE", "start": 24, "end": 24, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/alternation.yaml b/cucumber-expressions/go/testdata/tokens/alternation.yaml new file mode 100644 index 0000000000..a4920f22e5 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/alternation.yaml @@ -0,0 +1,9 @@ +expression: blind/cripple +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "blind"}, + {"type": "ALTERNATION", "start": 5, "end": 6, "text": "/"}, + {"type": "TEXT", "start": 6, "end": 13, "text": "cripple"}, + {"type": "END_OF_LINE", "start": 13, "end": 13, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/empty-string.yaml b/cucumber-expressions/go/testdata/tokens/empty-string.yaml new file mode 100644 index 0000000000..501f7522f2 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/empty-string.yaml @@ -0,0 +1,6 @@ +expression: "" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "END_OF_LINE", "start": 0, "end": 0, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/escape-non-reserved-character.yaml b/cucumber-expressions/go/testdata/tokens/escape-non-reserved-character.yaml new file mode 100644 index 0000000000..5e206be084 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escape-non-reserved-character.yaml @@ -0,0 +1,8 @@ +expression: \[ +exception: |- + This Cucumber Expression has a problem at column 2: + + \[ + ^ + Only the characters '{', '}', '(', ')', '\', '/' and whitespace can be escaped. + If you did mean to use an '\' you can use '\\' to escape it diff --git a/cucumber-expressions/go/testdata/tokens/escaped-alternation.yaml b/cucumber-expressions/go/testdata/tokens/escaped-alternation.yaml new file mode 100644 index 0000000000..7e21f7ad19 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escaped-alternation.yaml @@ -0,0 +1,9 @@ +expression: blind\ and\ famished\/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 29, "text": "blind and famished/cripple"}, + {"type": "WHITE_SPACE", "start": 29, "end": 30, "text": " "}, + {"type": "TEXT", "start": 30, "end": 34, "text": "mice"}, + {"type": "END_OF_LINE", "start": 34, "end": 34, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml b/cucumber-expressions/go/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml new file mode 100644 index 0000000000..6375ad52a5 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml @@ -0,0 +1,9 @@ +expression: ' \/ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "WHITE_SPACE", "start": 0, "end": 1, "text": " "}, + {"type": "TEXT", "start": 1, "end": 3, "text": "/"}, + {"type": "WHITE_SPACE", "start": 3, "end": 4, "text": " "}, + {"type": "END_OF_LINE", "start": 4, "end": 4, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/escaped-end-of-line.yaml b/cucumber-expressions/go/testdata/tokens/escaped-end-of-line.yaml new file mode 100644 index 0000000000..a1bd00fd98 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escaped-end-of-line.yaml @@ -0,0 +1,8 @@ +expression: \ +exception: |- + This Cucumber Expression has a problem at column 1: + + \ + ^ + The end of line can not be escaped. + You can use '\\' to escape the the '\' diff --git a/cucumber-expressions/go/testdata/tokens/escaped-optional.yaml b/cucumber-expressions/go/testdata/tokens/escaped-optional.yaml new file mode 100644 index 0000000000..2b365b581c --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: \(blind\) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 9, "text": "(blind)"}, + {"type": "END_OF_LINE", "start": 9, "end": 9, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/escaped-parameter.yaml b/cucumber-expressions/go/testdata/tokens/escaped-parameter.yaml new file mode 100644 index 0000000000..2cdac4f35a --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escaped-parameter.yaml @@ -0,0 +1,7 @@ +expression: \{string\} +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 10, "text": "{string}"}, + {"type": "END_OF_LINE", "start": 10, "end": 10, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/escaped-space.yaml b/cucumber-expressions/go/testdata/tokens/escaped-space.yaml new file mode 100644 index 0000000000..912827a941 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/escaped-space.yaml @@ -0,0 +1,7 @@ +expression: '\ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 2, "text": " "}, + {"type": "END_OF_LINE", "start": 2, "end": 2, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/optional-phrase.yaml b/cucumber-expressions/go/testdata/tokens/optional-phrase.yaml new file mode 100644 index 0000000000..2ddc6bb502 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/optional-phrase.yaml @@ -0,0 +1,13 @@ +expression: three (blind) mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_OPTIONAL", "start": 6, "end": 7, "text": "("}, + {"type": "TEXT", "start": 7, "end": 12, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 12, "end": 13, "text": ")"}, + {"type": "WHITE_SPACE", "start": 13, "end": 14, "text": " "}, + {"type": "TEXT", "start": 14, "end": 18, "text": "mice"}, + {"type": "END_OF_LINE", "start": 18, "end": 18, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/optional.yaml b/cucumber-expressions/go/testdata/tokens/optional.yaml new file mode 100644 index 0000000000..35b1474a7c --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/optional.yaml @@ -0,0 +1,9 @@ +expression: (blind) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_OPTIONAL", "start": 0, "end": 1, "text": "("}, + {"type": "TEXT", "start": 1, "end": 6, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 6, "end": 7, "text": ")"}, + {"type": "END_OF_LINE", "start": 7, "end": 7, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/parameter-phrase.yaml b/cucumber-expressions/go/testdata/tokens/parameter-phrase.yaml new file mode 100644 index 0000000000..5e98055ee6 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/parameter-phrase.yaml @@ -0,0 +1,13 @@ +expression: three {string} mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_PARAMETER", "start": 6, "end": 7, "text": "{"}, + {"type": "TEXT", "start": 7, "end": 13, "text": "string"}, + {"type": "END_PARAMETER", "start": 13, "end": 14, "text": "}"}, + {"type": "WHITE_SPACE", "start": 14, "end": 15, "text": " "}, + {"type": "TEXT", "start": 15, "end": 19, "text": "mice"}, + {"type": "END_OF_LINE", "start": 19, "end": 19, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/parameter.yaml b/cucumber-expressions/go/testdata/tokens/parameter.yaml new file mode 100644 index 0000000000..460363c393 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/parameter.yaml @@ -0,0 +1,9 @@ +expression: "{string}" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_PARAMETER", "start": 0, "end": 1, "text": "{"}, + {"type": "TEXT", "start": 1, "end": 7, "text": "string"}, + {"type": "END_PARAMETER", "start": 7, "end": 8, "text": "}"}, + {"type": "END_OF_LINE", "start": 8, "end": 8, "text": ""} + ] diff --git a/cucumber-expressions/go/testdata/tokens/phrase.yaml b/cucumber-expressions/go/testdata/tokens/phrase.yaml new file mode 100644 index 0000000000..e2cfccf7b4 --- /dev/null +++ b/cucumber-expressions/go/testdata/tokens/phrase.yaml @@ -0,0 +1,11 @@ +expression: three blind mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "WHITE_SPACE", "start": 11, "end": 12, "text": " "}, + {"type": "TEXT", "start": 12, "end": 16, "text": "mice"}, + {"type": "END_OF_LINE", "start": 16, "end": 16, "text": ""} + ] diff --git a/cucumber-expressions/java/.rsync b/cucumber-expressions/java/.rsync index 74e8b453f4..a27b58d5a7 100644 --- a/cucumber-expressions/java/.rsync +++ b/cucumber-expressions/java/.rsync @@ -2,3 +2,4 @@ ../../.templates/github/ .github/ ../../.templates/java/ . ../examples.txt examples.txt +../testdata . diff --git a/cucumber-expressions/java/examples.txt b/cucumber-expressions/java/examples.txt index 35b7bd17a9..3a8288d34d 100644 --- a/cucumber-expressions/java/examples.txt +++ b/cucumber-expressions/java/examples.txt @@ -2,7 +2,7 @@ I have {int} cuke(s) I have 22 cukes [22] --- -I have {int} cuke(s) and some \[]^$.|?*+ +I have {int} cuke(s) and some \\[]^$.|?*+ I have 1 cuke and some \[]^$.|?*+ [1] --- @@ -37,3 +37,7 @@ a purchase for $33 Some ${float} of cukes at {int}° Celsius Some $3.50 of cukes at 42° Celsius [3.5,42] +--- +I select the {int}st/nd/rd/th Cucumber +I select the 3rd Cucumber +[3] diff --git a/cucumber-expressions/java/pom.xml b/cucumber-expressions/java/pom.xml index c191bb422e..03bd32fffe 100644 --- a/cucumber-expressions/java/pom.xml +++ b/cucumber-expressions/java/pom.xml @@ -57,6 +57,12 @@ 2.12.0 test + + org.yaml + snakeyaml + 1.21 + test + org.junit.jupiter junit-jupiter-engine diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java index b5d6549f44..3dd15413bc 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Argument.java @@ -11,17 +11,13 @@ public final class Argument { private final ParameterType parameterType; private final Group group; - static List> build(Group group, TreeRegexp treeRegexp, List> parameterTypes) { + static List> build(Group group, List> parameterTypes) { List argGroups = group.getChildren(); if (argGroups.size() != parameterTypes.size()) { - throw new CucumberExpressionException(String.format("Expression /%s/ has %s capture groups (%s), but there were %s parameter types (%s)", - treeRegexp.pattern().pattern(), - argGroups.size(), - getGroupValues(argGroups), - parameterTypes.size(), - getParameterTypeNames(parameterTypes) - )); + // This requires regex injection through a Cucumber expression. + // Regex injection should be be possible any more. + throw new IllegalArgumentException(String.format("Group has %s capture groups, but there were %s parameter types", argGroups.size(), parameterTypes.size())); } List> args = new ArrayList<>(argGroups.size()); for (int i = 0; i < parameterTypes.size(); i++) { @@ -33,24 +29,6 @@ static List> build(Group group, TreeRegexp treeRegexp, List getParameterTypeNames(List> parameterTypes) { - List list = new ArrayList<>(); - for (ParameterType type : parameterTypes) { - String name = type.getName(); - list.add(name); - } - return list; - } - - private static List getGroupValues(List argGroups) { - List list = new ArrayList<>(); - for (Group argGroup : argGroups) { - String value = argGroup.getValue(); - list.add(value); - } - return list; - } - private Argument(Group group, ParameterType parameterType) { this.group = group; this.parameterType = parameterType; diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java new file mode 100644 index 0000000000..ba5eb4d5ee --- /dev/null +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/Ast.java @@ -0,0 +1,270 @@ +package io.cucumber.cucumberexpressions; + +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +import static java.util.Arrays.asList; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +final class Ast { + + private static final char escapeCharacter = '\\'; + private static final char alternationCharacter = '/'; + private static final char beginParameterCharacter = '{'; + private static final char endParameterCharacter = '}'; + private static final char beginOptionalCharacter = '('; + private static final char endOptionalCharacter = ')'; + + interface Located { + int start(); + + int end(); + + } + + static final class Node implements Located { + + private final Type type; + private final List nodes; + private final String token; + private final int start; + private final int end; + + Node(Type type, int start, int end, String token) { + this(type, start, end, null, token); + } + + Node(Type type, int start, int end, List nodes) { + this(type, start, end, nodes, null); + } + + private Node(Type type, int start, int end, List nodes, String token) { + this.type = requireNonNull(type); + this.nodes = nodes; + this.token = token; + this.start = start; + this.end = end; + + } + + enum Type { + TEXT_NODE, + OPTIONAL_NODE, + ALTERNATION_NODE, + ALTERNATIVE_NODE, + PARAMETER_NODE, + EXPRESSION_NODE + } + + public int start() { + return start; + } + + public int end() { + return end; + } + + List nodes() { + return nodes; + } + + Type type() { + return type; + } + + String text() { + if (nodes == null) + return token; + + return nodes().stream() + .map(Node::text) + .collect(joining()); + } + + @Override + public String toString() { + return toString(0).toString(); + } + + private StringBuilder toString(int depth) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + sb.append("{") + .append("\"type\": \"").append(type) + .append("\", \"start\": ") + .append(start) + .append(", \"end\": ") + .append(end); + + if (token != null) { + sb.append(", \"token\": \"").append(token.replaceAll("\\\\", "\\\\\\\\")).append("\""); + } + + if (nodes != null) { + sb.append(", \"nodes\": "); + if (!nodes.isEmpty()) { + StringBuilder padding = new StringBuilder(); + for (int i = 0; i < depth; i++) { + padding.append(" "); + } + sb.append(nodes.stream() + .map(node -> node.toString(depth + 1)) + .collect(joining(",\n", "[\n", "\n" +padding + "]"))); + + } else { + sb.append("[]"); + } + } + sb.append("}"); + return sb; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Node node = (Node) o; + return start == node.start && + end == node.end && + type == node.type && + Objects.equals(nodes, node.nodes) && + Objects.equals(token, node.token); + } + + @Override + public int hashCode() { + return Objects.hash(type, nodes, token, start, end); + } + + } + + static final class Token implements Located { + + final String text; + final Token.Type type; + final int start; + final int end; + + Token(String text, Token.Type type, int start, int end) { + this.text = requireNonNull(text); + this.type = requireNonNull(type); + this.start = start; + this.end = end; + } + + static boolean canEscape(Integer token) { + if (Character.isWhitespace(token)) { + return true; + } + switch (token) { + case (int) escapeCharacter: + case (int) alternationCharacter: + case (int) beginParameterCharacter: + case (int) endParameterCharacter: + case (int) beginOptionalCharacter: + case (int) endOptionalCharacter: + return true; + } + return false; + } + + static Type typeOf(Integer token) { + if (Character.isWhitespace(token)) { + return Type.WHITE_SPACE; + } + switch (token) { + case (int) alternationCharacter: + return Type.ALTERNATION; + case (int) beginParameterCharacter: + return Type.BEGIN_PARAMETER; + case (int) endParameterCharacter: + return Type.END_PARAMETER; + case (int) beginOptionalCharacter: + return Type.BEGIN_OPTIONAL; + case (int) endOptionalCharacter: + return Type.END_OPTIONAL; + } + return Type.TEXT; + } + + static boolean isEscapeCharacter(int token) { + return token == escapeCharacter; + } + + public int start() { + return start; + } + + public int end() { + return end; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Token token = (Token) o; + return start == token.start && + end == token.end && + text.equals(token.text) && + type == token.type; + } + + @Override + public int hashCode() { + return Objects.hash(start, end, text, type); + } + + @Override + public String toString() { + return new StringJoiner(", ", "" + "{", "}") + .add("\"type\": \"" + type + "\"") + .add("\"start\": " + start + "") + .add("\"end\": " + end + "") + .add("\"text\": \"" + text + "\"") + .toString(); + } + + enum Type { + START_OF_LINE, + END_OF_LINE, + WHITE_SPACE, + BEGIN_OPTIONAL("" + beginOptionalCharacter, "optional text"), + END_OPTIONAL("" + endOptionalCharacter, "optional text"), + BEGIN_PARAMETER("" + beginParameterCharacter, "a parameter"), + END_PARAMETER("" + endParameterCharacter, "a parameter"), + ALTERNATION("" + alternationCharacter, "alternation"), + TEXT; + + private final String symbol; + private final String purpose; + + Type() { + this(null, null); + } + + Type(String symbol, String purpose) { + this.symbol = symbol; + this.purpose = purpose; + } + + String purpose() { + return requireNonNull(purpose, name() + " does not have a purpose"); + } + + String symbol() { + return requireNonNull(symbol, name() + " does not have a symbol"); + } + } + + } + +} diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java index 9b7dbd43a3..d744705b9c 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpression.java @@ -1,24 +1,30 @@ package io.cucumber.cucumberexpressions; +import io.cucumber.cucumberexpressions.Ast.Node; import org.apiguardian.api.API; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; +import java.util.function.Function; import java.util.regex.Pattern; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.OPTIONAL_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.PARAMETER_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.TEXT_NODE; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternativeMayNotBeEmpty; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternativeMayNotExclusivelyContainOptionals; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createInvalidParameterTypeName; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createOptionalIsNotAllowedInOptional; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createOptionalMayNotBeEmpty; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createParameterIsNotAllowedInOptional; +import static io.cucumber.cucumberexpressions.ParameterType.isValidParameterTypeName; +import static io.cucumber.cucumberexpressions.UndefinedParameterTypeException.createUndefinedParameterType; +import static java.util.stream.Collectors.joining; + @API(status = API.Status.STABLE) public final class CucumberExpression implements Expression { - // Does not include (){} characters because they have special meaning - private static final Pattern ESCAPE_PATTERN = Pattern.compile("([\\\\^\\[$.|?*+\\]])"); - @SuppressWarnings("RegExpRedundantEscape") // Android can't parse unescaped braces - static final Pattern PARAMETER_PATTERN = Pattern.compile("(\\\\\\\\)?\\{([^}]*)\\}"); - private static final Pattern OPTIONAL_PATTERN = Pattern.compile("(\\\\\\\\)?\\(([^)]+)\\)"); - private static final Pattern ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = Pattern.compile("([^\\s^/]+)((/[^\\s^/]+)+)"); - private static final String DOUBLE_ESCAPE = "\\\\"; - private static final String PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE = "Parameter types cannot be alternative: "; - private static final String PARAMETER_TYPES_CANNOT_BE_OPTIONAL = "Parameter types cannot be optional: "; + private static final Pattern ESCAPE_PATTERN = Pattern.compile("([\\\\^\\[({$.|?*+})\\]])"); private final List> parameterTypes = new ArrayList<>(); private final String source; @@ -29,105 +35,119 @@ public final class CucumberExpression implements Expression { this.source = expression; this.parameterTypeRegistry = parameterTypeRegistry; - expression = processEscapes(expression); - expression = processOptional(expression); - expression = processAlternation(expression); - expression = processParameters(expression, parameterTypeRegistry); - expression = "^" + expression + "$"; - treeRegexp = new TreeRegexp(expression); - } - - private String processEscapes(String expression) { - // This will cause explicitly-escaped parentheses to be double-escaped - return ESCAPE_PATTERN.matcher(expression).replaceAll("\\\\$1"); + CucumberExpressionParser parser = new CucumberExpressionParser(); + Node ast = parser.parse(expression); + String pattern = rewriteToRegex(ast); + treeRegexp = new TreeRegexp(pattern); } - private String processAlternation(String expression) { - Matcher matcher = ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP.matcher(expression); - StringBuffer sb = new StringBuffer(); - while (matcher.find()) { - // replace \/ with / - // replace / with | - String replacement = matcher.group(0).replace('/', '|').replaceAll("\\\\\\|", "/"); - - if (replacement.contains("|")) { - // Make sure the alternative parts don't contain parameter types - for (String part : replacement.split("\\|")) { - checkNotParameterType(part, PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE); - } - matcher.appendReplacement(sb, "(?:" + replacement + ")"); - } else { - // All / were escaped - matcher.appendReplacement(sb, replacement); - } + private String rewriteToRegex(Node node) { + switch (node.type()) { + case TEXT_NODE: + return escapeRegex(node.text()); + case OPTIONAL_NODE: + return rewriteOptional(node); + case ALTERNATION_NODE: + return rewriteAlternation(node); + case ALTERNATIVE_NODE: + return rewriteAlternative(node); + case PARAMETER_NODE: + return rewriteParameter(node); + case EXPRESSION_NODE: + return rewriteExpression(node); + default: + // Can't happen as long as the switch case is exhaustive + throw new IllegalArgumentException(node.type().name()); } - matcher.appendTail(sb); - return sb.toString(); } - private void checkNotParameterType(String s, String message) { - Matcher matcher = PARAMETER_PATTERN.matcher(s); - if (matcher.find()) { - throw new CucumberExpressionException(message + source); - } + private static String escapeRegex(String text) { + return ESCAPE_PATTERN.matcher(text).replaceAll("\\\\$1"); } - private String processOptional(String expression) { - Matcher matcher = OPTIONAL_PATTERN.matcher(expression); - StringBuffer sb = new StringBuffer(); - while (matcher.find()) { - // look for double-escaped parentheses - String parameterPart = matcher.group(2); - if (DOUBLE_ESCAPE.equals(matcher.group(1))) { - matcher.appendReplacement(sb, "\\\\(" + parameterPart + "\\\\)"); - } else { - checkNotParameterType(parameterPart, PARAMETER_TYPES_CANNOT_BE_OPTIONAL); - matcher.appendReplacement(sb, "(?:" + parameterPart + ")?"); - } - } - matcher.appendTail(sb); - return sb.toString(); + private String rewriteOptional(Node node) { + assertNoParameters(node, astNode -> createParameterIsNotAllowedInOptional(astNode, source)); + assertNoOptionals(node, astNode -> createOptionalIsNotAllowedInOptional(astNode, source)); + assertNotEmpty(node, astNode -> createOptionalMayNotBeEmpty(astNode, source)); + return node.nodes().stream() + .map(this::rewriteToRegex) + .collect(joining("", "(?:", ")?")); } - private String processParameters(String expression, ParameterTypeRegistry parameterTypeRegistry) { - StringBuffer sb = new StringBuffer(); - Matcher matcher = PARAMETER_PATTERN.matcher(expression); - while (matcher.find()) { - if (DOUBLE_ESCAPE.equals(matcher.group(1))) { - matcher.appendReplacement(sb, "\\\\{" + matcher.group(2) + "\\\\}"); - } else { - String typeName = matcher.group(2); - ParameterType.checkParameterTypeName(typeName); - ParameterType parameterType = parameterTypeRegistry.lookupByTypeName(typeName); - if (parameterType == null) { - throw new UndefinedParameterTypeException(typeName); - } - parameterTypes.add(parameterType); - matcher.appendReplacement(sb, Matcher.quoteReplacement(buildCaptureRegexp(parameterType.getRegexps()))); + private String rewriteAlternation(Node node) { + // Make sure the alternative parts aren't empty and don't contain parameter types + for (Node alternative : node.nodes()) { + if (alternative.nodes().isEmpty()) { + throw createAlternativeMayNotBeEmpty(alternative, source); } + assertNotEmpty(alternative, astNode -> createAlternativeMayNotExclusivelyContainOptionals(astNode, source)); } - matcher.appendTail(sb); - return sb.toString(); + return node.nodes() + .stream() + .map(this::rewriteToRegex) + .collect(joining("|", "(?:", ")")); } - private String buildCaptureRegexp(List regexps) { - StringBuilder sb = new StringBuilder("("); + private String rewriteAlternative(Node node) { + return node.nodes().stream() + .map(this::rewriteToRegex) + .collect(joining()); + } + private String rewriteParameter(Node node) { + String name = node.text(); + ParameterType parameterType = parameterTypeRegistry.lookupByTypeName(name); + if (parameterType == null) { + throw createUndefinedParameterType(node, source, name); + } + parameterTypes.add(parameterType); + List regexps = parameterType.getRegexps(); if (regexps.size() == 1) { - sb.append(regexps.get(0)); - } else { - boolean bar = false; - for (String captureGroupRegexp : regexps) { - if (bar) sb.append("|"); - sb.append("(?:").append(captureGroupRegexp).append(")"); - bar = true; - } + return "(" + regexps.get(0) + ")"; } + return regexps.stream() + .collect(joining(")|(?:", "((?:", "))")); + } - sb.append(")"); - return sb.toString(); + + private String rewriteExpression(Node node) { + return node.nodes().stream() + .map(this::rewriteToRegex) + .collect(joining("", "^", "$")); + } + + private void assertNotEmpty(Node node, + Function createNodeWasNotEmptyException) { + node.nodes() + .stream() + .filter(astNode -> TEXT_NODE.equals(astNode.type())) + .findFirst() + .orElseThrow(() -> createNodeWasNotEmptyException.apply(node)); } + private void assertNoParameters(Node node, + Function createNodeContainedAParameterException) { + assertNoNodeOfType(PARAMETER_NODE, node, createNodeContainedAParameterException); + } + + private void assertNoOptionals(Node node, + Function createNodeContainedAnOptionalException) { + assertNoNodeOfType(OPTIONAL_NODE, node, createNodeContainedAnOptionalException); + } + + private void assertNoNodeOfType(Node.Type nodeType, Node node, + Function createException) { + node.nodes() + .stream() + .filter(astNode -> nodeType.equals(astNode.type())) + .map(createException) + .findFirst() + .ifPresent(exception -> { + throw exception; + }); + } + + @Override public List> match(String text, Type... typeHints) { final Group group = treeRegexp.match(text); @@ -145,7 +165,7 @@ public List> match(String text, Type... typeHints) { } } - return Argument.build(group, treeRegexp, parameterTypes); + return Argument.build(group, parameterTypes); } @Override @@ -157,4 +177,5 @@ public String getSource() { public Pattern getRegexp() { return treeRegexp.pattern(); } + } diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java index 443abc0e03..ef2a3cfc81 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionException.java @@ -1,9 +1,14 @@ package io.cucumber.cucumberexpressions; +import io.cucumber.cucumberexpressions.Ast.Located; +import io.cucumber.cucumberexpressions.Ast.Node; +import io.cucumber.cucumberexpressions.Ast.Token; +import io.cucumber.cucumberexpressions.Ast.Token.Type; import org.apiguardian.api.API; @API(status = API.Status.STABLE) public class CucumberExpressionException extends RuntimeException { + CucumberExpressionException(String message) { super(message); } @@ -11,4 +16,148 @@ public class CucumberExpressionException extends RuntimeException { CucumberExpressionException(String message, Throwable cause) { super(message, cause); } + + static CucumberExpressionException createMissingEndToken(String expression, Type beginToken, Type endToken, + Token current) { + return new CucumberExpressionException(message( + current.start(), + expression, + pointAt(current), + "The '" + beginToken.symbol() + "' does not have a matching '" + endToken.symbol() + "'", + "If you did not intend to use " + beginToken.purpose() + " you can use '\\" + beginToken + .symbol() + "' to escape the " + beginToken.purpose())); + } + + static CucumberExpressionException createAlternationNotAllowedInOptional(String expression, Token current) { + return new CucumberExpressionException(message( + current.start, + expression, + pointAt(current), + "An alternation can not be used inside an optional", + "You can use '\\/' to escape the the '/'" + )); + } + + static CucumberExpressionException createTheEndOfLineCanNotBeEscaped(String expression) { + int index = expression.codePointCount(0, expression.length()) - 1; + return new CucumberExpressionException(message( + index, + expression, + pointAt(index), + "The end of line can not be escaped", + "You can use '\\\\' to escape the the '\\'" + )); + } + + static CucumberExpressionException createAlternativeMayNotBeEmpty(Node node, String expression) { + return new CucumberExpressionException(message( + node.start(), + expression, + pointAt(node), + "Alternative may not be empty", + "If you did not mean to use an alternative you can use '\\/' to escape the the '/'")); + } + + static CucumberExpressionException createParameterIsNotAllowedInOptional(Node node, String expression) { + return new CucumberExpressionException(message( + node.start(), + expression, + pointAt(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 '{'")); + } + static CucumberExpressionException createOptionalIsNotAllowedInOptional(Node node, String expression) { + return new CucumberExpressionException(message( + node.start(), + expression, + pointAt(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.")); + } + + static CucumberExpressionException createOptionalMayNotBeEmpty(Node node, String expression) { + return new CucumberExpressionException(message( + node.start(), + expression, + pointAt(node), + "An optional must contain some text", + "If you did not mean to use an optional you can use '\\(' to escape the the '('")); + } + + static CucumberExpressionException createAlternativeMayNotExclusivelyContainOptionals(Node node, + String expression) { + return new CucumberExpressionException(message( + node.start(), + expression, + pointAt(node), + "An alternative may not exclusively contain optionals", + "If you did not mean to use an optional you can use '\\(' to escape the the '('")); + } + + private static String thisCucumberExpressionHasAProblemAt(int index) { + return "This Cucumber Expression has a problem at column " + (index + 1) + ":" + "\n"; + } + + static CucumberExpressionException createCantEscape(String expression, int index) { + return new CucumberExpressionException(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")); + } + + static CucumberExpressionException createInvalidParameterTypeName(String name) { + return new CucumberExpressionException( + "Illegal character in parameter name {" + name + "}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'"); + } + + /** + * Not very clear, but this message has to be language independent + * Other languages have dedicated syntax for writing down regular expressions + *

+ * In java a regular expression has to start with {@code ^} and end with + * {@code $} to be recognized as one by Cucumber. + * + * @see ExpressionFactory + */ + static CucumberExpressionException createInvalidParameterTypeName(Token token, String expression) { + return new CucumberExpressionException(message( + token.start(), + expression, + pointAt(token), + "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", + "Did you mean to use a regular expression?")); + } + + static String message(int index, String expression, String pointer, String problem, + String solution) { + return thisCucumberExpressionHasAProblemAt(index) + + "\n" + + expression + "\n" + + pointer + "\n" + + problem + ".\n" + + solution; + } + + static String pointAt(Located node) { + StringBuilder pointer = new StringBuilder(pointAt(node.start())); + if (node.start() + 1 < node.end()) { + for (int i = node.start() + 1; i < node.end() - 1; i++) { + pointer.append("-"); + } + pointer.append("^"); + } + return pointer.toString(); + } + + private static String pointAt(int index) { + StringBuilder pointer = new StringBuilder(); + for (int i = 0; i < index; i++) { + pointer.append(" "); + } + pointer.append("^"); + return pointer.toString(); + } + } diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java index 512f58e481..1cdd6fd3e4 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionGenerator.java @@ -85,17 +85,6 @@ private String escape(String s) { .replaceAll("/", "\\\\/"); } - /** - * @param text the text (step) to generate an expression for - * @return the first of the generated expressions - * @deprecated use {@link #generateExpressions(String)} - */ - @Deprecated - public GeneratedExpression generateExpression(String text) { - List generatedExpressions = generateExpressions(text); - return generatedExpressions.get(0); - } - private List createParameterTypeMatchers(String text) { Collection> parameterTypes = parameterTypeRegistry.getParameterTypes(); List parameterTypeMatchers = new ArrayList<>(); diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java new file mode 100644 index 0000000000..b77be40e91 --- /dev/null +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionParser.java @@ -0,0 +1,311 @@ +package io.cucumber.cucumberexpressions; + +import io.cucumber.cucumberexpressions.Ast.Node; +import io.cucumber.cucumberexpressions.Ast.Token; +import io.cucumber.cucumberexpressions.Ast.Token.Type; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static io.cucumber.cucumberexpressions.Ast.Node.Type.ALTERNATION_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.ALTERNATIVE_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.EXPRESSION_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.OPTIONAL_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.PARAMETER_NODE; +import static io.cucumber.cucumberexpressions.Ast.Node.Type.TEXT_NODE; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.ALTERNATION; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_OPTIONAL; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.BEGIN_PARAMETER; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OF_LINE; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_OPTIONAL; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.END_PARAMETER; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.START_OF_LINE; +import static io.cucumber.cucumberexpressions.Ast.Token.Type.WHITE_SPACE; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createAlternationNotAllowedInOptional; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createInvalidParameterTypeName; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createMissingEndToken; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +final class CucumberExpressionParser { + + /* + * text := whitespace | ')' | '}' | . + */ + private static final Parser textParser = (expression, tokens, current) -> { + Token token = tokens.get(current); + switch (token.type) { + case WHITE_SPACE: + case TEXT: + case END_PARAMETER: + case END_OPTIONAL: + return new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); + case ALTERNATION: + throw createAlternationNotAllowedInOptional(expression, token); + case BEGIN_PARAMETER: + case START_OF_LINE: + case END_OF_LINE: + case BEGIN_OPTIONAL: + default: + // If configured correctly this will never happen + return new Result(0); + } + }; + + /* + * name := whitespace | . + */ + private static final Parser nameParser = (expression, tokens, current) -> { + Token token = tokens.get(current); + switch (token.type) { + case WHITE_SPACE: + case TEXT: + return new Result(1, new Node(TEXT_NODE, token.start(), token.end(), token.text)); + case BEGIN_OPTIONAL: + case END_OPTIONAL: + case BEGIN_PARAMETER: + case END_PARAMETER: + case ALTERNATION: + throw createInvalidParameterTypeName(token, expression); + case START_OF_LINE: + case END_OF_LINE: + default: + // If configured correctly this will never happen + return new Result(0); + } + }; + + /* + * parameter := '{' + name* + '}' + */ + private static final Parser parameterParser = parseBetween( + PARAMETER_NODE, + BEGIN_PARAMETER, + END_PARAMETER, + singletonList(nameParser) + ); + + /* + * optional := '(' + option* + ')' + * option := optional | parameter | text + */ + private static final Parser optionalParser; + static { + List parsers = new ArrayList<>(); + optionalParser = parseBetween( + OPTIONAL_NODE, + BEGIN_OPTIONAL, + END_OPTIONAL, + parsers + ); + parsers.addAll(asList(optionalParser, parameterParser, textParser)); + } + + /* + * alternation := alternative* + ( '/' + alternative* )+ + */ + private static final Parser alternativeSeparator = (expression, tokens, current) -> { + if (!lookingAt(tokens, current, ALTERNATION)) { + return new Result(0); + } + Token token = tokens.get(current); + return new Result(1, new Node(ALTERNATIVE_NODE, token.start(), token.end(), token.text)); + }; + + private static final List alternativeParsers = asList( + alternativeSeparator, + optionalParser, + parameterParser, + textParser + ); + + /* + * alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary) + * left-boundary := whitespace | } | ^ + * right-boundary := whitespace | { | $ + * alternative: = optional | parameter | text + */ + private static final Parser alternationParser = (expression, tokens, current) -> { + int previous = current - 1; + if (!lookingAtAny(tokens, previous, START_OF_LINE, WHITE_SPACE, END_PARAMETER)) { + return new Result(0); + } + + Result result = parseTokensUntil(expression, alternativeParsers, tokens, current, WHITE_SPACE, END_OF_LINE, BEGIN_PARAMETER); + int subCurrent = current + result.consumed; + if (result.ast.stream().noneMatch(astNode -> astNode.type() == ALTERNATIVE_NODE)) { + return new Result(0); + } + + int start = tokens.get(current).start(); + int end = tokens.get(subCurrent).start(); + // Does not consume right hand boundary token + return new Result(result.consumed, + new Node(ALTERNATION_NODE, start, end, splitAlternatives(start, end, result.ast))); + }; + + /* + * cucumber-expression := ( alternation | optional | parameter | text )* + */ + private static final Parser cucumberExpressionParser = parseBetween( + EXPRESSION_NODE, + START_OF_LINE, + END_OF_LINE, + asList( + alternationParser, + optionalParser, + parameterParser, + textParser + ) + ); + + Node parse(String expression) { + CucumberExpressionTokenizer tokenizer = new CucumberExpressionTokenizer(); + List tokens = tokenizer.tokenize(expression); + Result result = cucumberExpressionParser.parse(expression, tokens, 0); + return result.ast.get(0); + } + + private interface Parser { + Result parse(String expression, List tokens, int current); + + } + + private static final class Result { + final int consumed; + final List ast; + + private Result(int consumed, Node... ast) { + this(consumed, Arrays.asList(ast)); + } + + private Result(int consumed, List ast) { + this.consumed = consumed; + this.ast = ast; + } + + } + + private static Parser parseBetween( + Node.Type type, + Type beginToken, + Type endToken, + List parsers) { + return (expression, tokens, current) -> { + if (!lookingAt(tokens, current, beginToken)) { + return new Result(0); + } + int subCurrent = current + 1; + Result result = parseTokensUntil(expression, parsers, tokens, subCurrent, endToken, END_OF_LINE); + subCurrent += result.consumed; + + // endToken not found + if (!lookingAt(tokens, subCurrent, endToken)) { + throw createMissingEndToken(expression, beginToken, endToken, tokens.get(current)); + } + // consumes endToken + int start = tokens.get(current).start(); + int end = tokens.get(subCurrent).end(); + return new Result(subCurrent + 1 - current, new Node(type, start, end, result.ast)); + }; + } + + private static Result parseTokensUntil( + String expression, + List parsers, + List tokens, + int startAt, + Type... endTokens) { + int current = startAt; + int size = tokens.size(); + List ast = new ArrayList<>(); + while (current < size) { + if (lookingAtAny(tokens, current, endTokens)) { + break; + } + + Result result = parseToken(expression, parsers, tokens, current); + if (result.consumed == 0) { + // If configured correctly this will never happen + // Keep to avoid infinite loops + throw new IllegalStateException("No eligible parsers for " + tokens); + } + current += result.consumed; + ast.addAll(result.ast); + } + return new Result(current - startAt, ast); + } + + private static Result parseToken(String expression, List parsers, + List tokens, + int startAt) { + for (Parser parser : parsers) { + Result result = parser.parse(expression, tokens, startAt); + if (result.consumed != 0) { + return result; + } + } + // If configured correctly this will never happen + throw new IllegalStateException("No eligible parsers for " + tokens); + } + + private static boolean lookingAtAny(List tokens, int at, Type... tokenTypes) { + for (Type tokeType : tokenTypes) { + if (lookingAt(tokens, at, tokeType)) { + return true; + } + } + return false; + } + + private static boolean lookingAt(List tokens, int at, Type token) { + if (at < 0) { + // If configured correctly this will never happen + // Keep for completeness + return token == START_OF_LINE; + } + if (at >= tokens.size()) { + return token == END_OF_LINE; + } + return tokens.get(at).type == token; + } + + private static List splitAlternatives(int start, int end, List alternation) { + List separators = new ArrayList<>(); + List> alternatives = new ArrayList<>(); + List alternative = new ArrayList<>(); + for (Node n : alternation) { + if (ALTERNATIVE_NODE.equals(n.type())) { + separators.add(n); + alternatives.add(alternative); + alternative = new ArrayList<>(); + } else { + alternative.add(n); + } + } + alternatives.add(alternative); + + return createAlternativeNodes(start, end, separators, alternatives); + } + + private static List createAlternativeNodes(int start, int end, List separators, List> alternatives) { + List nodes = new ArrayList<>(); + for (int i = 0; i < alternatives.size(); i++) { + List n = alternatives.get(i); + if (i == 0) { + Node rightSeparator = separators.get(i); + nodes.add(new Node(ALTERNATIVE_NODE, start, rightSeparator.start(), n)); + } else if (i == alternatives.size() - 1) { + Node leftSeparator = separators.get(i - 1); + nodes.add(new Node(ALTERNATIVE_NODE, leftSeparator.end(), end, n)); + } else { + Node leftSeparator = separators.get(i - 1); + Node rightSeparator = separators.get(i); + nodes.add(new Node(ALTERNATIVE_NODE, leftSeparator.end(), rightSeparator.start(), n)); + } + } + return nodes; + } + +} diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java new file mode 100644 index 0000000000..9a2a9df254 --- /dev/null +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizer.java @@ -0,0 +1,133 @@ +package io.cucumber.cucumberexpressions; + +import io.cucumber.cucumberexpressions.Ast.Token; +import io.cucumber.cucumberexpressions.Ast.Token.Type; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.PrimitiveIterator.OfInt; + +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createCantEscape; +import static io.cucumber.cucumberexpressions.CucumberExpressionException.createTheEndOfLineCanNotBeEscaped; + +final class CucumberExpressionTokenizer { + + List tokenize(String expression) { + List tokens = new ArrayList<>(); + tokenizeImpl(expression).forEach(tokens::add); + return tokens; + } + + private Iterable tokenizeImpl(String expression) { + return () -> new TokenIterator(expression); + } + + private static class TokenIterator implements Iterator { + + private final String expression; + private final OfInt codePoints; + + private StringBuilder buffer = new StringBuilder(); + private Type previousTokenType = null; + private Type currentTokenType = Type.START_OF_LINE; + private boolean treatAsText; + private int bufferStartIndex; + private int escaped; + + TokenIterator(String expression) { + this.expression = expression; + this.codePoints = expression.codePoints().iterator(); + } + + private Token convertBufferToToken(Type tokenType) { + int escapeTokens = 0; + if (tokenType == Type.TEXT) { + escapeTokens = escaped; + escaped = 0; + } + int consumedIndex = bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escapeTokens; + Token t = new Token(buffer.toString(), tokenType, bufferStartIndex, consumedIndex); + buffer = new StringBuilder(); + this.bufferStartIndex = consumedIndex; + return t; + } + + private void advanceTokenTypes() { + previousTokenType = currentTokenType; + currentTokenType = null; + } + + private Type tokenTypeOf(Integer token, boolean treatAsText) { + if (!treatAsText) { + return Token.typeOf(token); + } + if (Token.canEscape(token)) { + return Type.TEXT; + } + throw createCantEscape(expression, bufferStartIndex + buffer.codePointCount(0, buffer.length()) + escaped); + } + + private boolean shouldContinueTokenType(Type previousTokenType, + Type currentTokenType) { + return currentTokenType == previousTokenType + && (currentTokenType == Type.WHITE_SPACE || currentTokenType == Type.TEXT); + } + + @Override + public boolean hasNext() { + return previousTokenType != Type.END_OF_LINE; + } + + @Override + public Token next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + if (currentTokenType == Type.START_OF_LINE) { + Token token = convertBufferToToken(currentTokenType); + advanceTokenTypes(); + return token; + } + + while (codePoints.hasNext()) { + int codePoint = codePoints.nextInt(); + if (!treatAsText && Token.isEscapeCharacter(codePoint)) { + escaped++; + treatAsText = true; + continue; + } + currentTokenType = tokenTypeOf(codePoint, treatAsText); + treatAsText = false; + + if (previousTokenType == Type.START_OF_LINE || + shouldContinueTokenType(previousTokenType, currentTokenType)) { + advanceTokenTypes(); + buffer.appendCodePoint(codePoint); + } else { + Token t = convertBufferToToken(previousTokenType); + advanceTokenTypes(); + buffer.appendCodePoint(codePoint); + return t; + } + } + + if (buffer.length() > 0) { + Token token = convertBufferToToken(previousTokenType); + advanceTokenTypes(); + return token; + } + + currentTokenType = Type.END_OF_LINE; + if (treatAsText) { + throw createTheEndOfLineCanNotBeEscaped(expression); + } + Token token = convertBufferToToken(currentTokenType); + advanceTokenTypes(); + return token; + } + + } + +} diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java index cb5049500a..6565bc861f 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ExpressionFactory.java @@ -10,6 +10,9 @@ * Creates a {@link CucumberExpression} or {@link RegularExpression} from a {@link String} * using heuristics. This is particularly useful for languages that don't have a * literal syntax for regular expressions. In Java, a regular expression has to be represented as a String. + * + * A string that starts with `^` and/or ends with `$` is considered a regular expression. + * Everything else is considered a Cucumber expression. */ @API(status = API.Status.STABLE) public final class ExpressionFactory { @@ -17,6 +20,8 @@ public final class ExpressionFactory { private static final Pattern BEGIN_ANCHOR = Pattern.compile("^\\^.*"); private static final Pattern END_ANCHOR = Pattern.compile(".*\\$$"); private static final Pattern SCRIPT_STYLE_REGEXP = Pattern.compile("^/(.*)/$"); + private static final Pattern PARAMETER_PATTERN = Pattern.compile("((?:\\\\){0,2})\\{([^}]*)\\}"); + private final ParameterTypeRegistry parameterTypeRegistry; public ExpressionFactory(ParameterTypeRegistry parameterTypeRegistry) { @@ -38,7 +43,7 @@ private RegularExpression createRegularExpressionWithAnchors(String expressionSt try { return new RegularExpression(Pattern.compile(expressionString), parameterTypeRegistry); } catch (PatternSyntaxException e) { - if (CucumberExpression.PARAMETER_PATTERN.matcher(expressionString).find()) { + if (PARAMETER_PATTERN.matcher(expressionString).find()) { throw new CucumberExpressionException("You cannot use anchors (^ or $) in Cucumber Expressions. Please remove them from " + expressionString, e); } throw e; diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java index 938c7a5b2b..c3f8348073 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/ParameterType.java @@ -12,7 +12,7 @@ @API(status = API.Status.STABLE) public final class ParameterType implements Comparable> { @SuppressWarnings("RegExpRedundantEscape") // Android can't parse unescaped braces - private static final Pattern ILLEGAL_PARAMETER_NAME_PATTERN = Pattern.compile("([\\[\\]()$.|?*+])"); + private static final Pattern ILLEGAL_PARAMETER_NAME_PATTERN = Pattern.compile("([{}()\\\\/])"); private static final Pattern UNESCAPE_PATTERN = Pattern.compile("(\\\\([\\[$.|?*+\\]]))"); private final String name; @@ -25,11 +25,15 @@ public final class ParameterType implements Comparable> { private final boolean useRegexpMatchAsStrongTypeHint; static void checkParameterTypeName(String name) { + if (!isValidParameterTypeName(name)) { + throw CucumberExpressionException.createInvalidParameterTypeName(name); + } + } + + static boolean isValidParameterTypeName(String name) { String unescapedTypeName = UNESCAPE_PATTERN.matcher(name).replaceAll("$2"); Matcher matcher = ILLEGAL_PARAMETER_NAME_PATTERN.matcher(unescapedTypeName); - if (matcher.find()) { - throw new CucumberExpressionException(String.format("Illegal character '%s' in parameter name {%s}.", matcher.group(1), unescapedTypeName)); - } + return !matcher.find(); } static ParameterType createAnonymousParameterType(String regexp) { @@ -46,7 +50,8 @@ static ParameterType fromEnum(final Class enumClass) { Enum[] enumConstants = enumClass.getEnumConstants(); StringBuilder regexpBuilder = new StringBuilder(); for (int i = 0; i < enumConstants.length; i++) { - if (i > 0) regexpBuilder.append("|"); + if (i > 0) + regexpBuilder.append("|"); regexpBuilder.append(enumConstants[i].name()); } return new ParameterType<>( @@ -57,11 +62,17 @@ static ParameterType fromEnum(final Class enumClass) { ); } - private ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint, boolean anonymous) { - if (regexps == null) throw new NullPointerException("regexps cannot be null"); - if (type == null) throw new NullPointerException("type cannot be null"); - if (transformer == null) throw new NullPointerException("transformer cannot be null"); - if (name != null) checkParameterTypeName(name); + private ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint, + boolean anonymous) { + if (regexps == null) + throw new NullPointerException("regexps cannot be null"); + if (type == null) + throw new NullPointerException("type cannot be null"); + if (transformer == null) + throw new NullPointerException("transformer cannot be null"); + if (name != null) + checkParameterTypeName(name); this.name = name; this.regexps = regexps; this.type = type; @@ -72,11 +83,14 @@ private ParameterType(String name, List regexps, Type type, CaptureGroup this.useRegexpMatchAsStrongTypeHint = useRegexpMatchAsStrongTypeHint; } - public ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { - this(name, regexps, type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint, false); + public ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + this(name, regexps, type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint, + false); } - public ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, boolean useForSnippets, boolean preferForRegexpMatch) { + public ParameterType(String name, List regexps, Type type, CaptureGroupTransformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch) { // Unless explicitly set useRegexpMatchAsStrongTypeHint is true. // // Reasoning: @@ -96,11 +110,13 @@ public ParameterType(String name, List regexps, Type type, CaptureGroupT this(name, regexps, type, transformer, useForSnippets, preferForRegexpMatch, true); } - public ParameterType(String name, List regexps, Class type, CaptureGroupTransformer transformer, boolean useForSnippets, boolean preferForRegexpMatch) { + public ParameterType(String name, List regexps, Class type, CaptureGroupTransformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch); } - public ParameterType(String name, String regexp, Class type, CaptureGroupTransformer transformer, boolean useForSnippets, boolean preferForRegexpMatch) { + public ParameterType(String name, String regexp, Class type, CaptureGroupTransformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch); } @@ -112,27 +128,36 @@ public ParameterType(String name, String regexp, Class type, CaptureGroupTran this(name, singletonList(regexp), type, transformer, true, false); } - public ParameterType(String name, List regexps, Type type, Transformer transformer, boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { - this(name, regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint); + public ParameterType(String name, List regexps, Type type, Transformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + this(name, regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, preferForRegexpMatch, + useRegexpMatchAsStrongTypeHint); } - public ParameterType(String name, List regexps, Type type, Transformer transformer, boolean useForSnippets, boolean preferForRegexpMatch) { + public ParameterType(String name, List regexps, Type type, Transformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, preferForRegexpMatch); } - public ParameterType(String name, List regexps, Class type, Transformer transformer, boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { - this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint); + public ParameterType(String name, List regexps, Class type, Transformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch, + useRegexpMatchAsStrongTypeHint); } - public ParameterType(String name, List regexps, Class type, Transformer transformer, boolean useForSnippets, boolean preferForRegexpMatch) { + public ParameterType(String name, List regexps, Class type, Transformer transformer, + boolean useForSnippets, boolean preferForRegexpMatch) { this(name, regexps, (Type) type, transformer, useForSnippets, preferForRegexpMatch); } - public ParameterType(String name, String regexp, Class type, Transformer transformer, boolean useForSnippets, boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { - this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint); + public ParameterType(String name, String regexp, Class type, Transformer transformer, boolean useForSnippets, + boolean preferForRegexpMatch, boolean useRegexpMatchAsStrongTypeHint) { + this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch, + useRegexpMatchAsStrongTypeHint); } - public ParameterType(String name, String regexp, Class type, Transformer transformer, boolean useForSnippets, boolean preferForRegexpMatch) { + public ParameterType(String name, String regexp, Class type, Transformer transformer, boolean useForSnippets, + boolean preferForRegexpMatch) { this(name, singletonList(regexp), type, transformer, useForSnippets, preferForRegexpMatch); } @@ -211,7 +236,8 @@ public boolean useRegexpMatchAsStrongTypeHint() { } ParameterType deAnonymize(Type type, Transformer transformer) { - return new ParameterType<>("anonymous", regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, preferForRegexpMatch, useRegexpMatchAsStrongTypeHint, anonymous); + return new ParameterType<>("anonymous", regexps, type, new TransformerAdaptor<>(transformer), useForSnippets, + preferForRegexpMatch, useRegexpMatchAsStrongTypeHint, anonymous); } T transform(List groupValues) { @@ -234,14 +260,18 @@ T transform(List groupValues) { } catch (CucumberExpressionException e) { throw e; } catch (Throwable throwable) { - throw new CucumberExpressionException(String.format("ParameterType {%s} failed to transform %s to %s", name, groupValues, type), throwable); + throw new CucumberExpressionException( + String.format("ParameterType {%s} failed to transform %s to %s", name, groupValues, type), + throwable); } } @Override public int compareTo(ParameterType o) { - if (preferForRegexpMatch() && !o.preferForRegexpMatch()) return -1; - if (o.preferForRegexpMatch() && !preferForRegexpMatch()) return 1; + if (preferForRegexpMatch() && !o.preferForRegexpMatch()) + return -1; + if (o.preferForRegexpMatch() && !preferForRegexpMatch()) + return 1; String name = getName() != null ? getName() : ""; String otherName = o.getName() != null ? o.getName() : ""; return name.compareTo(otherName); @@ -259,7 +289,8 @@ private static final class TransformerAdaptor implements CaptureGroupTransfor private final Transformer transformer; private TransformerAdaptor(Transformer transformer) { - if (transformer == null) throw new NullPointerException("transformer cannot be null"); + if (transformer == null) + throw new NullPointerException("transformer cannot be null"); this.transformer = transformer; } @@ -267,5 +298,7 @@ private TransformerAdaptor(Transformer transformer) { public T transform(String[] args) throws Throwable { return transformer.transform(args[0]); } + } + } diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java index 4c0b4f0ca5..d13f8de1fb 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/RegularExpression.java @@ -69,7 +69,7 @@ public List> match(String text, Type... typeHints) { parameterTypes.add(parameterType); } - return Argument.build(group, treeRegexp, parameterTypes); + return Argument.build(group, parameterTypes); } @Override diff --git a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java index f3cf7666d1..b2b9efede7 100644 --- a/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java +++ b/cucumber-expressions/java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java @@ -1,17 +1,27 @@ package io.cucumber.cucumberexpressions; +import io.cucumber.cucumberexpressions.Ast.Node; import org.apiguardian.api.API; @API(status = API.Status.STABLE) public final class UndefinedParameterTypeException extends CucumberExpressionException { private final String undefinedParameterTypeName; - UndefinedParameterTypeException(String undefinedParameterTypeName) { - super(String.format("Undefined parameter type {%s}. Please register a ParameterType for {%s}.", undefinedParameterTypeName, undefinedParameterTypeName)); + UndefinedParameterTypeException(String message, String undefinedParameterTypeName) { + super(message); this.undefinedParameterTypeName = undefinedParameterTypeName; } public String getUndefinedParameterTypeName() { return undefinedParameterTypeName; } + + static CucumberExpressionException createUndefinedParameterType(Node node, String expression, String undefinedParameterTypeName) { + return new UndefinedParameterTypeException(message( + node.start(), + expression, + pointAt(node), + "Undefined parameter type '" +undefinedParameterTypeName+ "'", + "Please register a ParameterType for '"+undefinedParameterTypeName+"'"), undefinedParameterTypeName); + } } diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java index 753bd10817..0404d02506 100644 --- a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java @@ -2,10 +2,10 @@ import org.junit.jupiter.api.Test; -import java.util.Collections; import java.util.List; import java.util.Locale; +import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; public class ArgumentTest { @@ -15,9 +15,9 @@ public void exposes_parameter_type() { ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); List> arguments = Argument.build( treeRegexp.match("three blind mice"), - treeRegexp, - Collections.singletonList(parameterTypeRegistry.lookupByTypeName("string"))); + singletonList(parameterTypeRegistry.lookupByTypeName("string"))); Argument argument = arguments.get(0); assertEquals("string", argument.getParameterType().getName()); } + } diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java new file mode 100644 index 0000000000..63d1c3e18d --- /dev/null +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionParserTest.java @@ -0,0 +1,45 @@ +package io.cucumber.cucumberexpressions; + +import io.cucumber.cucumberexpressions.Ast.Node; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static java.nio.file.Files.newDirectoryStream; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CucumberExpressionParserTest { + + private final CucumberExpressionParser parser = new CucumberExpressionParser(); + + private static List acceptance_tests_pass() throws IOException { + List paths = new ArrayList<>(); + newDirectoryStream(Paths.get("testdata", "ast")).forEach(paths::add); + paths.sort(Comparator.naturalOrder()); + return paths; + } + + @ParameterizedTest + @MethodSource + void acceptance_tests_pass(@ConvertWith(FileToExpectationConverter.class) Expectation expectation) { + if (expectation.getException() == null) { + Node node = parser.parse(expectation.getExpression()); + assertThat(node.toString(), is(expectation.getExpected())); + } else { + CucumberExpressionException exception = assertThrows( + CucumberExpressionException.class, + () -> parser.parse(expectation.getExpression())); + assertThat(exception.getMessage(), is(expectation.getException())); + } + } + +} diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionPatternTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionPatternTest.java deleted file mode 100644 index 0f16c39f54..0000000000 --- a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionPatternTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.cucumber.cucumberexpressions; - -import org.junit.jupiter.api.Test; - -import java.util.Locale; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * This test verifies that the regular expression generated - * from the cucumber expression is as expected. - */ -public class CucumberExpressionPatternTest { - - @Test - public void translates_no_args() { - assertPattern( - "hello", - "^hello$" - ); - } - - @Test - public void translates_alternation() { - assertPattern( - "I had/have a great/nice/charming friend", - "^I (?:had|have) a (?:great|nice|charming) friend$" - ); - } - - @Test - public void translates_alternation_with_non_alpha() { - assertPattern( - "I said Alpha1/Beta1", - "^I said (?:Alpha1|Beta1)$" - ); - } - - @Test - public void translates_parameters() { - assertPattern( - "I have {float} cukes at {int} o'clock", - "^I have (" + - "(?=.*\\d.*)" + - "[-+]?" + - "(?:\\d+(?:[,]?\\d+)*)*" + - "(?:[.](?=\\d.*))?\\d*" + - "(?:\\d+[E]-?\\d+)?) cukes at ((?:-?\\d+)|(?:\\d+)) o'clock$" - ); - } - - @Test - public void translates_parenthesis_to_non_capturing_optional_capture_group() { - assertPattern( - "I have many big(ish) cukes", - "^I have many big(?:ish)? cukes$" - ); - } - - @Test - public void translates_parenthesis_with_alpha_unicode() { - assertPattern( - "Привет, Мир(ы)!", - "^Привет, Мир(?:ы)?!$"); - } - - private void assertPattern(String expr, String expectedRegexp) { - CucumberExpression cucumberExpression = new CucumberExpression(expr, new ParameterTypeRegistry(Locale.ENGLISH)); - assertEquals(expectedRegexp, cucumberExpression.getRegexp().pattern()); - } - -} diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java index 4c7a3e6593..b72a126143 100644 --- a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTest.java @@ -1,243 +1,150 @@ package io.cucumber.cucumberexpressions; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.provider.MethodSource; +import java.io.IOException; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; +import static java.nio.file.Files.newDirectoryStream; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -public class CucumberExpressionTest { +class CucumberExpressionTest { + private final ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - @Test - public void documents_match_arguments() { - ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); - - String expr = "I have {int} cuke(s)"; - Expression expression = new CucumberExpression(expr, parameterTypeRegistry); - List> args = expression.match("I have 7 cukes"); - assertEquals(7, args.get(0).getValue()); - } - - @Test - public void matches_word() { - assertEquals(singletonList("blind"), match("three {word} mice", "three blind mice")); - } - - @Test - public void matches_double_quoted_string() { - assertEquals(singletonList("blind"), match("three {string} mice", "three \"blind\" mice")); - } - - @Test - public void matches_multiple_double_quoted_strings() { - assertEquals(asList("blind", "crippled"), match("three {string} and {string} mice", "three \"blind\" and \"crippled\" mice")); - } - - @Test - public void matches_single_quoted_string() { - assertEquals(singletonList("blind"), match("three {string} mice", "three 'blind' mice")); - } - - @Test - public void matches_multiple_single_quoted_strings() { - assertEquals(asList("blind", "crippled"), match("three {string} and {string} mice", "three 'blind' and 'crippled' mice")); - } - - @Test - public void does_not_match_misquoted_string() { - assertNull(match("three {string} mice", "three \"blind' mice")); - } - - @Test - public void matches_single_quoted_string_with_double_quotes() { - assertEquals(singletonList("\"blind\""), match("three {string} mice", "three '\"blind\"' mice")); - } - - @Test - public void matches_double_quoted_string_with_single_quotes() { - assertEquals(singletonList("'blind'"), match("three {string} mice", "three \"'blind'\" mice")); - } - - @Test - public void matches_double_quoted_string_with_escaped_double_quote() { - assertEquals(singletonList("bl\"nd"), match("three {string} mice", "three \"bl\\\"nd\" mice")); - } - - @Test - public void matches_single_quoted_string_with_escaped_single_quote() { - assertEquals(singletonList("bl'nd"), match("three {string} mice", "three 'bl\\'nd' mice")); - } - - @Test - public void matches_single_quoted_empty_string_as_empty_string() { - assertEquals(singletonList(""), match("three {string} mice", "three '' mice")); - } - - @Test - public void matches_double_quoted_empty_string_as_empty_string() { - assertEquals(singletonList(""), match("three {string} mice", "three \"\" mice")); + private static List expression_acceptance_tests_pass() throws IOException { + List paths = new ArrayList<>(); + newDirectoryStream(Paths.get("testdata", "expression")).forEach(paths::add); + paths.sort(Comparator.naturalOrder()); + return paths; } - @Test - public void matches_single_quoted_empty_string_as_empty_string_along_with_other_strings() { - assertEquals(asList("", "handsome"), match("three {string} and {string} mice", "three '' and 'handsome' mice")); + private static List regex_acceptance_tests_pass() throws IOException { + List paths = new ArrayList<>(); + newDirectoryStream(Paths.get("testdata", "regex")).forEach(paths::add); + paths.sort(Comparator.naturalOrder()); + return paths; } - @Test - public void matches_double_quoted_empty_string_as_empty_string_along_with_other_strings() { - assertEquals(asList("", "handsome"), match("three {string} and {string} mice", "three \"\" and \"handsome\" mice")); - } + @ParameterizedTest + @MethodSource + void expression_acceptance_tests_pass(@ConvertWith(FileToExpectationConverter.class) Expectation expectation) { + if (expectation.getException() == null) { + CucumberExpression expression = new CucumberExpression(expectation.getExpression(), parameterTypeRegistry); + List> match = expression.match(expectation.getText()); + List values = match == null ? null : match.stream() + .map(Argument::getValue) + .collect(Collectors.toList()); - @Test - public void matches_escaped_parenthesis() { - assertEquals(singletonList("blind"), match("three \\(exceptionally) {string} mice", "three (exceptionally) \"blind\" mice")); - } + Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .create(); - @Test - public void matches_escaped_slash() { - assertEquals(emptyList(), match("12\\/2020", "12/2020")); + assertEquals(expectation.getExpected(), gson.toJson(values)); + } else { + Executable executable = () -> { + String expr = expectation.getExpression(); + CucumberExpression expression = new CucumberExpression(expr, parameterTypeRegistry); + expression.match(expectation.getText()); + }; + CucumberExpressionException exception = assertThrows(CucumberExpressionException.class, executable); + assertThat(exception.getMessage(), equalTo(expectation.getException())); + } } - @Test - public void matches_int() { - assertEquals(singletonList(22), match("{int}", "22")); + @ParameterizedTest + @MethodSource + void regex_acceptance_tests_pass(@ConvertWith(FileToExpectationConverter.class) Expectation expectation) { + CucumberExpression expression = new CucumberExpression(expectation.getExpression(), parameterTypeRegistry); + assertEquals(expectation.getExpected(), expression.getRegexp().pattern()); } - @Test - public void doesnt_match_float_as_int() { - assertNull(match("{int}", "1.22")); - } + // Misc tests @Test - public void matches_float() { - assertEquals(singletonList(0.22f), match("{float}", "0.22")); - assertEquals(singletonList(0.22f), match("{float}", ".22")); + void exposes_source() { + String expr = "I have {int} cuke(s)"; + assertEquals(expr, new CucumberExpression(expr, new ParameterTypeRegistry(Locale.ENGLISH)).getSource()); } + // Java-specific @Test - public void matches_anonymous_parameter_type_with_hint() { + void matches_anonymous_parameter_type_with_hint() { assertEquals(singletonList(0.22f), match("{}", "0.22", Float.class)); } @Test - public void matches_anonymous_parameter_type() { - assertEquals(singletonList("0.22"), match("{}", "0.22")); - } - - @Test - public void does_not_allow_parameter_type_with_left_bracket() { - - final Executable testMethod = () -> match("{[string]}", "something"); - - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Illegal character '[' in parameter name {[string]}."))); - } - - @Test - public void throws_unknown_parameter_type() { - - final Executable testMethod = () -> match("{unknown}", "something"); - - final UndefinedParameterTypeException thrownException = assertThrows(UndefinedParameterTypeException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Undefined parameter type {unknown}. Please register a ParameterType for {unknown}."))); - } - - @Test - public void does_not_allow_optional_parameter_types() { - - final Executable testMethod = () -> match("({int})", "3"); - - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Parameter types cannot be optional: ({int})"))); - } - - @Test - public void allows_escaped_optional_parameter_types() { - assertEquals(singletonList(3), match("\\({int})", "(3)")); - } - - @Test - public void does_not_allow_text_parameter_type_alternation() { - - final Executable testMethod = () -> match("x/{int}", "3"); - - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Parameter types cannot be alternative: x/{int}"))); - } - - @Test - public void does_not_allow_parameter_type_text_alternation() { - - final Executable testMethod = () -> match("{int}/x", "3"); - - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Parameter types cannot be alternative: {int}/x"))); - } - - @Test - public void exposes_source() { + void documents_match_arguments() { String expr = "I have {int} cuke(s)"; - assertEquals(expr, new CucumberExpression(expr, new ParameterTypeRegistry(Locale.ENGLISH)).getSource()); + Expression expression = new CucumberExpression(expr, parameterTypeRegistry); + List> args = expression.match("I have 7 cukes"); + assertEquals(7, args.get(0).getValue()); } - // Java-specific - @Test - public void matches_byte() { + void matches_byte() { assertEquals(singletonList(Byte.MAX_VALUE), match("{byte}", "127")); } @Test - public void matches_short() { + void matches_short() { assertEquals(singletonList(Short.MAX_VALUE), match("{short}", String.valueOf(Short.MAX_VALUE))); } @Test - public void matches_long() { + void matches_long() { assertEquals(singletonList(Long.MAX_VALUE), match("{long}", String.valueOf(Long.MAX_VALUE))); } @Test - public void matches_biginteger() { + void matches_biginteger() { BigInteger bigInteger = BigInteger.valueOf(Long.MAX_VALUE); bigInteger = bigInteger.pow(10); assertEquals(singletonList(bigInteger), match("{biginteger}", bigInteger.toString())); } @Test - public void matches_bigdecimal() { + void matches_bigdecimal() { BigDecimal bigDecimal = BigDecimal.valueOf(Math.PI); assertEquals(singletonList(bigDecimal), match("{bigdecimal}", bigDecimal.toString())); } @Test - public void matches_double_with_comma_for_locale_using_comma() { + void matches_double_with_comma_for_locale_using_comma() { List values = match("{double}", "1,22", Locale.FRANCE); assertEquals(singletonList(1.22), values); } @Test - public void matches_float_with_zero() { + void matches_float_with_zero() { List values = match("{float}", "0", Locale.ENGLISH); assertEquals(0.0f, values.get(0)); } @Test - public void unmatched_optional_groups_have_null_values() { + void unmatched_optional_groups_have_null_values() { ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); parameterTypeRegistry.defineParameterType(new ParameterType<>( "textAndOrNumber", @@ -258,7 +165,7 @@ public List transform(String... args) { } private List match(String expr, String text, Type... typeHints) { - return match(expr, text, Locale.ENGLISH, typeHints); + return match(expr, text, parameterTypeRegistry, typeHints); } private List match(String expr, String text, Locale locale, Type... typeHints) { diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java new file mode 100644 index 0000000000..aaf5084672 --- /dev/null +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CucumberExpressionTokenizerTest.java @@ -0,0 +1,50 @@ +package io.cucumber.cucumberexpressions; + +import io.cucumber.cucumberexpressions.Ast.Token; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static java.nio.file.Files.newDirectoryStream; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CucumberExpressionTokenizerTest { + + private final CucumberExpressionTokenizer tokenizer = new CucumberExpressionTokenizer(); + + private static List acceptance_tests_pass() throws IOException { + List paths = new ArrayList<>(); + newDirectoryStream(Paths.get("testdata", "tokens")).forEach(paths::add); + paths.sort(Comparator.naturalOrder()); + return paths; + } + + @ParameterizedTest + @MethodSource + void acceptance_tests_pass(@ConvertWith(FileToExpectationConverter.class) Expectation expectation) { + if (expectation.getException() == null) { + String tokens = tokenizer + .tokenize(expectation.getExpression()) + .stream() + .map(Token::toString) + .collect(Collectors.joining(",\n ", "[\n ","\n]")); + assertThat(tokens, is(expectation.getExpected())); + } else { + CucumberExpressionException exception = assertThrows( + CucumberExpressionException.class, + () -> tokenizer.tokenize(expectation.getExpression())); + assertThat(exception.getMessage(), is(expectation.getException())); + } + } + +} diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java index 759a5b7db9..81eb19cead 100644 --- a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/CustomParameterTypeTest.java @@ -66,7 +66,7 @@ public void create_parameter() { public void throws_exception_for_illegal_character_in_parameter_name() { final Executable testMethod = () -> new ParameterType<>( - "[string]", + "(string)", ".*", String.class, (Transformer) s -> s, @@ -75,7 +75,7 @@ public void throws_exception_for_illegal_character_in_parameter_name() { ); final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Illegal character '[' in parameter name {[string]}."))); + assertThat(thrownException.getMessage(), is(equalTo("Illegal character in parameter name {(string)}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'"))); } @Test diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/Expectation.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/Expectation.java new file mode 100644 index 0000000000..6f865b2d7e --- /dev/null +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/Expectation.java @@ -0,0 +1,59 @@ +package io.cucumber.cucumberexpressions; + +import java.util.Map; + +class Expectation { + String expression; + String text; + String expected; + String exception; + + Expectation(String expression, String text, String expected, String exception) { + this.expression = expression; + this.text = text; + this.expected = expected; + this.exception = exception; + } + + public String getExpression() { + return expression; + } + + + public String getException() { + return exception; + } + + public static Expectation fromMap(Map map) { + return new Expectation( + (String) map.get("expression"), + (String) map.get("text"), + (String) map.get("expected"), + (String) map.get("exception")); + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public void setException(String exception) { + this.exception = exception; + } + + public String getExpected() { + return expected; + } + + public void setExpected(String expected) { + this.expected = expected; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionExamplesTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionExamplesTest.java index 086f2f7126..2b1c08eb50 100644 --- a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionExamplesTest.java +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionExamplesTest.java @@ -6,7 +6,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.io.IOException; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -27,7 +27,7 @@ static Collection data() throws IOException { final Collection data = new ArrayList<>(); - String s = new String(readAllBytes(get("examples.txt")), Charset.forName("UTF-8")); + String s = new String(readAllBytes(get("examples.txt")), StandardCharsets.UTF_8); String[] chunks = s.split("---"); for (String chunk : chunks) { chunk = chunk.trim(); diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java index f83468fb71..ee0826651d 100644 --- a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/ExpressionFactoryTest.java @@ -46,7 +46,7 @@ public void creates_cucumber_expression_for_escaped_parenthesis_with_alpha() { @Test public void creates_cucumber_expression_for_parenthesis_with_regex_symbols() { - assertCucumberExpression("the temperature is (\\+){int} degrees celsius"); + assertCucumberExpression("the temperature is (+){int} degrees celsius"); } @Test @@ -72,13 +72,6 @@ public void explains_cukexp_regexp_mix() { assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("You cannot use anchors (^ or $) in Cucumber Expressions. Please remove them from ^the seller has {int} strike(s)$"))); } - @Test - public void explains_undefined_parameter_types() { - final Executable testMethod = () -> createExpression("{x}"); - final CucumberExpressionException thrownException = assertThrows(CucumberExpressionException.class, testMethod); - assertThat("Unexpected message", thrownException.getMessage(), is(equalTo("Undefined parameter type {x}. Please register a ParameterType for {x}."))); - } - private void assertRegularExpression(String expressionString) { assertRegularExpression(expressionString, expressionString); } diff --git a/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/FileToExpectationConverter.java b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/FileToExpectationConverter.java new file mode 100644 index 0000000000..53999faf58 --- /dev/null +++ b/cucumber-expressions/java/src/test/java/io/cucumber/cucumberexpressions/FileToExpectationConverter.java @@ -0,0 +1,31 @@ +package io.cucumber.cucumberexpressions; + +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ArgumentConverter; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Map; + +import static io.cucumber.cucumberexpressions.Expectation.fromMap; +import static java.nio.file.Files.newInputStream; + +class FileToExpectationConverter implements ArgumentConverter { + Yaml yaml = new Yaml(); + + @Override + public Object convert(Object source, ParameterContext context) throws ArgumentConversionException { + try { + Path path = (Path) source; + InputStream inputStream = newInputStream(path); + Map map = yaml.loadAs(inputStream, Map.class); + return fromMap(map); + } catch (IOException e) { + throw new ArgumentConversionException("Could not load " + source, e); + } + } + +} diff --git a/cucumber-expressions/java/testdata/ast/alternation-followed-by-optional.yaml b/cucumber-expressions/java/testdata/ast/alternation-followed-by-optional.yaml new file mode 100644 index 0000000000..0bfb53a534 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation-followed-by-optional.yaml @@ -0,0 +1,17 @@ +expression: three blind\ rat/cat(s) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 16, "token": "blind rat"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 17, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 17, "end": 20, "token": "cat"}, + {"type": "OPTIONAL_NODE", "start": 20, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": "s"} + ]} + ]} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/alternation-phrase.yaml b/cucumber-expressions/java/testdata/ast/alternation-phrase.yaml new file mode 100644 index 0000000000..9a243822d5 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation-phrase.yaml @@ -0,0 +1,16 @@ +expression: three hungry/blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 12, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 12, "token": "hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 13, "end": 18, "token": "blind"} + ]} + ]}, + {"type": "TEXT_NODE", "start": 18, "end": 19, "token": " "}, + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "mice"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/alternation-with-parameter.yaml b/cucumber-expressions/java/testdata/ast/alternation-with-parameter.yaml new file mode 100644 index 0000000000..c5daf32bd7 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation-with-parameter.yaml @@ -0,0 +1,27 @@ +expression: I select the {int}st/nd/rd/th +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "I"}, + {"type": "TEXT_NODE", "start": 1, "end": 2, "token": " "}, + {"type": "TEXT_NODE", "start": 2, "end": 8, "token": "select"}, + {"type": "TEXT_NODE", "start": 8, "end": 9, "token": " "}, + {"type": "TEXT_NODE", "start": 9, "end": 12, "token": "the"}, + {"type": "TEXT_NODE", "start": 12, "end": 13, "token": " "}, + {"type": "PARAMETER_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 14, "end": 17, "token": "int"} + ]}, + {"type": "ALTERNATION_NODE", "start": 18, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 18, "end": 20, "nodes": [ + {"type": "TEXT_NODE", "start": 18, "end": 20, "token": "st"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 21, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 23, "token": "nd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 24, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 24, "end": 26, "token": "rd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 27, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 27, "end": 29, "token": "th"} + ]} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/alternation-with-unused-end-optional.yaml b/cucumber-expressions/java/testdata/ast/alternation-with-unused-end-optional.yaml new file mode 100644 index 0000000000..842838b75f --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation-with-unused-end-optional.yaml @@ -0,0 +1,15 @@ +expression: three )blind\ mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 7, "token": ")"}, + {"type": "TEXT_NODE", "start": 7, "end": 18, "token": "blind mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 19, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/alternation-with-unused-start-optional.yaml b/cucumber-expressions/java/testdata/ast/alternation-with-unused-start-optional.yaml new file mode 100644 index 0000000000..e2f0584556 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation-with-unused-start-optional.yaml @@ -0,0 +1,8 @@ +expression: three blind\ mice/rats( +exception: |- + This Cucumber Expression has a problem at column 23: + + three blind\ mice/rats( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/java/testdata/ast/alternation-with-white-space.yaml b/cucumber-expressions/java/testdata/ast/alternation-with-white-space.yaml new file mode 100644 index 0000000000..eedd57dd21 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation-with-white-space.yaml @@ -0,0 +1,12 @@ +expression: '\ three\ hungry/blind\ mice\ ' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 15, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 15, "token": " three hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 16, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 16, "end": 29, "token": "blind mice "} + ]} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/alternation.yaml b/cucumber-expressions/java/testdata/ast/alternation.yaml new file mode 100644 index 0000000000..88df8325fe --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/alternation.yaml @@ -0,0 +1,12 @@ +expression: mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 4, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 4, "token": "mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 5, "end": 9, "nodes": [ + {"type": "TEXT_NODE", "start": 5, "end": 9, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/anonymous-parameter.yaml b/cucumber-expressions/java/testdata/ast/anonymous-parameter.yaml new file mode 100644 index 0000000000..2c4d339333 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/anonymous-parameter.yaml @@ -0,0 +1,5 @@ +expression: "{}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 2, "nodes": []} + ]} diff --git a/cucumber-expressions/java/testdata/ast/closing-brace.yaml b/cucumber-expressions/java/testdata/ast/closing-brace.yaml new file mode 100644 index 0000000000..1bafd9c6a8 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/closing-brace.yaml @@ -0,0 +1,5 @@ +expression: "}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "}"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/closing-parenthesis.yaml b/cucumber-expressions/java/testdata/ast/closing-parenthesis.yaml new file mode 100644 index 0000000000..23daf7bcd3 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/closing-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: ) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": ")"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/empty-alternation.yaml b/cucumber-expressions/java/testdata/ast/empty-alternation.yaml new file mode 100644 index 0000000000..6d810fc8f3 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/empty-alternation.yaml @@ -0,0 +1,8 @@ +expression: / +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/empty-alternations.yaml b/cucumber-expressions/java/testdata/ast/empty-alternations.yaml new file mode 100644 index 0000000000..f8d4dd4cf8 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/empty-alternations.yaml @@ -0,0 +1,9 @@ +expression: '//' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 2, "end": 2, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/empty-string.yaml b/cucumber-expressions/java/testdata/ast/empty-string.yaml new file mode 100644 index 0000000000..4d33c2dc76 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/empty-string.yaml @@ -0,0 +1,3 @@ +expression: "" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 0, "nodes": []} diff --git a/cucumber-expressions/java/testdata/ast/escaped-alternation.yaml b/cucumber-expressions/java/testdata/ast/escaped-alternation.yaml new file mode 100644 index 0000000000..3ed9c37674 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/escaped-alternation.yaml @@ -0,0 +1,5 @@ +expression: 'mice\/rats' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 10, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 10, "token": "mice/rats"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/escaped-backslash.yaml b/cucumber-expressions/java/testdata/ast/escaped-backslash.yaml new file mode 100644 index 0000000000..da2d008e1e --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/escaped-backslash.yaml @@ -0,0 +1,5 @@ +expression: '\\' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "\\"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/escaped-opening-parenthesis.yaml b/cucumber-expressions/java/testdata/ast/escaped-opening-parenthesis.yaml new file mode 100644 index 0000000000..afafc59eb8 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/escaped-opening-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: '\(' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "("} + ]} diff --git a/cucumber-expressions/java/testdata/ast/escaped-optional-followed-by-optional.yaml b/cucumber-expressions/java/testdata/ast/escaped-optional-followed-by-optional.yaml new file mode 100644 index 0000000000..1e4746291b --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/escaped-optional-followed-by-optional.yaml @@ -0,0 +1,15 @@ +expression: three \((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 8, "token": "("}, + {"type": "OPTIONAL_NODE", "start": 8, "end": 14, "nodes": [ + {"type": "TEXT_NODE", "start": 9, "end": 13, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 20, "token": "blind"}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": ")"}, + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": " "}, + {"type": "TEXT_NODE", "start": 22, "end": 26, "token": "mice"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/escaped-optional-phrase.yaml b/cucumber-expressions/java/testdata/ast/escaped-optional-phrase.yaml new file mode 100644 index 0000000000..832249e2a7 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/escaped-optional-phrase.yaml @@ -0,0 +1,10 @@ +expression: three \(blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 19, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 13, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": ")"}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 19, "token": "mice"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/escaped-optional.yaml b/cucumber-expressions/java/testdata/ast/escaped-optional.yaml new file mode 100644 index 0000000000..4c2b457d6f --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: '\(blind)' + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 7, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 7, "end": 8, "token": ")"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/opening-brace.yaml b/cucumber-expressions/java/testdata/ast/opening-brace.yaml new file mode 100644 index 0000000000..916a674a36 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/opening-brace.yaml @@ -0,0 +1,8 @@ +expression: '{' +exception: |- + This Cucumber Expression has a problem at column 1: + + { + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/java/testdata/ast/opening-parenthesis.yaml b/cucumber-expressions/java/testdata/ast/opening-parenthesis.yaml new file mode 100644 index 0000000000..929d6ae304 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/opening-parenthesis.yaml @@ -0,0 +1,8 @@ +expression: ( +exception: |- + This Cucumber Expression has a problem at column 1: + + ( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/java/testdata/ast/optional-containing-nested-optional.yaml b/cucumber-expressions/java/testdata/ast/optional-containing-nested-optional.yaml new file mode 100644 index 0000000000..0fdd55d46b --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/optional-containing-nested-optional.yaml @@ -0,0 +1,15 @@ +expression: three ((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 25, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 20, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 7, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 8, "end": 12, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 19, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": " "}, + {"type": "TEXT_NODE", "start": 21, "end": 25, "token": "mice"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/optional-phrase.yaml b/cucumber-expressions/java/testdata/ast/optional-phrase.yaml new file mode 100644 index 0000000000..0ef4fb3f95 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/optional-phrase.yaml @@ -0,0 +1,12 @@ +expression: three (blind) mice + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 7, "end": 12, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 18, "token": "mice"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/optional.yaml b/cucumber-expressions/java/testdata/ast/optional.yaml new file mode 100644 index 0000000000..6ce2b632e7 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/optional.yaml @@ -0,0 +1,7 @@ +expression: (blind) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 6, "token": "blind"} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/parameter.yaml b/cucumber-expressions/java/testdata/ast/parameter.yaml new file mode 100644 index 0000000000..92ac8c147e --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/parameter.yaml @@ -0,0 +1,7 @@ +expression: "{string}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 7, "token": "string"} + ]} + ]} diff --git a/cucumber-expressions/java/testdata/ast/phrase.yaml b/cucumber-expressions/java/testdata/ast/phrase.yaml new file mode 100644 index 0000000000..ba340d0122 --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/phrase.yaml @@ -0,0 +1,9 @@ +expression: three blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 11, "token": "blind"}, + {"type": "TEXT_NODE", "start": 11, "end": 12, "token": " "}, + {"type": "TEXT_NODE", "start": 12, "end": 16, "token": "mice"} + ]} diff --git a/cucumber-expressions/java/testdata/ast/unfinished-parameter.yaml b/cucumber-expressions/java/testdata/ast/unfinished-parameter.yaml new file mode 100644 index 0000000000..d02f9b4ccf --- /dev/null +++ b/cucumber-expressions/java/testdata/ast/unfinished-parameter.yaml @@ -0,0 +1,8 @@ +expression: "{string" +exception: |- + This Cucumber Expression has a problem at column 1: + + {string + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/java/testdata/expression/allows-escaped-optional-parameter-types.yaml b/cucumber-expressions/java/testdata/expression/allows-escaped-optional-parameter-types.yaml new file mode 100644 index 0000000000..a00b45acef --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/allows-escaped-optional-parameter-types.yaml @@ -0,0 +1,4 @@ +expression: \({int}) +text: (3) +expected: |- + [3] diff --git a/cucumber-expressions/java/testdata/expression/allows-parameter-type-in-alternation-1.yaml b/cucumber-expressions/java/testdata/expression/allows-parameter-type-in-alternation-1.yaml new file mode 100644 index 0000000000..bb1a6f21b1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/allows-parameter-type-in-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: i18n +expected: |- + [18] diff --git a/cucumber-expressions/java/testdata/expression/allows-parameter-type-in-alternation-2.yaml b/cucumber-expressions/java/testdata/expression/allows-parameter-type-in-alternation-2.yaml new file mode 100644 index 0000000000..cdddce7d84 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/allows-parameter-type-in-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: a11y +expected: |- + [11] diff --git a/cucumber-expressions/java/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml b/cucumber-expressions/java/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml new file mode 100644 index 0000000000..9e2ecdfbe1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml @@ -0,0 +1,5 @@ +expression: |- + {int}st/nd/rd/th +text: 3rd +expected: |- + [3] diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-in-optional.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-in-optional.yaml new file mode 100644 index 0000000000..b507e27220 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-in-optional.yaml @@ -0,0 +1,9 @@ +expression: three( brown/black) mice +text: three brown/black mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three( brown/black) mice + ^ + An alternation can not be used inside an optional. + You can use '\/' to escape the the '/' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml new file mode 100644 index 0000000000..b32540a4a9 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml @@ -0,0 +1,10 @@ +expression: |- + {int}/x +text: '3' +exception: |- + This Cucumber Expression has a problem at column 6: + + {int}/x + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml new file mode 100644 index 0000000000..a0aab0e5a9 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml @@ -0,0 +1,9 @@ +expression: three (brown)/black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (brown)/black mice + ^-----^ + An alternative may not exclusively contain optionals. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml new file mode 100644 index 0000000000..50250f00aa --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml @@ -0,0 +1,9 @@ +expression: x/{int} +text: '3' +exception: |- + This Cucumber Expression has a problem at column 3: + + x/{int} + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml new file mode 100644 index 0000000000..b724cfa77f --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml @@ -0,0 +1,9 @@ +expression: three brown//black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three brown//black mice + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-empty-optional.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-empty-optional.yaml new file mode 100644 index 0000000000..00e341af0b --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-empty-optional.yaml @@ -0,0 +1,9 @@ +expression: three () mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three () mice + ^^ + An optional must contain some text. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-nested-optional.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-nested-optional.yaml new file mode 100644 index 0000000000..017c3be25d --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-nested-optional.yaml @@ -0,0 +1,8 @@ +expression: "(a(b))" +exception: |- + This Cucumber Expression has a problem at column 3: + + (a(b)) + ^-^ + 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. diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-optional-parameter-types.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-optional-parameter-types.yaml new file mode 100644 index 0000000000..b88061e9b4 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-optional-parameter-types.yaml @@ -0,0 +1,9 @@ +expression: ({int}) +text: '3' +exception: |- + This Cucumber Expression has a problem at column 2: + + ({int}) + ^---^ + 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 '{' diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml new file mode 100644 index 0000000000..d1c89689e9 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml @@ -0,0 +1,10 @@ +expression: |- + {(string)} +text: something +exception: |- + This Cucumber Expression has a problem at column 2: + + {(string)} + ^ + Parameter names may not contain '{', '}', '(', ')', '\' or '/'. + Did you mean to use a regular expression? diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml new file mode 100644 index 0000000000..e033648972 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string\} mice +exception: |- + This Cucumber Expression has a problem at column 24: + + three (exceptionally\) {string\} mice + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml new file mode 100644 index 0000000000..7cb9c6d56a --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string} mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (exceptionally\) {string} mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml b/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml new file mode 100644 index 0000000000..029c4e63bd --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml @@ -0,0 +1,8 @@ +expression: three ((exceptionally\) strong) mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three ((exceptionally\) strong) mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/java/testdata/expression/does-not-match-misquoted-string.yaml b/cucumber-expressions/java/testdata/expression/does-not-match-misquoted-string.yaml new file mode 100644 index 0000000000..18023180af --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/does-not-match-misquoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind' mice +expected: |- + null diff --git a/cucumber-expressions/java/testdata/expression/doesnt-match-float-as-int.yaml b/cucumber-expressions/java/testdata/expression/doesnt-match-float-as-int.yaml new file mode 100644 index 0000000000..d66b586430 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/doesnt-match-float-as-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '1.22' +expected: |- + null diff --git a/cucumber-expressions/java/testdata/expression/matches-alternation.yaml b/cucumber-expressions/java/testdata/expression/matches-alternation.yaml new file mode 100644 index 0000000000..20a9b9a728 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-alternation.yaml @@ -0,0 +1,4 @@ +expression: mice/rats and rats\/mice +text: rats and rats/mice +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-anonymous-parameter-type.yaml b/cucumber-expressions/java/testdata/expression/matches-anonymous-parameter-type.yaml new file mode 100644 index 0000000000..fc954960df --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-anonymous-parameter-type.yaml @@ -0,0 +1,5 @@ +expression: |- + {} +text: '0.22' +expected: |- + ["0.22"] diff --git a/cucumber-expressions/java/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/java/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c3e1962e22 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "" and "handsome" mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/java/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/java/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..89315b62ef --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "" mice +expected: |- + [""] diff --git a/cucumber-expressions/java/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml b/cucumber-expressions/java/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml new file mode 100644 index 0000000000..fe30a044c1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "bl\"nd" mice +expected: |- + ["bl\"nd"] diff --git a/cucumber-expressions/java/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml b/cucumber-expressions/java/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml new file mode 100644 index 0000000000..25fcf304c1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "'blind'" mice +expected: |- + ["'blind'"] diff --git a/cucumber-expressions/java/testdata/expression/matches-double-quoted-string.yaml b/cucumber-expressions/java/testdata/expression/matches-double-quoted-string.yaml new file mode 100644 index 0000000000..8b0560f332 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-double-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-parenthesis.yaml b/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-parenthesis.yaml new file mode 100644 index 0000000000..902a084103 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-parenthesis.yaml @@ -0,0 +1,4 @@ +expression: three \\(exceptionally) \\{string} mice +text: three \exceptionally \"blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-slash-1.yaml b/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-slash-1.yaml new file mode 100644 index 0000000000..94e531eca7 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-slash-1.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: 12\ +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-slash-2.yaml b/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-slash-2.yaml new file mode 100644 index 0000000000..9c9c735b86 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-doubly-escaped-slash-2.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: '2020' +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-1.yaml b/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-1.yaml new file mode 100644 index 0000000000..171df89ee1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-1.yaml @@ -0,0 +1,4 @@ +expression: three \(exceptionally) \{string} mice +text: three (exceptionally) {string} mice +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-2.yaml b/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-2.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-2.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-3.yaml b/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-3.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-escaped-parenthesis-3.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/java/testdata/expression/matches-escaped-slash.yaml b/cucumber-expressions/java/testdata/expression/matches-escaped-slash.yaml new file mode 100644 index 0000000000..d8b3933399 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-escaped-slash.yaml @@ -0,0 +1,4 @@ +expression: 12\/2020 +text: 12/2020 +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-float-1.yaml b/cucumber-expressions/java/testdata/expression/matches-float-1.yaml new file mode 100644 index 0000000000..fe7e8b1869 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-float-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '0.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/java/testdata/expression/matches-float-2.yaml b/cucumber-expressions/java/testdata/expression/matches-float-2.yaml new file mode 100644 index 0000000000..c1e5894eac --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-float-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/java/testdata/expression/matches-int.yaml b/cucumber-expressions/java/testdata/expression/matches-int.yaml new file mode 100644 index 0000000000..bcd3763886 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '22' +expected: |- + [22] diff --git a/cucumber-expressions/java/testdata/expression/matches-multiple-double-quoted-strings.yaml b/cucumber-expressions/java/testdata/expression/matches-multiple-double-quoted-strings.yaml new file mode 100644 index 0000000000..6c74bc2350 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-multiple-double-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "blind" and "crippled" mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/java/testdata/expression/matches-multiple-single-quoted-strings.yaml b/cucumber-expressions/java/testdata/expression/matches-multiple-single-quoted-strings.yaml new file mode 100644 index 0000000000..5037821c14 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-multiple-single-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three 'blind' and 'crippled' mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-1.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-1.yaml new file mode 100644 index 0000000000..821776715b --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three brown mice +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-2.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-2.yaml new file mode 100644 index 0000000000..71b3a341f1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three rats +expected: |- + [] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml new file mode 100644 index 0000000000..2632f410ce --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 2 seconds? +expected: |- + [2] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml new file mode 100644 index 0000000000..7b30f667bc --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 1 second. +expected: |- + [1] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-1.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-1.yaml new file mode 100644 index 0000000000..6574bb4bdf --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 3 rats +expected: |- + [3] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-2.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-2.yaml new file mode 100644 index 0000000000..4eb0f0e1c6 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 2 mice +expected: |- + [2] diff --git a/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-3.yaml b/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-3.yaml new file mode 100644 index 0000000000..964fa6489e --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-optional-in-alternation-3.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 1 mouse +expected: |- + [1] diff --git a/cucumber-expressions/java/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/java/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c963dcf1c7 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three '' and 'handsome' mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/java/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/java/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..cff2a2d1ec --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '' mice +expected: |- + [""] diff --git a/cucumber-expressions/java/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml b/cucumber-expressions/java/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml new file mode 100644 index 0000000000..eb9ed537cd --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '"blind"' mice +expected: |- + ["\"blind\""] diff --git a/cucumber-expressions/java/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml b/cucumber-expressions/java/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml new file mode 100644 index 0000000000..4c2b0055b9 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'bl\'nd' mice +expected: |- + ["bl'nd"] diff --git a/cucumber-expressions/java/testdata/expression/matches-single-quoted-string.yaml b/cucumber-expressions/java/testdata/expression/matches-single-quoted-string.yaml new file mode 100644 index 0000000000..6c8f4652a5 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-single-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'blind' mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/java/testdata/expression/matches-word.yaml b/cucumber-expressions/java/testdata/expression/matches-word.yaml new file mode 100644 index 0000000000..358fd3afd1 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/matches-word.yaml @@ -0,0 +1,4 @@ +expression: three {word} mice +text: three blind mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/java/testdata/expression/throws-unknown-parameter-type.yaml b/cucumber-expressions/java/testdata/expression/throws-unknown-parameter-type.yaml new file mode 100644 index 0000000000..384e3a48c3 --- /dev/null +++ b/cucumber-expressions/java/testdata/expression/throws-unknown-parameter-type.yaml @@ -0,0 +1,10 @@ +expression: |- + {unknown} +text: something +exception: |- + This Cucumber Expression has a problem at column 1: + + {unknown} + ^-------^ + Undefined parameter type 'unknown'. + Please register a ParameterType for 'unknown' diff --git a/cucumber-expressions/java/testdata/regex/alternation-with-optional.yaml b/cucumber-expressions/java/testdata/regex/alternation-with-optional.yaml new file mode 100644 index 0000000000..73787b2b0a --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/alternation-with-optional.yaml @@ -0,0 +1,2 @@ +expression: "a/b(c)" +expected: ^(?:a|b(?:c)?)$ diff --git a/cucumber-expressions/java/testdata/regex/alternation.yaml b/cucumber-expressions/java/testdata/regex/alternation.yaml new file mode 100644 index 0000000000..1dc293fb62 --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/alternation.yaml @@ -0,0 +1,2 @@ +expression: "a/b c/d/e" +expected: ^(?:a|b) (?:c|d|e)$ diff --git a/cucumber-expressions/java/testdata/regex/empty.yaml b/cucumber-expressions/java/testdata/regex/empty.yaml new file mode 100644 index 0000000000..bb9a81906c --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/empty.yaml @@ -0,0 +1,2 @@ +expression: "" +expected: ^$ diff --git a/cucumber-expressions/java/testdata/regex/escape-regex-characters.yaml b/cucumber-expressions/java/testdata/regex/escape-regex-characters.yaml new file mode 100644 index 0000000000..c8ea8c549e --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/escape-regex-characters.yaml @@ -0,0 +1,2 @@ +expression: '^$[]\(\){}\\.|?*+' +expected: ^\^\$\[\]\(\)(.*)\\\.\|\?\*\+$ diff --git a/cucumber-expressions/java/testdata/regex/optional.yaml b/cucumber-expressions/java/testdata/regex/optional.yaml new file mode 100644 index 0000000000..7d6d84cc14 --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/optional.yaml @@ -0,0 +1,2 @@ +expression: "(a)" +expected: ^(?:a)?$ diff --git a/cucumber-expressions/java/testdata/regex/parameter.yaml b/cucumber-expressions/java/testdata/regex/parameter.yaml new file mode 100644 index 0000000000..f793b21c0f --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/parameter.yaml @@ -0,0 +1,2 @@ +expression: "{int}" +expected: ^((?:-?\d+)|(?:\d+))$ diff --git a/cucumber-expressions/java/testdata/regex/text.yaml b/cucumber-expressions/java/testdata/regex/text.yaml new file mode 100644 index 0000000000..2af3e41664 --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/text.yaml @@ -0,0 +1,2 @@ +expression: "a" +expected: ^a$ diff --git a/cucumber-expressions/java/testdata/regex/unicode.yaml b/cucumber-expressions/java/testdata/regex/unicode.yaml new file mode 100644 index 0000000000..f93fe35db1 --- /dev/null +++ b/cucumber-expressions/java/testdata/regex/unicode.yaml @@ -0,0 +1,2 @@ +expression: "Привет, Мир(ы)!" +expected: ^Привет, Мир(?:ы)?!$ diff --git a/cucumber-expressions/java/testdata/tokens/alternation-phrase.yaml b/cucumber-expressions/java/testdata/tokens/alternation-phrase.yaml new file mode 100644 index 0000000000..48b107f64e --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/alternation-phrase.yaml @@ -0,0 +1,13 @@ +expression: three blind/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "ALTERNATION", "start": 11, "end": 12, "text": "/"}, + {"type": "TEXT", "start": 12, "end": 19, "text": "cripple"}, + {"type": "WHITE_SPACE", "start": 19, "end": 20, "text": " "}, + {"type": "TEXT", "start": 20, "end": 24, "text": "mice"}, + {"type": "END_OF_LINE", "start": 24, "end": 24, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/alternation.yaml b/cucumber-expressions/java/testdata/tokens/alternation.yaml new file mode 100644 index 0000000000..a4920f22e5 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/alternation.yaml @@ -0,0 +1,9 @@ +expression: blind/cripple +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "blind"}, + {"type": "ALTERNATION", "start": 5, "end": 6, "text": "/"}, + {"type": "TEXT", "start": 6, "end": 13, "text": "cripple"}, + {"type": "END_OF_LINE", "start": 13, "end": 13, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/empty-string.yaml b/cucumber-expressions/java/testdata/tokens/empty-string.yaml new file mode 100644 index 0000000000..501f7522f2 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/empty-string.yaml @@ -0,0 +1,6 @@ +expression: "" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "END_OF_LINE", "start": 0, "end": 0, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/escape-non-reserved-character.yaml b/cucumber-expressions/java/testdata/tokens/escape-non-reserved-character.yaml new file mode 100644 index 0000000000..5e206be084 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escape-non-reserved-character.yaml @@ -0,0 +1,8 @@ +expression: \[ +exception: |- + This Cucumber Expression has a problem at column 2: + + \[ + ^ + Only the characters '{', '}', '(', ')', '\', '/' and whitespace can be escaped. + If you did mean to use an '\' you can use '\\' to escape it diff --git a/cucumber-expressions/java/testdata/tokens/escaped-alternation.yaml b/cucumber-expressions/java/testdata/tokens/escaped-alternation.yaml new file mode 100644 index 0000000000..7e21f7ad19 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escaped-alternation.yaml @@ -0,0 +1,9 @@ +expression: blind\ and\ famished\/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 29, "text": "blind and famished/cripple"}, + {"type": "WHITE_SPACE", "start": 29, "end": 30, "text": " "}, + {"type": "TEXT", "start": 30, "end": 34, "text": "mice"}, + {"type": "END_OF_LINE", "start": 34, "end": 34, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml b/cucumber-expressions/java/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml new file mode 100644 index 0000000000..6375ad52a5 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml @@ -0,0 +1,9 @@ +expression: ' \/ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "WHITE_SPACE", "start": 0, "end": 1, "text": " "}, + {"type": "TEXT", "start": 1, "end": 3, "text": "/"}, + {"type": "WHITE_SPACE", "start": 3, "end": 4, "text": " "}, + {"type": "END_OF_LINE", "start": 4, "end": 4, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/escaped-end-of-line.yaml b/cucumber-expressions/java/testdata/tokens/escaped-end-of-line.yaml new file mode 100644 index 0000000000..a1bd00fd98 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escaped-end-of-line.yaml @@ -0,0 +1,8 @@ +expression: \ +exception: |- + This Cucumber Expression has a problem at column 1: + + \ + ^ + The end of line can not be escaped. + You can use '\\' to escape the the '\' diff --git a/cucumber-expressions/java/testdata/tokens/escaped-optional.yaml b/cucumber-expressions/java/testdata/tokens/escaped-optional.yaml new file mode 100644 index 0000000000..2b365b581c --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: \(blind\) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 9, "text": "(blind)"}, + {"type": "END_OF_LINE", "start": 9, "end": 9, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/escaped-parameter.yaml b/cucumber-expressions/java/testdata/tokens/escaped-parameter.yaml new file mode 100644 index 0000000000..2cdac4f35a --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escaped-parameter.yaml @@ -0,0 +1,7 @@ +expression: \{string\} +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 10, "text": "{string}"}, + {"type": "END_OF_LINE", "start": 10, "end": 10, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/escaped-space.yaml b/cucumber-expressions/java/testdata/tokens/escaped-space.yaml new file mode 100644 index 0000000000..912827a941 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/escaped-space.yaml @@ -0,0 +1,7 @@ +expression: '\ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 2, "text": " "}, + {"type": "END_OF_LINE", "start": 2, "end": 2, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/optional-phrase.yaml b/cucumber-expressions/java/testdata/tokens/optional-phrase.yaml new file mode 100644 index 0000000000..2ddc6bb502 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/optional-phrase.yaml @@ -0,0 +1,13 @@ +expression: three (blind) mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_OPTIONAL", "start": 6, "end": 7, "text": "("}, + {"type": "TEXT", "start": 7, "end": 12, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 12, "end": 13, "text": ")"}, + {"type": "WHITE_SPACE", "start": 13, "end": 14, "text": " "}, + {"type": "TEXT", "start": 14, "end": 18, "text": "mice"}, + {"type": "END_OF_LINE", "start": 18, "end": 18, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/optional.yaml b/cucumber-expressions/java/testdata/tokens/optional.yaml new file mode 100644 index 0000000000..35b1474a7c --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/optional.yaml @@ -0,0 +1,9 @@ +expression: (blind) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_OPTIONAL", "start": 0, "end": 1, "text": "("}, + {"type": "TEXT", "start": 1, "end": 6, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 6, "end": 7, "text": ")"}, + {"type": "END_OF_LINE", "start": 7, "end": 7, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/parameter-phrase.yaml b/cucumber-expressions/java/testdata/tokens/parameter-phrase.yaml new file mode 100644 index 0000000000..5e98055ee6 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/parameter-phrase.yaml @@ -0,0 +1,13 @@ +expression: three {string} mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_PARAMETER", "start": 6, "end": 7, "text": "{"}, + {"type": "TEXT", "start": 7, "end": 13, "text": "string"}, + {"type": "END_PARAMETER", "start": 13, "end": 14, "text": "}"}, + {"type": "WHITE_SPACE", "start": 14, "end": 15, "text": " "}, + {"type": "TEXT", "start": 15, "end": 19, "text": "mice"}, + {"type": "END_OF_LINE", "start": 19, "end": 19, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/parameter.yaml b/cucumber-expressions/java/testdata/tokens/parameter.yaml new file mode 100644 index 0000000000..460363c393 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/parameter.yaml @@ -0,0 +1,9 @@ +expression: "{string}" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_PARAMETER", "start": 0, "end": 1, "text": "{"}, + {"type": "TEXT", "start": 1, "end": 7, "text": "string"}, + {"type": "END_PARAMETER", "start": 7, "end": 8, "text": "}"}, + {"type": "END_OF_LINE", "start": 8, "end": 8, "text": ""} + ] diff --git a/cucumber-expressions/java/testdata/tokens/phrase.yaml b/cucumber-expressions/java/testdata/tokens/phrase.yaml new file mode 100644 index 0000000000..e2cfccf7b4 --- /dev/null +++ b/cucumber-expressions/java/testdata/tokens/phrase.yaml @@ -0,0 +1,11 @@ +expression: three blind mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "WHITE_SPACE", "start": 11, "end": 12, "text": " "}, + {"type": "TEXT", "start": 12, "end": 16, "text": "mice"}, + {"type": "END_OF_LINE", "start": 16, "end": 16, "text": ""} + ] diff --git a/cucumber-expressions/javascript/.rsync b/cucumber-expressions/javascript/.rsync index fe4ed7c3d0..998c00c802 100644 --- a/cucumber-expressions/javascript/.rsync +++ b/cucumber-expressions/javascript/.rsync @@ -2,3 +2,4 @@ ../../.templates/github/ .github/ ../../.templates/javascript/ . ../examples.txt examples.txt +../testdata . diff --git a/cucumber-expressions/javascript/examples.txt b/cucumber-expressions/javascript/examples.txt index 35b7bd17a9..3a8288d34d 100644 --- a/cucumber-expressions/javascript/examples.txt +++ b/cucumber-expressions/javascript/examples.txt @@ -2,7 +2,7 @@ I have {int} cuke(s) I have 22 cukes [22] --- -I have {int} cuke(s) and some \[]^$.|?*+ +I have {int} cuke(s) and some \\[]^$.|?*+ I have 1 cuke and some \[]^$.|?*+ [1] --- @@ -37,3 +37,7 @@ a purchase for $33 Some ${float} of cukes at {int}° Celsius Some $3.50 of cukes at 42° Celsius [3.5,42] +--- +I select the {int}st/nd/rd/th Cucumber +I select the 3rd Cucumber +[3] diff --git a/cucumber-expressions/javascript/package.json b/cucumber-expressions/javascript/package.json index 1af381a964..33a34f9048 100644 --- a/cucumber-expressions/javascript/package.json +++ b/cucumber-expressions/javascript/package.json @@ -29,6 +29,7 @@ }, "homepage": "https://github.com/cucumber/cucumber-expressions-javascript#readme", "devDependencies": { + "@types/js-yaml": "^3.12.5", "@types/mocha": "^8.0.3", "@types/node": "^14.14.6", "@typescript-eslint/eslint-plugin": "^4.6.1", diff --git a/cucumber-expressions/javascript/src/Ast.ts b/cucumber-expressions/javascript/src/Ast.ts new file mode 100644 index 0000000000..07d4b515b1 --- /dev/null +++ b/cucumber-expressions/javascript/src/Ast.ts @@ -0,0 +1,157 @@ +const escapeCharacter = '\\' +const alternationCharacter = '/' +const beginParameterCharacter = '{' +const endParameterCharacter = '}' +const beginOptionalCharacter = '(' +const endOptionalCharacter = ')' + +export function symbolOf(token: TokenType): string { + switch (token) { + case TokenType.beginOptional: + return beginOptionalCharacter + case TokenType.endOptional: + return endOptionalCharacter + case TokenType.beginParameter: + return beginParameterCharacter + case TokenType.endParameter: + return endParameterCharacter + case TokenType.alternation: + return alternationCharacter + } + return '' +} + +export function purposeOf(token: TokenType): string { + switch (token) { + case TokenType.beginOptional: + case TokenType.endOptional: + return 'optional text' + case TokenType.beginParameter: + case TokenType.endParameter: + return 'a parameter' + case TokenType.alternation: + return 'alternation' + } + return '' +} + +export interface Located { + readonly start: number + readonly end: number +} + +export class Node { + readonly type: NodeType + readonly nodes?: ReadonlyArray | undefined + readonly token?: string | undefined + readonly start: number + readonly end: number + + constructor( + type: NodeType, + nodes: ReadonlyArray = undefined, + token: string = undefined, + start: number, + end: number + ) { + if (nodes === undefined && token === undefined) { + throw new Error('Either nodes or token must be defined') + } + if (nodes === null || token === null) { + throw new Error('Either nodes or token may not be null') + } + this.type = type + this.nodes = nodes + this.token = token + this.start = start + this.end = end + } + + text(): string { + if (this.nodes) { + return this.nodes.map((value) => value.text()).join('') + } + return this.token + } +} + +export enum NodeType { + text = 'TEXT_NODE', + optional = 'OPTIONAL_NODE', + alternation = 'ALTERNATION_NODE', + alternative = 'ALTERNATIVE_NODE', + parameter = 'PARAMETER_NODE', + expression = 'EXPRESSION_NODE', +} + +export class Token { + readonly type: TokenType + readonly text: string + readonly start: number + readonly end: number + + constructor(type: TokenType, text: string, start: number, end: number) { + this.type = type + this.text = text + this.start = start + this.end = end + } + + static isEscapeCharacter(codePoint: string): boolean { + return codePoint == escapeCharacter + } + + static canEscape(codePoint: string): boolean { + if (codePoint == ' ') { + // TODO: Unicode whitespace? + return true + } + switch (codePoint) { + case escapeCharacter: + return true + case alternationCharacter: + return true + case beginParameterCharacter: + return true + case endParameterCharacter: + return true + case beginOptionalCharacter: + return true + case endOptionalCharacter: + return true + } + return false + } + + static typeOf(codePoint: string): TokenType { + if (codePoint == ' ') { + // TODO: Unicode whitespace? + return TokenType.whiteSpace + } + switch (codePoint) { + case alternationCharacter: + return TokenType.alternation + case beginParameterCharacter: + return TokenType.beginParameter + case endParameterCharacter: + return TokenType.endParameter + case beginOptionalCharacter: + return TokenType.beginOptional + case endOptionalCharacter: + return TokenType.endOptional + } + return TokenType.text + } +} + +export enum TokenType { + startOfLine = 'START_OF_LINE', + endOfLine = 'END_OF_LINE', + whiteSpace = 'WHITE_SPACE', + beginOptional = 'BEGIN_OPTIONAL', + endOptional = 'END_OPTIONAL', + beginParameter = 'BEGIN_PARAMETER', + endParameter = 'END_PARAMETER', + alternation = 'ALTERNATION', + text = 'TEXT', +} diff --git a/cucumber-expressions/javascript/src/CucumberExpression.ts b/cucumber-expressions/javascript/src/CucumberExpression.ts index 19d6ed0a3e..d00915f4c7 100644 --- a/cucumber-expressions/javascript/src/CucumberExpression.ts +++ b/cucumber-expressions/javascript/src/CucumberExpression.ts @@ -2,24 +2,20 @@ import ParameterTypeRegistry from './ParameterTypeRegistry' import ParameterType from './ParameterType' import TreeRegexp from './TreeRegexp' 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 - -// 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: ' +const ESCAPE_PATTERN = () => /([\\^[({$.|?*+})\]])/g export default class CucumberExpression implements Expression { private readonly parameterTypes: Array> = [] @@ -33,69 +29,129 @@ export default class CucumberExpression implements Expression { private readonly expression: string, private readonly parameterTypeRegistry: ParameterTypeRegistry ) { - 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]) + } } public match(text: string): ReadonlyArray> { @@ -109,22 +165,4 @@ export default class CucumberExpression implements Expression { get source(): string { return this.expression } - - private checkNoParameterType(s: string, message: string) { - if (s.match(PARAMETER_REGEXP())) { - throw new CucumberExpressionError(message + this.source) - } - } -} - -function buildCaptureRegexp(regexps: ReadonlyArray) { - if (regexps.length === 1) { - return `(${regexps[0]})` - } - - const captureGroups = regexps.map((group) => { - return `(?:${group})` - }) - - return `(${captureGroups.join('|')})` } diff --git a/cucumber-expressions/javascript/src/CucumberExpressionParser.ts b/cucumber-expressions/javascript/src/CucumberExpressionParser.ts new file mode 100644 index 0000000000..2bd41e3927 --- /dev/null +++ b/cucumber-expressions/javascript/src/CucumberExpressionParser.ts @@ -0,0 +1,380 @@ +import { Node, NodeType, Token, TokenType } from './Ast' +import CucumberExpressionTokenizer from './CucumberExpressionTokenizer' +import { + createAlternationNotAllowedInOptional, + createInvalidParameterTypeNameInNode, + createMissingEndToken, +} from './Errors' + +/* + * text := whitespace | ')' | '}' | . + */ +function parseText( + expression: string, + tokens: ReadonlyArray, + current: number +) { + const token = tokens[current] + switch (token.type) { + case TokenType.whiteSpace: + case TokenType.text: + case TokenType.endParameter: + case TokenType.endOptional: + return { + consumed: 1, + ast: [ + new Node( + NodeType.text, + undefined, + token.text, + token.start, + token.end + ), + ], + } + case TokenType.alternation: + throw createAlternationNotAllowedInOptional(expression, token) + case TokenType.startOfLine: + case TokenType.endOfLine: + case TokenType.beginOptional: + case TokenType.beginParameter: + default: + // If configured correctly this will never happen + return { consumed: 0 } + } +} + +/* + * parameter := '{' + name* + '}' + */ +function parseName( + expression: string, + tokens: ReadonlyArray, + current: number +) { + const token = tokens[current] + switch (token.type) { + case TokenType.whiteSpace: + case TokenType.text: + return { + consumed: 1, + ast: [ + new Node( + NodeType.text, + undefined, + token.text, + token.start, + token.end + ), + ], + } + case TokenType.beginOptional: + case TokenType.endOptional: + case TokenType.beginParameter: + case TokenType.endParameter: + case TokenType.alternation: + throw createInvalidParameterTypeNameInNode(token, expression) + case TokenType.startOfLine: + case TokenType.endOfLine: + default: + // If configured correctly this will never happen + return { consumed: 0 } + } +} + +/* + * parameter := '{' + text* + '}' + */ +const parseParameter = parseBetween( + NodeType.parameter, + TokenType.beginParameter, + TokenType.endParameter, + [parseName] +) + +/* + * optional := '(' + option* + ')' + * option := optional | parameter | text + */ +const optionalSubParsers: Array = [] +const parseOptional = parseBetween( + NodeType.optional, + TokenType.beginOptional, + TokenType.endOptional, + optionalSubParsers +) +optionalSubParsers.push(parseOptional, parseParameter, parseText) + +/* + * alternation := alternative* + ( '/' + alternative* )+ + */ +function parseAlternativeSeparator( + expression: string, + tokens: ReadonlyArray, + current: number +) { + if (!lookingAt(tokens, current, TokenType.alternation)) { + return { consumed: 0 } + } + const token = tokens[current] + return { + consumed: 1, + ast: [ + new Node( + NodeType.alternative, + undefined, + token.text, + token.start, + token.end + ), + ], + } +} + +const alternativeParsers: ReadonlyArray = [ + parseAlternativeSeparator, + parseOptional, + parseParameter, + parseText, +] + +/* + * alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary) + * left-boundary := whitespace | } | ^ + * right-boundary := whitespace | { | $ + * alternative: = optional | parameter | text + */ +const parseAlternation: Parser = (expression, tokens, current) => { + const previous = current - 1 + if ( + !lookingAtAny(tokens, previous, [ + TokenType.startOfLine, + TokenType.whiteSpace, + TokenType.endParameter, + ]) + ) { + return { consumed: 0 } + } + + const result = parseTokensUntil( + expression, + alternativeParsers, + tokens, + current, + [TokenType.whiteSpace, TokenType.endOfLine, TokenType.beginParameter] + ) + const subCurrent = current + result.consumed + if (!result.ast.some((astNode) => astNode.type == NodeType.alternative)) { + return { consumed: 0 } + } + + const start = tokens[current].start + const end = tokens[subCurrent].start + // Does not consume right hand boundary token + return { + consumed: result.consumed, + ast: [ + new Node( + NodeType.alternation, + splitAlternatives(start, end, result.ast), + undefined, + start, + end + ), + ], + } +} + +/* + * cucumber-expression := ( alternation | optional | parameter | text )* + */ +const parseCucumberExpression = parseBetween( + NodeType.expression, + TokenType.startOfLine, + TokenType.endOfLine, + [parseAlternation, parseOptional, parseParameter, parseText] +) + +export default class CucumberExpressionParser { + parse(expression: string): Node { + const tokenizer = new CucumberExpressionTokenizer() + const tokens = tokenizer.tokenize(expression) + const result = parseCucumberExpression(expression, tokens, 0) + return result.ast[0] + } +} + +interface Parser { + (expression: string, tokens: ReadonlyArray, current: number): Result +} + +interface Result { + readonly consumed: number + readonly ast?: ReadonlyArray +} + +function parseBetween( + type: NodeType, + beginToken: TokenType, + endToken: TokenType, + parsers: Array +): Parser { + return (expression, tokens, current) => { + if (!lookingAt(tokens, current, beginToken)) { + return { consumed: 0 } + } + let subCurrent = current + 1 + const result = parseTokensUntil(expression, parsers, tokens, subCurrent, [ + endToken, + TokenType.endOfLine, + ]) + subCurrent += result.consumed + + // endToken not found + if (!lookingAt(tokens, subCurrent, endToken)) { + throw createMissingEndToken( + expression, + beginToken, + endToken, + tokens[current] + ) + } + // consumes endToken + const start = tokens[current].start + const end = tokens[subCurrent].end + const consumed = subCurrent + 1 - current + const ast = [new Node(type, result.ast, undefined, start, end)] + return { consumed, ast } + } +} + +function parseToken( + expression: string, + parsers: ReadonlyArray, + tokens: ReadonlyArray, + startAt: number +): Result { + for (let i = 0; i < parsers.length; i++) { + const parse = parsers[i] + const result = parse(expression, tokens, startAt) + if (result.consumed != 0) { + return result + } + } + // If configured correctly this will never happen + throw new Error('No eligible parsers for ' + tokens) +} + +function parseTokensUntil( + expression: string, + parsers: ReadonlyArray, + tokens: ReadonlyArray, + startAt: number, + endTokens: ReadonlyArray +): Result { + let current = startAt + const size = tokens.length + const ast: Node[] = [] + while (current < size) { + if (lookingAtAny(tokens, current, endTokens)) { + break + } + const result = parseToken(expression, parsers, tokens, current) + if (result.consumed == 0) { + // If configured correctly this will never happen + // Keep to avoid infinite loops + throw new Error('No eligible parsers for ' + tokens) + } + current += result.consumed + ast.push(...result.ast) + } + return { consumed: current - startAt, ast } +} + +function lookingAtAny( + tokens: ReadonlyArray, + at: number, + tokenTypes: ReadonlyArray +): boolean { + return tokenTypes.some((tokenType) => lookingAt(tokens, at, tokenType)) +} + +function lookingAt( + tokens: ReadonlyArray, + at: number, + token: TokenType +): boolean { + if (at < 0) { + // If configured correctly this will never happen + // Keep for completeness + return token == TokenType.startOfLine + } + if (at >= tokens.length) { + return token == TokenType.endOfLine + } + return tokens[at].type == token +} + +function splitAlternatives( + start: number, + end: number, + alternation: ReadonlyArray +): ReadonlyArray { + const separators: Node[] = [] + const alternatives: Node[][] = [] + let alternative: Node[] = [] + alternation.forEach((n) => { + if (NodeType.alternative == n.type) { + separators.push(n) + alternatives.push(alternative) + alternative = [] + } else { + alternative.push(n) + } + }) + alternatives.push(alternative) + return createAlternativeNodes(start, end, separators, alternatives) +} + +function createAlternativeNodes( + start: number, + end: number, + separators: ReadonlyArray, + alternatives: ReadonlyArray> +): ReadonlyArray { + const nodes: Node[] = [] + + for (let i = 0; i < alternatives.length; i++) { + const n = alternatives[i] + if (i == 0) { + const rightSeparator = separators[i] + nodes.push( + new Node( + NodeType.alternative, + n, + undefined, + start, + rightSeparator.start + ) + ) + } else if (i == alternatives.length - 1) { + const leftSeparator = separators[i - 1] + nodes.push( + new Node(NodeType.alternative, n, undefined, leftSeparator.end, end) + ) + } else { + const leftSeparator = separators[i - 1] + const rightSeparator = separators[i] + nodes.push( + new Node( + NodeType.alternative, + n, + undefined, + leftSeparator.end, + rightSeparator.start + ) + ) + } + } + return nodes +} diff --git a/cucumber-expressions/javascript/src/CucumberExpressionTokenizer.ts b/cucumber-expressions/javascript/src/CucumberExpressionTokenizer.ts new file mode 100644 index 0000000000..8853f39043 --- /dev/null +++ b/cucumber-expressions/javascript/src/CucumberExpressionTokenizer.ts @@ -0,0 +1,97 @@ +import { Token, TokenType } from './Ast' +import { createCantEscaped, createTheEndOfLIneCanNotBeEscaped } from './Errors' + +export default class CucumberExpressionTokenizer { + tokenize(expression: string): ReadonlyArray { + const codePoints = Array.from(expression) + const tokens: Array = [] + let buffer: Array = [] + let previousTokenType = TokenType.startOfLine + let treatAsText = false + let escaped = 0 + let bufferStartIndex = 0 + + function convertBufferToToken(tokenType: TokenType): Token { + let escapeTokens = 0 + if (tokenType == TokenType.text) { + escapeTokens = escaped + escaped = 0 + } + + const consumedIndex = bufferStartIndex + buffer.length + escapeTokens + const t = new Token( + tokenType, + buffer.join(''), + bufferStartIndex, + consumedIndex + ) + buffer = [] + bufferStartIndex = consumedIndex + return t + } + + function tokenTypeOf(codePoint: string, treatAsText: boolean): TokenType { + if (!treatAsText) { + return Token.typeOf(codePoint) + } + if (Token.canEscape(codePoint)) { + return TokenType.text + } + throw createCantEscaped( + expression, + bufferStartIndex + buffer.length + escaped + ) + } + + function shouldCreateNewToken( + previousTokenType: TokenType, + currentTokenType: TokenType + ) { + if (currentTokenType != previousTokenType) { + return true + } + return ( + currentTokenType != TokenType.whiteSpace && + currentTokenType != TokenType.text + ) + } + + if (codePoints.length == 0) { + tokens.push(new Token(TokenType.startOfLine, '', 0, 0)) + } + + codePoints.forEach((codePoint) => { + if (!treatAsText && Token.isEscapeCharacter(codePoint)) { + escaped++ + treatAsText = true + return + } + const currentTokenType = tokenTypeOf(codePoint, treatAsText) + treatAsText = false + + if (shouldCreateNewToken(previousTokenType, currentTokenType)) { + const token = convertBufferToToken(previousTokenType) + previousTokenType = currentTokenType + buffer.push(codePoint) + tokens.push(token) + } else { + previousTokenType = currentTokenType + buffer.push(codePoint) + } + }) + + if (buffer.length > 0) { + const token = convertBufferToToken(previousTokenType) + tokens.push(token) + } + + if (treatAsText) { + throw createTheEndOfLIneCanNotBeEscaped(expression) + } + + tokens.push( + new Token(TokenType.endOfLine, '', codePoints.length, codePoints.length) + ) + return tokens + } +} diff --git a/cucumber-expressions/javascript/src/Errors.ts b/cucumber-expressions/javascript/src/Errors.ts index 2dedafb57f..94effbf6e8 100644 --- a/cucumber-expressions/javascript/src/Errors.ts +++ b/cucumber-expressions/javascript/src/Errors.ts @@ -1,9 +1,200 @@ 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 = [] + 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( keyName: string, keyValue: string, @@ -49,14 +240,28 @@ I couldn't decide which one to use. You have two options: } } -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}'` + ) + ) } diff --git a/cucumber-expressions/javascript/src/ParameterType.ts b/cucumber-expressions/javascript/src/ParameterType.ts index 21df7fd24c..1ceff73942 100644 --- a/cucumber-expressions/javascript/src/ParameterType.ts +++ b/cucumber-expressions/javascript/src/ParameterType.ts @@ -1,4 +1,7 @@ -import { CucumberExpressionError } from './Errors' +import { + createInvalidParameterTypeName, + CucumberExpressionError, +} from './Errors' const ILLEGAL_PARAMETER_NAME_PATTERN = /([[\]()$.|?*+])/ const UNESCAPE_PATTERN = () => /(\\([[$.|?*+\]]))/g @@ -17,15 +20,16 @@ export default class ParameterType { } 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 /** diff --git a/cucumber-expressions/javascript/test/CucumberExpressionParserTest.ts b/cucumber-expressions/javascript/test/CucumberExpressionParserTest.ts new file mode 100644 index 0000000000..bfe560fe79 --- /dev/null +++ b/cucumber-expressions/javascript/test/CucumberExpressionParserTest.ts @@ -0,0 +1,33 @@ +import fs from 'fs' +// eslint-disable-next-line node/no-extraneous-import +import yaml from 'js-yaml' // why? +import assert from 'assert' +import { CucumberExpressionError } from '../src/Errors' +import CucumberExpressionParser from '../src/CucumberExpressionParser' + +interface Expectation { + expression: string + expected?: string + exception?: string +} + +describe('Cucumber expression parser', () => { + fs.readdirSync('testdata/ast').forEach((testcase) => { + const testCaseData = fs.readFileSync(`testdata/ast/${testcase}`, 'utf-8') + const expectation = yaml.safeLoad(testCaseData) as Expectation + it(`${testcase}`, () => { + const parser = new CucumberExpressionParser() + if (expectation.exception == undefined) { + const node = parser.parse(expectation.expression) + assert.deepStrictEqual( + JSON.parse(JSON.stringify(node)), // Removes type information. + JSON.parse(expectation.expected) + ) + } else { + assert.throws(() => { + parser.parse(expectation.expression) + }, new CucumberExpressionError(expectation.exception)) + } + }) + }) +}) diff --git a/cucumber-expressions/javascript/test/CucumberExpressionRegExpTest.ts b/cucumber-expressions/javascript/test/CucumberExpressionRegExpTest.ts deleted file mode 100644 index 3d001f304a..0000000000 --- a/cucumber-expressions/javascript/test/CucumberExpressionRegExpTest.ts +++ /dev/null @@ -1,51 +0,0 @@ -import assert from 'assert' -import CucumberExpression from '../src/CucumberExpression' -import ParameterTypeRegistry from '../src/ParameterTypeRegistry' - -describe('CucumberExpression', () => { - describe('RegExp translation', () => { - it('translates no arguments', () => { - assertRegexp( - 'I have 10 cukes in my belly now', - /^I have 10 cukes in my belly now$/ - ) - }) - - it('translates alternation', () => { - assertRegexp( - 'I had/have a great/nice/charming friend', - /^I (?:had|have) a (?:great|nice|charming) friend$/ - ) - }) - - it('translates alternation with non-alpha', () => { - assertRegexp('I said Alpha1/Beta1', /^I said (?:Alpha1|Beta1)$/) - }) - - it('translates parameters', () => { - assertRegexp( - "I have {float} cukes at {int} o'clock", - /^I have ((?=.*\d.*)[-+]?\d*(?:\.(?=\d.*))?\d*(?:\d+[E][+-]?\d+)?) cukes at ((?:-?\d+)|(?:\d+)) o'clock$/ - ) - }) - - it('translates parenthesis to non-capturing optional capture group', () => { - assertRegexp( - 'I have many big(ish) cukes', - /^I have many big(?:ish)? cukes$/ - ) - }) - - it('translates parenthesis with alpha unicode', () => { - assertRegexp('Привет, Мир(ы)!', /^Привет, Мир(?:ы)?!$/) - }) - }) -}) - -const assertRegexp = (expression: string, expectedRegexp: RegExp) => { - const cucumberExpression = new CucumberExpression( - expression, - new ParameterTypeRegistry() - ) - assert.deepStrictEqual(cucumberExpression.regexp, expectedRegexp) -} diff --git a/cucumber-expressions/javascript/test/CucumberExpressionTest.ts b/cucumber-expressions/javascript/test/CucumberExpressionTest.ts index 0e2c233e77..3afe31887f 100644 --- a/cucumber-expressions/javascript/test/CucumberExpressionTest.ts +++ b/cucumber-expressions/javascript/test/CucumberExpressionTest.ts @@ -2,8 +2,66 @@ import assert from 'assert' import CucumberExpression from '../src/CucumberExpression' import ParameterTypeRegistry from '../src/ParameterTypeRegistry' 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', () => { const parameterTypeRegistry = new ParameterTypeRegistry() @@ -15,130 +73,6 @@ describe('CucumberExpression', () => { /// [capture-match-arguments] }) - 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', () => { assert.deepStrictEqual(match('{float}', ''), null) assert.deepStrictEqual(match('{float}', '.'), null) @@ -178,73 +112,6 @@ describe('CucumberExpression', () => { assert.deepEqual(match('{float}', '0'), [0]) }) - 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', () => { const expr = 'I have {int} cuke(s)' assert.strictEqual( @@ -314,44 +181,6 @@ describe('CucumberExpression', () => { const args = expression.match(`I have a bolt`) assert.strictEqual(args[0].getValue(world), 'widget:bolt') }) - - 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) - }) - }) }) const match = (expression: string, text: string) => { diff --git a/cucumber-expressions/javascript/test/CucumberExpressionTokenizerTest.ts b/cucumber-expressions/javascript/test/CucumberExpressionTokenizerTest.ts new file mode 100644 index 0000000000..2d55174495 --- /dev/null +++ b/cucumber-expressions/javascript/test/CucumberExpressionTokenizerTest.ts @@ -0,0 +1,33 @@ +import fs from 'fs' +// eslint-disable-next-line node/no-extraneous-import +import yaml from 'js-yaml' // why? +import CucumberExpressionTokenizer from '../src/CucumberExpressionTokenizer' +import assert from 'assert' +import { CucumberExpressionError } from '../src/Errors' + +interface Expectation { + expression: string + expected?: string + exception?: string +} + +describe('Cucumber expression tokenizer', () => { + fs.readdirSync('testdata/tokens').forEach((testcase) => { + const testCaseData = fs.readFileSync(`testdata/tokens/${testcase}`, 'utf-8') + const expectation = yaml.safeLoad(testCaseData) as Expectation + it(`${testcase}`, () => { + const tokenizer = new CucumberExpressionTokenizer() + if (expectation.exception == undefined) { + const tokens = tokenizer.tokenize(expectation.expression) + assert.deepStrictEqual( + JSON.parse(JSON.stringify(tokens)), // Removes type information. + JSON.parse(expectation.expected) + ) + } else { + assert.throws(() => { + tokenizer.tokenize(expectation.expression) + }, new CucumberExpressionError(expectation.exception)) + } + }) + }) +}) diff --git a/cucumber-expressions/javascript/test/CustomParameterTypeTest.ts b/cucumber-expressions/javascript/test/CustomParameterTypeTest.ts index 0eca6398c5..e91544fe6e 100644 --- a/cucumber-expressions/javascript/test/CustomParameterTypeTest.ts +++ b/cucumber-expressions/javascript/test/CustomParameterTypeTest.ts @@ -42,7 +42,10 @@ describe('Custom parameter type', () => { assert.throws( () => 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 '/'", + } ) }) diff --git a/cucumber-expressions/javascript/test/ExpressionExamplesTest.ts b/cucumber-expressions/javascript/test/ExpressionExamplesTest.ts index e9f09ce649..65d221e944 100644 --- a/cucumber-expressions/javascript/test/ExpressionExamplesTest.ts +++ b/cucumber-expressions/javascript/test/ExpressionExamplesTest.ts @@ -6,7 +6,7 @@ import ParameterTypeRegistry from '../src/ParameterTypeRegistry' describe('examples.txt', () => { const match = (expressionText: string, text: string) => { - const m = /\/(.*)\//.exec(expressionText) + const m = /^\/(.*)\/$/.exec(expressionText) const expression = m ? new RegularExpression(new RegExp(m[1]), new ParameterTypeRegistry()) : new CucumberExpression(expressionText, new ParameterTypeRegistry()) diff --git a/cucumber-expressions/javascript/test/ExpressionFactoryTest.ts b/cucumber-expressions/javascript/test/ExpressionFactoryTest.ts index 6f99ea8120..9520e4d1d2 100644 --- a/cucumber-expressions/javascript/test/ExpressionFactoryTest.ts +++ b/cucumber-expressions/javascript/test/ExpressionFactoryTest.ts @@ -23,10 +23,4 @@ describe('ExpressionFactory', () => { CucumberExpression ) }) - - it('creates an UndefinedParameterTypeExpression', () => { - assert.throws(() => expressionFactory.createExpression('{x}'), { - message: 'Undefined parameter type {x}', - }) - }) }) diff --git a/cucumber-expressions/javascript/testdata/ast/alternation-followed-by-optional.yaml b/cucumber-expressions/javascript/testdata/ast/alternation-followed-by-optional.yaml new file mode 100644 index 0000000000..0bfb53a534 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation-followed-by-optional.yaml @@ -0,0 +1,17 @@ +expression: three blind\ rat/cat(s) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 16, "token": "blind rat"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 17, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 17, "end": 20, "token": "cat"}, + {"type": "OPTIONAL_NODE", "start": 20, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": "s"} + ]} + ]} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/alternation-phrase.yaml b/cucumber-expressions/javascript/testdata/ast/alternation-phrase.yaml new file mode 100644 index 0000000000..9a243822d5 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation-phrase.yaml @@ -0,0 +1,16 @@ +expression: three hungry/blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 12, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 12, "token": "hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 13, "end": 18, "token": "blind"} + ]} + ]}, + {"type": "TEXT_NODE", "start": 18, "end": 19, "token": " "}, + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "mice"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/alternation-with-parameter.yaml b/cucumber-expressions/javascript/testdata/ast/alternation-with-parameter.yaml new file mode 100644 index 0000000000..c5daf32bd7 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation-with-parameter.yaml @@ -0,0 +1,27 @@ +expression: I select the {int}st/nd/rd/th +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "I"}, + {"type": "TEXT_NODE", "start": 1, "end": 2, "token": " "}, + {"type": "TEXT_NODE", "start": 2, "end": 8, "token": "select"}, + {"type": "TEXT_NODE", "start": 8, "end": 9, "token": " "}, + {"type": "TEXT_NODE", "start": 9, "end": 12, "token": "the"}, + {"type": "TEXT_NODE", "start": 12, "end": 13, "token": " "}, + {"type": "PARAMETER_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 14, "end": 17, "token": "int"} + ]}, + {"type": "ALTERNATION_NODE", "start": 18, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 18, "end": 20, "nodes": [ + {"type": "TEXT_NODE", "start": 18, "end": 20, "token": "st"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 21, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 23, "token": "nd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 24, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 24, "end": 26, "token": "rd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 27, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 27, "end": 29, "token": "th"} + ]} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/alternation-with-unused-end-optional.yaml b/cucumber-expressions/javascript/testdata/ast/alternation-with-unused-end-optional.yaml new file mode 100644 index 0000000000..842838b75f --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation-with-unused-end-optional.yaml @@ -0,0 +1,15 @@ +expression: three )blind\ mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 7, "token": ")"}, + {"type": "TEXT_NODE", "start": 7, "end": 18, "token": "blind mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 19, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/alternation-with-unused-start-optional.yaml b/cucumber-expressions/javascript/testdata/ast/alternation-with-unused-start-optional.yaml new file mode 100644 index 0000000000..e2f0584556 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation-with-unused-start-optional.yaml @@ -0,0 +1,8 @@ +expression: three blind\ mice/rats( +exception: |- + This Cucumber Expression has a problem at column 23: + + three blind\ mice/rats( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/javascript/testdata/ast/alternation-with-white-space.yaml b/cucumber-expressions/javascript/testdata/ast/alternation-with-white-space.yaml new file mode 100644 index 0000000000..eedd57dd21 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation-with-white-space.yaml @@ -0,0 +1,12 @@ +expression: '\ three\ hungry/blind\ mice\ ' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 15, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 15, "token": " three hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 16, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 16, "end": 29, "token": "blind mice "} + ]} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/alternation.yaml b/cucumber-expressions/javascript/testdata/ast/alternation.yaml new file mode 100644 index 0000000000..88df8325fe --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/alternation.yaml @@ -0,0 +1,12 @@ +expression: mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 4, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 4, "token": "mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 5, "end": 9, "nodes": [ + {"type": "TEXT_NODE", "start": 5, "end": 9, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/anonymous-parameter.yaml b/cucumber-expressions/javascript/testdata/ast/anonymous-parameter.yaml new file mode 100644 index 0000000000..2c4d339333 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/anonymous-parameter.yaml @@ -0,0 +1,5 @@ +expression: "{}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 2, "nodes": []} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/closing-brace.yaml b/cucumber-expressions/javascript/testdata/ast/closing-brace.yaml new file mode 100644 index 0000000000..1bafd9c6a8 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/closing-brace.yaml @@ -0,0 +1,5 @@ +expression: "}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "}"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/closing-parenthesis.yaml b/cucumber-expressions/javascript/testdata/ast/closing-parenthesis.yaml new file mode 100644 index 0000000000..23daf7bcd3 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/closing-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: ) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": ")"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/empty-alternation.yaml b/cucumber-expressions/javascript/testdata/ast/empty-alternation.yaml new file mode 100644 index 0000000000..6d810fc8f3 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/empty-alternation.yaml @@ -0,0 +1,8 @@ +expression: / +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/empty-alternations.yaml b/cucumber-expressions/javascript/testdata/ast/empty-alternations.yaml new file mode 100644 index 0000000000..f8d4dd4cf8 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/empty-alternations.yaml @@ -0,0 +1,9 @@ +expression: '//' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 2, "end": 2, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/empty-string.yaml b/cucumber-expressions/javascript/testdata/ast/empty-string.yaml new file mode 100644 index 0000000000..4d33c2dc76 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/empty-string.yaml @@ -0,0 +1,3 @@ +expression: "" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 0, "nodes": []} diff --git a/cucumber-expressions/javascript/testdata/ast/escaped-alternation.yaml b/cucumber-expressions/javascript/testdata/ast/escaped-alternation.yaml new file mode 100644 index 0000000000..3ed9c37674 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/escaped-alternation.yaml @@ -0,0 +1,5 @@ +expression: 'mice\/rats' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 10, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 10, "token": "mice/rats"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/escaped-backslash.yaml b/cucumber-expressions/javascript/testdata/ast/escaped-backslash.yaml new file mode 100644 index 0000000000..da2d008e1e --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/escaped-backslash.yaml @@ -0,0 +1,5 @@ +expression: '\\' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "\\"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/escaped-opening-parenthesis.yaml b/cucumber-expressions/javascript/testdata/ast/escaped-opening-parenthesis.yaml new file mode 100644 index 0000000000..afafc59eb8 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/escaped-opening-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: '\(' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "("} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/escaped-optional-followed-by-optional.yaml b/cucumber-expressions/javascript/testdata/ast/escaped-optional-followed-by-optional.yaml new file mode 100644 index 0000000000..1e4746291b --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/escaped-optional-followed-by-optional.yaml @@ -0,0 +1,15 @@ +expression: three \((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 8, "token": "("}, + {"type": "OPTIONAL_NODE", "start": 8, "end": 14, "nodes": [ + {"type": "TEXT_NODE", "start": 9, "end": 13, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 20, "token": "blind"}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": ")"}, + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": " "}, + {"type": "TEXT_NODE", "start": 22, "end": 26, "token": "mice"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/escaped-optional-phrase.yaml b/cucumber-expressions/javascript/testdata/ast/escaped-optional-phrase.yaml new file mode 100644 index 0000000000..832249e2a7 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/escaped-optional-phrase.yaml @@ -0,0 +1,10 @@ +expression: three \(blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 19, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 13, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": ")"}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 19, "token": "mice"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/escaped-optional.yaml b/cucumber-expressions/javascript/testdata/ast/escaped-optional.yaml new file mode 100644 index 0000000000..4c2b457d6f --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: '\(blind)' + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 7, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 7, "end": 8, "token": ")"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/opening-brace.yaml b/cucumber-expressions/javascript/testdata/ast/opening-brace.yaml new file mode 100644 index 0000000000..916a674a36 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/opening-brace.yaml @@ -0,0 +1,8 @@ +expression: '{' +exception: |- + This Cucumber Expression has a problem at column 1: + + { + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/javascript/testdata/ast/opening-parenthesis.yaml b/cucumber-expressions/javascript/testdata/ast/opening-parenthesis.yaml new file mode 100644 index 0000000000..929d6ae304 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/opening-parenthesis.yaml @@ -0,0 +1,8 @@ +expression: ( +exception: |- + This Cucumber Expression has a problem at column 1: + + ( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/javascript/testdata/ast/optional-containing-nested-optional.yaml b/cucumber-expressions/javascript/testdata/ast/optional-containing-nested-optional.yaml new file mode 100644 index 0000000000..0fdd55d46b --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/optional-containing-nested-optional.yaml @@ -0,0 +1,15 @@ +expression: three ((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 25, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 20, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 7, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 8, "end": 12, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 19, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": " "}, + {"type": "TEXT_NODE", "start": 21, "end": 25, "token": "mice"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/optional-phrase.yaml b/cucumber-expressions/javascript/testdata/ast/optional-phrase.yaml new file mode 100644 index 0000000000..0ef4fb3f95 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/optional-phrase.yaml @@ -0,0 +1,12 @@ +expression: three (blind) mice + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 7, "end": 12, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 18, "token": "mice"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/optional.yaml b/cucumber-expressions/javascript/testdata/ast/optional.yaml new file mode 100644 index 0000000000..6ce2b632e7 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/optional.yaml @@ -0,0 +1,7 @@ +expression: (blind) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 6, "token": "blind"} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/parameter.yaml b/cucumber-expressions/javascript/testdata/ast/parameter.yaml new file mode 100644 index 0000000000..92ac8c147e --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/parameter.yaml @@ -0,0 +1,7 @@ +expression: "{string}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 7, "token": "string"} + ]} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/phrase.yaml b/cucumber-expressions/javascript/testdata/ast/phrase.yaml new file mode 100644 index 0000000000..ba340d0122 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/phrase.yaml @@ -0,0 +1,9 @@ +expression: three blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 11, "token": "blind"}, + {"type": "TEXT_NODE", "start": 11, "end": 12, "token": " "}, + {"type": "TEXT_NODE", "start": 12, "end": 16, "token": "mice"} + ]} diff --git a/cucumber-expressions/javascript/testdata/ast/unfinished-parameter.yaml b/cucumber-expressions/javascript/testdata/ast/unfinished-parameter.yaml new file mode 100644 index 0000000000..d02f9b4ccf --- /dev/null +++ b/cucumber-expressions/javascript/testdata/ast/unfinished-parameter.yaml @@ -0,0 +1,8 @@ +expression: "{string" +exception: |- + This Cucumber Expression has a problem at column 1: + + {string + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/javascript/testdata/expression/allows-escaped-optional-parameter-types.yaml b/cucumber-expressions/javascript/testdata/expression/allows-escaped-optional-parameter-types.yaml new file mode 100644 index 0000000000..a00b45acef --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/allows-escaped-optional-parameter-types.yaml @@ -0,0 +1,4 @@ +expression: \({int}) +text: (3) +expected: |- + [3] diff --git a/cucumber-expressions/javascript/testdata/expression/allows-parameter-type-in-alternation-1.yaml b/cucumber-expressions/javascript/testdata/expression/allows-parameter-type-in-alternation-1.yaml new file mode 100644 index 0000000000..bb1a6f21b1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/allows-parameter-type-in-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: i18n +expected: |- + [18] diff --git a/cucumber-expressions/javascript/testdata/expression/allows-parameter-type-in-alternation-2.yaml b/cucumber-expressions/javascript/testdata/expression/allows-parameter-type-in-alternation-2.yaml new file mode 100644 index 0000000000..cdddce7d84 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/allows-parameter-type-in-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: a11y +expected: |- + [11] diff --git a/cucumber-expressions/javascript/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml b/cucumber-expressions/javascript/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml new file mode 100644 index 0000000000..9e2ecdfbe1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml @@ -0,0 +1,5 @@ +expression: |- + {int}st/nd/rd/th +text: 3rd +expected: |- + [3] diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-in-optional.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-in-optional.yaml new file mode 100644 index 0000000000..b507e27220 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-in-optional.yaml @@ -0,0 +1,9 @@ +expression: three( brown/black) mice +text: three brown/black mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three( brown/black) mice + ^ + An alternation can not be used inside an optional. + You can use '\/' to escape the the '/' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml new file mode 100644 index 0000000000..b32540a4a9 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml @@ -0,0 +1,10 @@ +expression: |- + {int}/x +text: '3' +exception: |- + This Cucumber Expression has a problem at column 6: + + {int}/x + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml new file mode 100644 index 0000000000..a0aab0e5a9 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml @@ -0,0 +1,9 @@ +expression: three (brown)/black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (brown)/black mice + ^-----^ + An alternative may not exclusively contain optionals. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml new file mode 100644 index 0000000000..50250f00aa --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml @@ -0,0 +1,9 @@ +expression: x/{int} +text: '3' +exception: |- + This Cucumber Expression has a problem at column 3: + + x/{int} + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml new file mode 100644 index 0000000000..b724cfa77f --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml @@ -0,0 +1,9 @@ +expression: three brown//black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three brown//black mice + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-empty-optional.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-empty-optional.yaml new file mode 100644 index 0000000000..00e341af0b --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-empty-optional.yaml @@ -0,0 +1,9 @@ +expression: three () mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three () mice + ^^ + An optional must contain some text. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-nested-optional.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-nested-optional.yaml new file mode 100644 index 0000000000..017c3be25d --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-nested-optional.yaml @@ -0,0 +1,8 @@ +expression: "(a(b))" +exception: |- + This Cucumber Expression has a problem at column 3: + + (a(b)) + ^-^ + 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. diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-optional-parameter-types.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-optional-parameter-types.yaml new file mode 100644 index 0000000000..b88061e9b4 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-optional-parameter-types.yaml @@ -0,0 +1,9 @@ +expression: ({int}) +text: '3' +exception: |- + This Cucumber Expression has a problem at column 2: + + ({int}) + ^---^ + 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 '{' diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml new file mode 100644 index 0000000000..d1c89689e9 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml @@ -0,0 +1,10 @@ +expression: |- + {(string)} +text: something +exception: |- + This Cucumber Expression has a problem at column 2: + + {(string)} + ^ + Parameter names may not contain '{', '}', '(', ')', '\' or '/'. + Did you mean to use a regular expression? diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml new file mode 100644 index 0000000000..e033648972 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string\} mice +exception: |- + This Cucumber Expression has a problem at column 24: + + three (exceptionally\) {string\} mice + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml new file mode 100644 index 0000000000..7cb9c6d56a --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string} mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (exceptionally\) {string} mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml new file mode 100644 index 0000000000..029c4e63bd --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml @@ -0,0 +1,8 @@ +expression: three ((exceptionally\) strong) mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three ((exceptionally\) strong) mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/javascript/testdata/expression/does-not-match-misquoted-string.yaml b/cucumber-expressions/javascript/testdata/expression/does-not-match-misquoted-string.yaml new file mode 100644 index 0000000000..18023180af --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/does-not-match-misquoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind' mice +expected: |- + null diff --git a/cucumber-expressions/javascript/testdata/expression/doesnt-match-float-as-int.yaml b/cucumber-expressions/javascript/testdata/expression/doesnt-match-float-as-int.yaml new file mode 100644 index 0000000000..d66b586430 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/doesnt-match-float-as-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '1.22' +expected: |- + null diff --git a/cucumber-expressions/javascript/testdata/expression/matches-alternation.yaml b/cucumber-expressions/javascript/testdata/expression/matches-alternation.yaml new file mode 100644 index 0000000000..20a9b9a728 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-alternation.yaml @@ -0,0 +1,4 @@ +expression: mice/rats and rats\/mice +text: rats and rats/mice +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-anonymous-parameter-type.yaml b/cucumber-expressions/javascript/testdata/expression/matches-anonymous-parameter-type.yaml new file mode 100644 index 0000000000..fc954960df --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-anonymous-parameter-type.yaml @@ -0,0 +1,5 @@ +expression: |- + {} +text: '0.22' +expected: |- + ["0.22"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c3e1962e22 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "" and "handsome" mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..89315b62ef --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "" mice +expected: |- + [""] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml new file mode 100644 index 0000000000..fe30a044c1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "bl\"nd" mice +expected: |- + ["bl\"nd"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml new file mode 100644 index 0000000000..25fcf304c1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "'blind'" mice +expected: |- + ["'blind'"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string.yaml b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string.yaml new file mode 100644 index 0000000000..8b0560f332 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-double-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-parenthesis.yaml b/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-parenthesis.yaml new file mode 100644 index 0000000000..902a084103 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-parenthesis.yaml @@ -0,0 +1,4 @@ +expression: three \\(exceptionally) \\{string} mice +text: three \exceptionally \"blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-slash-1.yaml b/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-slash-1.yaml new file mode 100644 index 0000000000..94e531eca7 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-slash-1.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: 12\ +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-slash-2.yaml b/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-slash-2.yaml new file mode 100644 index 0000000000..9c9c735b86 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-doubly-escaped-slash-2.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: '2020' +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-1.yaml b/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-1.yaml new file mode 100644 index 0000000000..171df89ee1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-1.yaml @@ -0,0 +1,4 @@ +expression: three \(exceptionally) \{string} mice +text: three (exceptionally) {string} mice +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-2.yaml b/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-2.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-2.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-3.yaml b/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-3.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-escaped-parenthesis-3.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-escaped-slash.yaml b/cucumber-expressions/javascript/testdata/expression/matches-escaped-slash.yaml new file mode 100644 index 0000000000..d8b3933399 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-escaped-slash.yaml @@ -0,0 +1,4 @@ +expression: 12\/2020 +text: 12/2020 +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-float-1.yaml b/cucumber-expressions/javascript/testdata/expression/matches-float-1.yaml new file mode 100644 index 0000000000..fe7e8b1869 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-float-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '0.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-float-2.yaml b/cucumber-expressions/javascript/testdata/expression/matches-float-2.yaml new file mode 100644 index 0000000000..c1e5894eac --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-float-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-int.yaml b/cucumber-expressions/javascript/testdata/expression/matches-int.yaml new file mode 100644 index 0000000000..bcd3763886 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '22' +expected: |- + [22] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-multiple-double-quoted-strings.yaml b/cucumber-expressions/javascript/testdata/expression/matches-multiple-double-quoted-strings.yaml new file mode 100644 index 0000000000..6c74bc2350 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-multiple-double-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "blind" and "crippled" mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-multiple-single-quoted-strings.yaml b/cucumber-expressions/javascript/testdata/expression/matches-multiple-single-quoted-strings.yaml new file mode 100644 index 0000000000..5037821c14 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-multiple-single-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three 'blind' and 'crippled' mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-1.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-1.yaml new file mode 100644 index 0000000000..821776715b --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three brown mice +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-2.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-2.yaml new file mode 100644 index 0000000000..71b3a341f1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three rats +expected: |- + [] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml new file mode 100644 index 0000000000..2632f410ce --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 2 seconds? +expected: |- + [2] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml new file mode 100644 index 0000000000..7b30f667bc --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 1 second. +expected: |- + [1] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-1.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-1.yaml new file mode 100644 index 0000000000..6574bb4bdf --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 3 rats +expected: |- + [3] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-2.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-2.yaml new file mode 100644 index 0000000000..4eb0f0e1c6 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 2 mice +expected: |- + [2] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-3.yaml b/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-3.yaml new file mode 100644 index 0000000000..964fa6489e --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-optional-in-alternation-3.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 1 mouse +expected: |- + [1] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c963dcf1c7 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three '' and 'handsome' mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..cff2a2d1ec --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '' mice +expected: |- + [""] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml new file mode 100644 index 0000000000..eb9ed537cd --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '"blind"' mice +expected: |- + ["\"blind\""] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml new file mode 100644 index 0000000000..4c2b0055b9 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'bl\'nd' mice +expected: |- + ["bl'nd"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string.yaml b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string.yaml new file mode 100644 index 0000000000..6c8f4652a5 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-single-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'blind' mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/javascript/testdata/expression/matches-word.yaml b/cucumber-expressions/javascript/testdata/expression/matches-word.yaml new file mode 100644 index 0000000000..358fd3afd1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/matches-word.yaml @@ -0,0 +1,4 @@ +expression: three {word} mice +text: three blind mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/javascript/testdata/expression/throws-unknown-parameter-type.yaml b/cucumber-expressions/javascript/testdata/expression/throws-unknown-parameter-type.yaml new file mode 100644 index 0000000000..384e3a48c3 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/expression/throws-unknown-parameter-type.yaml @@ -0,0 +1,10 @@ +expression: |- + {unknown} +text: something +exception: |- + This Cucumber Expression has a problem at column 1: + + {unknown} + ^-------^ + Undefined parameter type 'unknown'. + Please register a ParameterType for 'unknown' diff --git a/cucumber-expressions/javascript/testdata/regex/alternation-with-optional.yaml b/cucumber-expressions/javascript/testdata/regex/alternation-with-optional.yaml new file mode 100644 index 0000000000..73787b2b0a --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/alternation-with-optional.yaml @@ -0,0 +1,2 @@ +expression: "a/b(c)" +expected: ^(?:a|b(?:c)?)$ diff --git a/cucumber-expressions/javascript/testdata/regex/alternation.yaml b/cucumber-expressions/javascript/testdata/regex/alternation.yaml new file mode 100644 index 0000000000..1dc293fb62 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/alternation.yaml @@ -0,0 +1,2 @@ +expression: "a/b c/d/e" +expected: ^(?:a|b) (?:c|d|e)$ diff --git a/cucumber-expressions/javascript/testdata/regex/empty.yaml b/cucumber-expressions/javascript/testdata/regex/empty.yaml new file mode 100644 index 0000000000..bb9a81906c --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/empty.yaml @@ -0,0 +1,2 @@ +expression: "" +expected: ^$ diff --git a/cucumber-expressions/javascript/testdata/regex/escape-regex-characters.yaml b/cucumber-expressions/javascript/testdata/regex/escape-regex-characters.yaml new file mode 100644 index 0000000000..c8ea8c549e --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/escape-regex-characters.yaml @@ -0,0 +1,2 @@ +expression: '^$[]\(\){}\\.|?*+' +expected: ^\^\$\[\]\(\)(.*)\\\.\|\?\*\+$ diff --git a/cucumber-expressions/javascript/testdata/regex/optional.yaml b/cucumber-expressions/javascript/testdata/regex/optional.yaml new file mode 100644 index 0000000000..7d6d84cc14 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/optional.yaml @@ -0,0 +1,2 @@ +expression: "(a)" +expected: ^(?:a)?$ diff --git a/cucumber-expressions/javascript/testdata/regex/parameter.yaml b/cucumber-expressions/javascript/testdata/regex/parameter.yaml new file mode 100644 index 0000000000..f793b21c0f --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/parameter.yaml @@ -0,0 +1,2 @@ +expression: "{int}" +expected: ^((?:-?\d+)|(?:\d+))$ diff --git a/cucumber-expressions/javascript/testdata/regex/text.yaml b/cucumber-expressions/javascript/testdata/regex/text.yaml new file mode 100644 index 0000000000..2af3e41664 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/text.yaml @@ -0,0 +1,2 @@ +expression: "a" +expected: ^a$ diff --git a/cucumber-expressions/javascript/testdata/regex/unicode.yaml b/cucumber-expressions/javascript/testdata/regex/unicode.yaml new file mode 100644 index 0000000000..f93fe35db1 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/regex/unicode.yaml @@ -0,0 +1,2 @@ +expression: "Привет, Мир(ы)!" +expected: ^Привет, Мир(?:ы)?!$ diff --git a/cucumber-expressions/javascript/testdata/tokens/alternation-phrase.yaml b/cucumber-expressions/javascript/testdata/tokens/alternation-phrase.yaml new file mode 100644 index 0000000000..48b107f64e --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/alternation-phrase.yaml @@ -0,0 +1,13 @@ +expression: three blind/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "ALTERNATION", "start": 11, "end": 12, "text": "/"}, + {"type": "TEXT", "start": 12, "end": 19, "text": "cripple"}, + {"type": "WHITE_SPACE", "start": 19, "end": 20, "text": " "}, + {"type": "TEXT", "start": 20, "end": 24, "text": "mice"}, + {"type": "END_OF_LINE", "start": 24, "end": 24, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/alternation.yaml b/cucumber-expressions/javascript/testdata/tokens/alternation.yaml new file mode 100644 index 0000000000..a4920f22e5 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/alternation.yaml @@ -0,0 +1,9 @@ +expression: blind/cripple +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "blind"}, + {"type": "ALTERNATION", "start": 5, "end": 6, "text": "/"}, + {"type": "TEXT", "start": 6, "end": 13, "text": "cripple"}, + {"type": "END_OF_LINE", "start": 13, "end": 13, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/empty-string.yaml b/cucumber-expressions/javascript/testdata/tokens/empty-string.yaml new file mode 100644 index 0000000000..501f7522f2 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/empty-string.yaml @@ -0,0 +1,6 @@ +expression: "" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "END_OF_LINE", "start": 0, "end": 0, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/escape-non-reserved-character.yaml b/cucumber-expressions/javascript/testdata/tokens/escape-non-reserved-character.yaml new file mode 100644 index 0000000000..5e206be084 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escape-non-reserved-character.yaml @@ -0,0 +1,8 @@ +expression: \[ +exception: |- + This Cucumber Expression has a problem at column 2: + + \[ + ^ + Only the characters '{', '}', '(', ')', '\', '/' and whitespace can be escaped. + If you did mean to use an '\' you can use '\\' to escape it diff --git a/cucumber-expressions/javascript/testdata/tokens/escaped-alternation.yaml b/cucumber-expressions/javascript/testdata/tokens/escaped-alternation.yaml new file mode 100644 index 0000000000..7e21f7ad19 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escaped-alternation.yaml @@ -0,0 +1,9 @@ +expression: blind\ and\ famished\/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 29, "text": "blind and famished/cripple"}, + {"type": "WHITE_SPACE", "start": 29, "end": 30, "text": " "}, + {"type": "TEXT", "start": 30, "end": 34, "text": "mice"}, + {"type": "END_OF_LINE", "start": 34, "end": 34, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml b/cucumber-expressions/javascript/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml new file mode 100644 index 0000000000..6375ad52a5 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml @@ -0,0 +1,9 @@ +expression: ' \/ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "WHITE_SPACE", "start": 0, "end": 1, "text": " "}, + {"type": "TEXT", "start": 1, "end": 3, "text": "/"}, + {"type": "WHITE_SPACE", "start": 3, "end": 4, "text": " "}, + {"type": "END_OF_LINE", "start": 4, "end": 4, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/escaped-end-of-line.yaml b/cucumber-expressions/javascript/testdata/tokens/escaped-end-of-line.yaml new file mode 100644 index 0000000000..a1bd00fd98 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escaped-end-of-line.yaml @@ -0,0 +1,8 @@ +expression: \ +exception: |- + This Cucumber Expression has a problem at column 1: + + \ + ^ + The end of line can not be escaped. + You can use '\\' to escape the the '\' diff --git a/cucumber-expressions/javascript/testdata/tokens/escaped-optional.yaml b/cucumber-expressions/javascript/testdata/tokens/escaped-optional.yaml new file mode 100644 index 0000000000..2b365b581c --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: \(blind\) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 9, "text": "(blind)"}, + {"type": "END_OF_LINE", "start": 9, "end": 9, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/escaped-parameter.yaml b/cucumber-expressions/javascript/testdata/tokens/escaped-parameter.yaml new file mode 100644 index 0000000000..2cdac4f35a --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escaped-parameter.yaml @@ -0,0 +1,7 @@ +expression: \{string\} +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 10, "text": "{string}"}, + {"type": "END_OF_LINE", "start": 10, "end": 10, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/escaped-space.yaml b/cucumber-expressions/javascript/testdata/tokens/escaped-space.yaml new file mode 100644 index 0000000000..912827a941 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/escaped-space.yaml @@ -0,0 +1,7 @@ +expression: '\ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 2, "text": " "}, + {"type": "END_OF_LINE", "start": 2, "end": 2, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/optional-phrase.yaml b/cucumber-expressions/javascript/testdata/tokens/optional-phrase.yaml new file mode 100644 index 0000000000..2ddc6bb502 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/optional-phrase.yaml @@ -0,0 +1,13 @@ +expression: three (blind) mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_OPTIONAL", "start": 6, "end": 7, "text": "("}, + {"type": "TEXT", "start": 7, "end": 12, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 12, "end": 13, "text": ")"}, + {"type": "WHITE_SPACE", "start": 13, "end": 14, "text": " "}, + {"type": "TEXT", "start": 14, "end": 18, "text": "mice"}, + {"type": "END_OF_LINE", "start": 18, "end": 18, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/optional.yaml b/cucumber-expressions/javascript/testdata/tokens/optional.yaml new file mode 100644 index 0000000000..35b1474a7c --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/optional.yaml @@ -0,0 +1,9 @@ +expression: (blind) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_OPTIONAL", "start": 0, "end": 1, "text": "("}, + {"type": "TEXT", "start": 1, "end": 6, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 6, "end": 7, "text": ")"}, + {"type": "END_OF_LINE", "start": 7, "end": 7, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/parameter-phrase.yaml b/cucumber-expressions/javascript/testdata/tokens/parameter-phrase.yaml new file mode 100644 index 0000000000..5e98055ee6 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/parameter-phrase.yaml @@ -0,0 +1,13 @@ +expression: three {string} mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_PARAMETER", "start": 6, "end": 7, "text": "{"}, + {"type": "TEXT", "start": 7, "end": 13, "text": "string"}, + {"type": "END_PARAMETER", "start": 13, "end": 14, "text": "}"}, + {"type": "WHITE_SPACE", "start": 14, "end": 15, "text": " "}, + {"type": "TEXT", "start": 15, "end": 19, "text": "mice"}, + {"type": "END_OF_LINE", "start": 19, "end": 19, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/parameter.yaml b/cucumber-expressions/javascript/testdata/tokens/parameter.yaml new file mode 100644 index 0000000000..460363c393 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/parameter.yaml @@ -0,0 +1,9 @@ +expression: "{string}" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_PARAMETER", "start": 0, "end": 1, "text": "{"}, + {"type": "TEXT", "start": 1, "end": 7, "text": "string"}, + {"type": "END_PARAMETER", "start": 7, "end": 8, "text": "}"}, + {"type": "END_OF_LINE", "start": 8, "end": 8, "text": ""} + ] diff --git a/cucumber-expressions/javascript/testdata/tokens/phrase.yaml b/cucumber-expressions/javascript/testdata/tokens/phrase.yaml new file mode 100644 index 0000000000..e2cfccf7b4 --- /dev/null +++ b/cucumber-expressions/javascript/testdata/tokens/phrase.yaml @@ -0,0 +1,11 @@ +expression: three blind mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "WHITE_SPACE", "start": 11, "end": 12, "text": " "}, + {"type": "TEXT", "start": 12, "end": 16, "text": "mice"}, + {"type": "END_OF_LINE", "start": 16, "end": 16, "text": ""} + ] diff --git a/cucumber-expressions/ruby/.rsync b/cucumber-expressions/ruby/.rsync index d101b2935c..0b4ed5255f 100644 --- a/cucumber-expressions/ruby/.rsync +++ b/cucumber-expressions/ruby/.rsync @@ -2,3 +2,4 @@ ../../.templates/github/ .github/ ../../.templates/ruby/ . ../examples.txt examples.txt +../testdata . diff --git a/cucumber-expressions/ruby/examples.txt b/cucumber-expressions/ruby/examples.txt index 35b7bd17a9..3a8288d34d 100644 --- a/cucumber-expressions/ruby/examples.txt +++ b/cucumber-expressions/ruby/examples.txt @@ -2,7 +2,7 @@ I have {int} cuke(s) I have 22 cukes [22] --- -I have {int} cuke(s) and some \[]^$.|?*+ +I have {int} cuke(s) and some \\[]^$.|?*+ I have 1 cuke and some \[]^$.|?*+ [1] --- @@ -37,3 +37,7 @@ a purchase for $33 Some ${float} of cukes at {int}° Celsius Some $3.50 of cukes at 42° Celsius [3.5,42] +--- +I select the {int}st/nd/rd/th Cucumber +I select the 3rd Cucumber +[3] diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/ast.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/ast.rb new file mode 100644 index 0000000000..c096cc8718 --- /dev/null +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/ast.rb @@ -0,0 +1,201 @@ +module Cucumber + module CucumberExpressions + ESCAPE_CHARACTER = '\\' + ALTERNATION_CHARACTER = '/' + BEGIN_PARAMETER_CHARACTER = '{' + END_PARAMETER_CHARACTER = '}' + BEGIN_OPTIONAL_CHARACTER = '(' + END_OPTIONAL_CHARACTER = ')' + + class Node + def initialize(type, nodes, token, start, _end) + if nodes.nil? && token.nil? + raise 'Either nodes or token must be defined' + end + @type = type + @nodes = nodes + @token = token + @start = start + @end = _end + end + + def type + @type + end + + def nodes + @nodes + end + + def token + @token + end + + def start + @start + end + + def end + @end + end + + def text + if @token.nil? + return @nodes.map { |value| value.text }.join('') + end + @token + end + + def to_hash + hash = Hash.new + hash["type"] = @type + unless @nodes.nil? + hash["nodes"] = @nodes.map { |node| node.to_hash } + end + unless @token.nil? + hash["token"] = @token + end + hash["start"] = @start + hash["end"] = @end + hash + end + end + + module NodeType + TEXT = 'TEXT_NODE' + OPTIONAL = 'OPTIONAL_NODE' + ALTERNATION = 'ALTERNATION_NODE' + ALTERNATIVE = 'ALTERNATIVE_NODE' + PARAMETER = 'PARAMETER_NODE' + EXPRESSION = 'EXPRESSION_NODE' + end + + + class Token + def initialize(type, text, start, _end) + @type, @text, @start, @end = type, text, start, _end + end + + def type + @type + end + + def text + @text + end + + def start + @start + end + + def end + @end + end + + def self.is_escape_character(codepoint) + codepoint.chr(Encoding::UTF_8) == ESCAPE_CHARACTER + end + + def self.can_escape(codepoint) + c = codepoint.chr(Encoding::UTF_8) + if c == ' ' + # TODO: Unicode whitespace? + return true + end + case c + when ESCAPE_CHARACTER + true + when ALTERNATION_CHARACTER + true + when BEGIN_PARAMETER_CHARACTER + true + when END_PARAMETER_CHARACTER + true + when BEGIN_OPTIONAL_CHARACTER + true + when END_OPTIONAL_CHARACTER + true + else + false + end + end + + def self.type_of(codepoint) + c = codepoint.chr(Encoding::UTF_8) + if c == ' ' + # TODO: Unicode whitespace? + return TokenType::WHITE_SPACE + end + case c + when ALTERNATION_CHARACTER + TokenType::ALTERNATION + when BEGIN_PARAMETER_CHARACTER + TokenType::BEGIN_PARAMETER + when END_PARAMETER_CHARACTER + TokenType::END_PARAMETER + when BEGIN_OPTIONAL_CHARACTER + TokenType::BEGIN_OPTIONAL + when END_OPTIONAL_CHARACTER + TokenType::END_OPTIONAL + else + TokenType::TEXT + end + end + + def self.symbol_of(token) + case token + when TokenType::BEGIN_OPTIONAL + return BEGIN_OPTIONAL_CHARACTER + when TokenType::END_OPTIONAL + return END_OPTIONAL_CHARACTER + when TokenType::BEGIN_PARAMETER + return BEGIN_PARAMETER_CHARACTER + when TokenType::END_PARAMETER + return END_PARAMETER_CHARACTER + when TokenType::ALTERNATION + return ALTERNATION_CHARACTER + else + return '' + end + end + + def self.purpose_of(token) + case token + when TokenType::BEGIN_OPTIONAL + return 'optional text' + when TokenType::END_OPTIONAL + return 'optional text' + when TokenType::BEGIN_PARAMETER + return 'a parameter' + when TokenType::END_PARAMETER + return 'a parameter' + when TokenType::ALTERNATION + return 'alternation' + else + return '' + end + end + + def to_hash + { + "type" => @type, + "text" => @text, + "start" => @start, + "end" => @end + } + end + end + + module TokenType + START_OF_LINE = 'START_OF_LINE' + END_OF_LINE = 'END_OF_LINE' + WHITE_SPACE = 'WHITE_SPACE' + BEGIN_OPTIONAL = 'BEGIN_OPTIONAL' + END_OPTIONAL = 'END_OPTIONAL' + BEGIN_PARAMETER = 'BEGIN_PARAMETER' + END_PARAMETER = 'END_PARAMETER' + ALTERNATION = 'ALTERNATION' + TEXT = 'TEXT' + end + end +end diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression.rb index e7fb7123a4..2c1807dfd8 100644 --- a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression.rb +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression.rb @@ -1,38 +1,32 @@ require 'cucumber/cucumber_expressions/argument' require 'cucumber/cucumber_expressions/tree_regexp' require 'cucumber/cucumber_expressions/errors' +require 'cucumber/cucumber_expressions/cucumber_expression_parser' module Cucumber module CucumberExpressions class CucumberExpression - # Does not include (){} characters because they have special meaning - ESCAPE_REGEXP = /([\\^\[$.|?*+\]])/ - PARAMETER_REGEXP = /(\\\\)?{([^}]*)}/ - OPTIONAL_REGEXP = /(\\\\)?\(([^)]+)\)/ - ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = /([^\s^\/]+)((\/[^\s^\/]+)+)/ - DOUBLE_ESCAPE = '\\\\' - PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE = 'Parameter types cannot be alternative: ' - PARAMETER_TYPES_CANNOT_BE_OPTIONAL = 'Parameter types cannot be optional: ' - attr_reader :source + ESCAPE_PATTERN = /([\\^\[({$.|?*+})\]])/ def initialize(expression, parameter_type_registry) - @source = expression + @expression = expression + @parameter_type_registry = parameter_type_registry @parameter_types = [] - - expression = process_escapes(expression) - expression = process_optional(expression) - expression = process_alternation(expression) - expression = process_parameters(expression, parameter_type_registry) - expression = "^#{expression}$" - - @tree_regexp = TreeRegexp.new(expression) + parser = CucumberExpressionParser.new + ast = parser.parse(expression) + pattern = rewrite_to_regex(ast) + @tree_regexp = TreeRegexp.new(pattern) end def match(text) Argument.build(@tree_regexp, text, @parameter_types) end + def source + @expression + end + def regexp @tree_regexp.regexp end @@ -43,76 +37,86 @@ def to_s private - def process_escapes(expression) - expression.gsub(ESCAPE_REGEXP, '\\\\\1') + def rewrite_to_regex(node) + case node.type + when NodeType::TEXT + return escape_regex(node.text) + when NodeType::OPTIONAL + return rewrite_optional(node) + when NodeType::ALTERNATION + return rewrite_alternation(node) + when NodeType::ALTERNATIVE + return rewrite_alternative(node) + when NodeType::PARAMETER + return rewrite_parameter(node) + when NodeType::EXPRESSION + return rewrite_expression(node) + else + # Can't happen as long as the switch case is exhaustive + raise "#{node.type}" + end end - def process_optional(expression) - # Create non-capturing, optional capture groups from parenthesis - expression.gsub(OPTIONAL_REGEXP) do - g2 = $2 - # When using Parameter Types, the () characters are used to represent an optional - # item such as (a ) which would be equivalent to (?:a )? in regex - # - # You cannot have optional Parameter Types i.e. ({int}) as this causes - # problems during the conversion phase to regex. So we check for that here - # - # One exclusion to this rule is if you actually want the brackets i.e. you - # want to capture (3) then we still permit this as an individual rule - # See: https://github.com/cucumber/cucumber-ruby/issues/1337 for more info - # look for double-escaped parentheses - if $1 == DOUBLE_ESCAPE - "\\(#{g2}\\)" - else - check_no_parameter_type(g2, PARAMETER_TYPES_CANNOT_BE_OPTIONAL) - "(?:#{g2})?" - end - end + def escape_regex(expression) + expression.gsub(ESCAPE_PATTERN, '\\\\\1') end - def process_alternation(expression) - expression.gsub(ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP) do - # replace \/ with / - # replace / with | - replacement = $&.tr('/', '|').gsub(/\\\|/, '/') - if replacement.include?('|') - replacement.split(/\|/).each do |part| - check_no_parameter_type(part, PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE) - end - "(?:#{replacement})" - else - replacement - end - end + def rewrite_optional(node) + assert_no_parameters(node) { |astNode| raise ParameterIsNotAllowedInOptional.new(astNode, @expression) } + assert_no_optionals(node) { |astNode| raise OptionalIsNotAllowedInOptional.new(astNode, @expression) } + assert_not_empty(node) { |astNode| raise OptionalMayNotBeEmpty.new(astNode, @expression) } + regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('') + "(?:#{regex})?" end - def process_parameters(expression, parameter_type_registry) - # Create non-capturing, optional capture groups from parenthesis - expression.gsub(PARAMETER_REGEXP) do - if ($1 == DOUBLE_ESCAPE) - "\\{#{$2}\\}" - else - type_name = $2 - ParameterType.check_parameter_type_name(type_name) - parameter_type = parameter_type_registry.lookup_by_type_name(type_name) - raise UndefinedParameterTypeError.new(type_name) if parameter_type.nil? - @parameter_types.push(parameter_type) - - build_capture_regexp(parameter_type.regexps) + def rewrite_alternation(node) + # Make sure the alternative parts aren't empty and don't contain parameter types + node.nodes.each { |alternative| + if alternative.nodes.length == 0 + raise AlternativeMayNotBeEmpty.new(alternative, @expression) end - end + assert_not_empty(alternative) { |astNode| raise AlternativeMayNotExclusivelyContainOptionals.new(astNode, @expression) } + } + regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('|') + "(?:#{regex})" end - def build_capture_regexp(regexps) - return "(#{regexps[0]})" if regexps.size == 1 - capture_groups = regexps.map { |group| "(?:#{group})" } - "(#{capture_groups.join('|')})" + def rewrite_alternative(node) + node.nodes.map { |lastNode| rewrite_to_regex(lastNode) }.join('') end - def check_no_parameter_type(s, message) - if PARAMETER_REGEXP =~ s - raise CucumberExpressionError.new("#{message}#{source}") + def rewrite_parameter(node) + name = node.text + parameter_type = @parameter_type_registry.lookup_by_type_name(name) + if parameter_type.nil? + raise UndefinedParameterTypeError.new(node, @expression, name) end + @parameter_types.push(parameter_type) + regexps = parameter_type.regexps + if regexps.length == 1 + return "(#{regexps[0]})" + end + "((?:#{regexps.join(')|(?:')}))" + end + + def rewrite_expression(node) + regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('') + "^#{regex}$" + end + + def assert_not_empty(node, &raise_error) + text_nodes = node.nodes.filter { |astNode| NodeType::TEXT == astNode.type } + raise_error.call(node) if text_nodes.length == 0 + end + + def assert_no_parameters(node, &raise_error) + nodes = node.nodes.filter { |astNode| NodeType::PARAMETER == astNode.type } + raise_error.call(nodes[0]) if nodes.length > 0 + end + + def assert_no_optionals(node, &raise_error) + nodes = node.nodes.filter { |astNode| NodeType::OPTIONAL == astNode.type } + raise_error.call(nodes[0]) if nodes.length > 0 end end end diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb index b161584fe0..d7c4d1835a 100644 --- a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb @@ -59,9 +59,7 @@ def generate_expressions(text) break end - if pos >= text.length - break - end + break if pos >= text.length end expression_template += escape(text.slice(pos..-1)) @@ -85,19 +83,17 @@ def create_parameter_type_matchers(text) end def create_parameter_type_matchers2(parameter_type, text) - result = [] regexps = parameter_type.regexps - regexps.each do |regexp| + regexps.map do |regexp| regexp = Regexp.new("(#{regexp})") - result.push(ParameterTypeMatcher.new(parameter_type, regexp, text, 0)) + ParameterTypeMatcher.new(parameter_type, regexp, text, 0) end - result end def escape(s) s.gsub(/%/, '%%') .gsub(/\(/, '\\(') - .gsub(/\{/, '\\{') + .gsub(/{/, '\\{') .gsub(/\//, '\\/') end end diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb new file mode 100644 index 0000000000..e986789470 --- /dev/null +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb @@ -0,0 +1,219 @@ +require 'cucumber/cucumber_expressions/ast' +require 'cucumber/cucumber_expressions/errors' +require 'cucumber/cucumber_expressions/cucumber_expression_tokenizer' + +module Cucumber + module CucumberExpressions + class CucumberExpressionParser + def parse(expression) + # text := whitespace | ')' | '}' | . + parse_text = lambda do |_, tokens, current| + token = tokens[current] + case token.type + when TokenType::WHITE_SPACE, TokenType::TEXT, TokenType::END_PARAMETER, TokenType::END_OPTIONAL + return 1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)] + when TokenType::ALTERNATION + raise AlternationNotAllowedInOptional.new(expression, token) + when TokenType::BEGIN_PARAMETER, TokenType::START_OF_LINE, TokenType::END_OF_LINE, TokenType::BEGIN_OPTIONAL + else + # If configured correctly this will never happen + return 0, nil + end + # If configured correctly this will never happen + return 0, nil + end + + # name := whitespace | . + parse_name = lambda do |_, tokens, current| + token = tokens[current] + case token.type + when TokenType::WHITE_SPACE, TokenType::TEXT + return 1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)] + when TokenType::BEGIN_PARAMETER, TokenType::END_PARAMETER, TokenType::BEGIN_OPTIONAL, TokenType::END_OPTIONAL, TokenType::ALTERNATION + raise InvalidParameterTypeNameInNode.new(expression, token) + when TokenType::START_OF_LINE, TokenType::END_OF_LINE + # If configured correctly this will never happen + return 0, nil + else + # If configured correctly this will never happen + return 0, nil + end + end + + # parameter := '{' + name* + '}' + parse_parameter = parse_between( + NodeType::PARAMETER, + TokenType::BEGIN_PARAMETER, + TokenType::END_PARAMETER, + [parse_name] + ) + + # optional := '(' + option* + ')' + # option := optional | parameter | text + optional_sub_parsers = [] + parse_optional = parse_between( + NodeType::OPTIONAL, + TokenType::BEGIN_OPTIONAL, + TokenType::END_OPTIONAL, + optional_sub_parsers + ) + optional_sub_parsers << parse_optional << parse_parameter << parse_text + + # alternation := alternative* + ( '/' + alternative* )+ + parse_alternative_separator = lambda do |_, tokens, current| + unless looking_at(tokens, current, TokenType::ALTERNATION) + return 0, nil + end + token = tokens[current] + return 1, [Node.new(NodeType::ALTERNATIVE, nil, token.text, token.start, token.end)] + end + + alternative_parsers = [ + parse_alternative_separator, + parse_optional, + parse_parameter, + parse_text, + ] + + # alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary) + # left-boundary := whitespace | } | ^ + # right-boundary := whitespace | { | $ + # alternative: = optional | parameter | text + parse_alternation = lambda do |expr, tokens, current| + previous = current - 1 + unless looking_at_any(tokens, previous, [TokenType::START_OF_LINE, TokenType::WHITE_SPACE, TokenType::END_PARAMETER]) + return 0, nil + end + + consumed, ast = parse_tokens_until(expr, alternative_parsers, tokens, current, [TokenType::WHITE_SPACE, TokenType::END_OF_LINE, TokenType::BEGIN_PARAMETER]) + sub_current = current + consumed + unless ast.map { |astNode| astNode.type }.include? NodeType::ALTERNATIVE + return 0, nil + end + + start = tokens[current].start + _end = tokens[sub_current].start + # Does not consume right hand boundary token + return consumed, [Node.new(NodeType::ALTERNATION, split_alternatives(start, _end, ast), nil, start, _end)] + end + + # + # cucumber-expression := ( alternation | optional | parameter | text )* + # + parse_cucumber_expression = parse_between( + NodeType::EXPRESSION, + TokenType::START_OF_LINE, + TokenType::END_OF_LINE, + [parse_alternation, parse_optional, parse_parameter, parse_text] + ) + + tokenizer = CucumberExpressionTokenizer.new + tokens = tokenizer.tokenize(expression) + _, ast = parse_cucumber_expression.call(expression, tokens, 0) + ast[0] + end + + private + + def parse_between(type, begin_token, end_token, parsers) + lambda do |expression, tokens, current| + unless looking_at(tokens, current, begin_token) + return 0, nil + end + sub_current = current + 1 + consumed, ast = parse_tokens_until(expression, parsers, tokens, sub_current, [end_token, TokenType::END_OF_LINE]) + sub_current += consumed + + # endToken not found + unless looking_at(tokens, sub_current, end_token) + raise MissingEndToken.new(expression, begin_token, end_token, tokens[current]) + end + # consumes endToken + start = tokens[current].start + _end = tokens[sub_current].end + consumed = sub_current + 1 - current + ast = [Node.new(type, ast, nil, start, _end)] + return consumed, ast + end + end + + def parse_token(expression, parsers, tokens, start_at) + parsers.each do |parser| + consumed, ast = parser.call(expression, tokens, start_at) + return consumed, ast unless consumed == 0 + end + # If configured correctly this will never happen + raise 'No eligible parsers for ' + tokens + end + + def parse_tokens_until(expression, parsers, tokens, start_at, end_tokens) + current = start_at + size = tokens.length + ast = [] + while current < size do + if looking_at_any(tokens, current, end_tokens) + break + end + consumed, sub_ast = parse_token(expression, parsers, tokens, current) + if consumed == 0 + # If configured correctly this will never happen + # Keep to avoid infinite loops + raise 'No eligible parsers for ' + tokens + end + current += consumed + ast += sub_ast + end + [current - start_at, ast] + end + + def looking_at_any(tokens, at, token_types) + token_types.detect { |token_type| looking_at(tokens, at, token_type) } + end + + def looking_at(tokens, at, token) + if at < 0 + # If configured correctly this will never happen + # Keep for completeness + return token == TokenType::START_OF_LINE + end + if at >= tokens.length + return token == TokenType::END_OF_LINE + end + tokens[at].type == token + end + + def split_alternatives(start, _end, alternation) + separators = [] + alternatives = [] + alternative = [] + alternation.each { |n| + if NodeType::ALTERNATIVE == n.type + separators.push(n) + alternatives.push(alternative) + alternative = [] + else + alternative.push(n) + end + } + alternatives.push(alternative) + create_alternative_nodes(start, _end, separators, alternatives) + end + + def create_alternative_nodes(start, _end, separators, alternatives) + alternatives.each_with_index.map do |n, i| + if i == 0 + right_separator = separators[i] + Node.new(NodeType::ALTERNATIVE, n, nil, start, right_separator.start) + elsif i == alternatives.length - 1 + left_separator = separators[i - 1] + Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, _end) + else + left_separator = separators[i - 1] + right_separator = separators[i] + Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, right_separator.start) + end + end + end + end + end +end diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb new file mode 100644 index 0000000000..fb65f03ade --- /dev/null +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb @@ -0,0 +1,95 @@ +require 'cucumber/cucumber_expressions/ast' +require 'cucumber/cucumber_expressions/errors' + +module Cucumber + module CucumberExpressions + class CucumberExpressionTokenizer + def tokenize(expression) + @expression = expression + tokens = [] + @buffer = [] + previous_token_type = TokenType::START_OF_LINE + treat_as_text = false + @escaped = 0 + @buffer_start_index = 0 + + codepoints = expression.codepoints + + if codepoints.empty? + tokens.push(Token.new(TokenType::START_OF_LINE, '', 0, 0)) + end + + codepoints.each do |codepoint| + if !treat_as_text && Token.is_escape_character(codepoint) + @escaped += 1 + treat_as_text = true + next + end + current_token_type = token_type_of(codepoint, treat_as_text) + treat_as_text = false + + if should_create_new_token?(previous_token_type, current_token_type) + token = convert_buffer_to_token(previous_token_type) + previous_token_type = current_token_type + @buffer.push(codepoint) + tokens.push(token) + else + previous_token_type = current_token_type + @buffer.push(codepoint) + end + end + + if @buffer.length > 0 + token = convert_buffer_to_token(previous_token_type) + tokens.push(token) + end + + raise TheEndOfLineCannotBeEscaped.new(expression) if treat_as_text + + tokens.push(Token.new(TokenType::END_OF_LINE, '', codepoints.length, codepoints.length)) + tokens + end + + private + + # TODO: Make these lambdas + + def convert_buffer_to_token(token_type) + escape_tokens = 0 + if token_type == TokenType::TEXT + escape_tokens = @escaped + @escaped = 0 + end + + consumed_index = @buffer_start_index + @buffer.length + escape_tokens + t = Token.new( + token_type, + @buffer.map { |codepoint| codepoint.chr(Encoding::UTF_8) }.join(''), + @buffer_start_index, + consumed_index + ) + @buffer = [] + @buffer_start_index = consumed_index + t + end + + def token_type_of(codepoint, treat_as_text) + unless treat_as_text + return Token.type_of(codepoint) + end + if Token.can_escape(codepoint) + return TokenType::TEXT + end + raise CantEscape.new( + @expression, + @buffer_start_index + @buffer.length + @escaped + ) + end + + def should_create_new_token?(previous_token_type, current_token_type) + current_token_type != previous_token_type || + (current_token_type != TokenType::WHITE_SPACE && current_token_type != TokenType::TEXT) + end + end + end +end diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/errors.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/errors.rb index 9a480cc698..7e5fa647b6 100644 --- a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/errors.rb +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/errors.rb @@ -1,11 +1,182 @@ +require 'cucumber/cucumber_expressions/ast' + module Cucumber module CucumberExpressions class CucumberExpressionError < StandardError + + def build_message( + index, + expression, + pointer, + problem, + solution + ) + m = <<-EOF +This Cucumber Expression has a problem at column #{index + 1}: + +#{expression} +#{pointer} +#{problem}. +#{solution} + EOF + m.strip + end + + def point_at(index) + ' ' * index + '^' + end + + def point_at_located(node) + pointer = [point_at(node.start)] + if node.start + 1 < node.end + for _ in node.start + 1...node.end - 1 + pointer.push('-') + end + pointer.push('^') + end + pointer.join('') + end end - class UndefinedParameterTypeError < CucumberExpressionError + class AlternativeMayNotExclusivelyContainOptionals < CucumberExpressionError + def initialize(node, expression) + super(build_message( + node.start, + expression, + point_at_located(node), + 'An alternative may not exclusively contain optionals', + "If you did not mean to use an optional you can use '\\(' to escape the the '('" + )) + end + end + + class AlternativeMayNotBeEmpty < CucumberExpressionError + def initialize(node, expression) + super(build_message( + node.start, + expression, + point_at_located(node), + 'Alternative may not be empty', + "If you did not mean to use an alternative you can use '\\/' to escape the the '/'" + )) + end + end + + class CantEscape < CucumberExpressionError + def initialize(expression, index) + super(build_message( + index, + expression, + point_at(index), + "Only the characters '{', '}', '(', ')', '\\', '/' and whitespace can be escaped", + "If you did mean to use an '\\' you can use '\\\\' to escape it" + )) + end + end + + class OptionalMayNotBeEmpty < CucumberExpressionError + def initialize(node, expression) + super(build_message( + node.start, + expression, + point_at_located(node), + 'An optional must contain some text', + "If you did not mean to use an optional you can use '\\(' to escape the the '('" + )) + end + end + + class ParameterIsNotAllowedInOptional < CucumberExpressionError + def initialize(node, expression) + super(build_message( + node.start, + expression, + point_at_located(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 '{'" + )) + end + end + + class OptionalIsNotAllowedInOptional < CucumberExpressionError + def initialize(node, expression) + super(build_message( + node.start, + expression, + point_at_located(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." + )) + end + end + + class TheEndOfLineCannotBeEscaped < CucumberExpressionError + def initialize(expression) + index = expression.codepoints.length - 1 + super(build_message( + index, + expression, + point_at(index), + 'The end of line can not be escaped', + "You can use '\\\\' to escape the the '\\'" + )) + end + end + + class MissingEndToken < CucumberExpressionError + def initialize(expression, begin_token, end_token, current) + begin_symbol = Token::symbol_of(begin_token) + end_symbol = Token::symbol_of(end_token) + purpose = Token::purpose_of(begin_token) + super(build_message( + current.start, + expression, + point_at_located(current), + "The '#{begin_symbol}' does not have a matching '#{end_symbol}'", + "If you did not intend to use #{purpose} you can use '\\#{begin_symbol}' to escape the #{purpose}" + )) + end + end + + class AlternationNotAllowedInOptional < CucumberExpressionError + def initialize(expression, current) + super(build_message( + current.start, + expression, + point_at_located(current), + "An alternation can not be used inside an optional", + "You can use '\\/' to escape the the '/'" + )) + end + end + + class InvalidParameterTypeName < CucumberExpressionError def initialize(type_name) - super("Undefined parameter type {#{type_name}}") + super("Illegal character in parameter name {#{type_name}}. " + + "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'") + end + end + + + class InvalidParameterTypeNameInNode < CucumberExpressionError + def initialize(expression, token) + super(build_message( + token.start, + expression, + point_at_located(token), + "Parameter names may not contain '{', '}', '(', ')', '\\' or '/'", + "Did you mean to use a regular expression?" + )) + end + end + + class UndefinedParameterTypeError < CucumberExpressionError + def initialize(node, expression, parameter_type_name) + super(build_message(node.start, + expression, + point_at_located(node), + "Undefined parameter type '#{parameter_type_name}'", + "Please register a ParameterType for '#{parameter_type_name}'")) end end @@ -29,11 +200,11 @@ def initialize(parameter_type_regexp, expression_regexp, parameter_types, genera private def parameter_type_names(parameter_types) - parameter_types.map{|p| "{#{p.name}}"}.join("\n ") + parameter_types.map { |p| "{#{p.name}}" }.join("\n ") end def expressions(generated_expressions) - generated_expressions.map{|ge| ge.source}.join("\n ") + generated_expressions.map { |ge| ge.source }.join("\n ") end end end diff --git a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/parameter_type.rb b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/parameter_type.rb index 26d4721747..420b81fae5 100644 --- a/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/parameter_type.rb +++ b/cucumber-expressions/ruby/lib/cucumber/cucumber_expressions/parameter_type.rb @@ -9,12 +9,16 @@ class ParameterType attr_reader :name, :type, :regexps def self.check_parameter_type_name(type_name) + unless is_valid_parameter_type_name(type_name) + raise CucumberExpressionError.new("Illegal character in parameter name {#{type_name}}. Parameter names may not contain '[]()$.|?*+'") + end + end + + def self.is_valid_parameter_type_name(type_name) unescaped_type_name = type_name.gsub(UNESCAPE_PATTERN) do $2 end - if ILLEGAL_PARAMETER_NAME_PATTERN =~ unescaped_type_name - raise CucumberExpressionError.new("Illegal character '#{$1}' in parameter name {#{unescaped_type_name}}") - end + !(ILLEGAL_PARAMETER_NAME_PATTERN =~ unescaped_type_name) end def prefer_for_regexp_match? @@ -58,16 +62,17 @@ def <=>(other) private + def string_array(regexps) array = regexps.is_a?(Array) ? regexps : [regexps] - array.map {|regexp| regexp.is_a?(String) ? regexp : regexp_source(regexp)} + array.map { |regexp| regexp.is_a?(String) ? regexp : regexp_source(regexp) } end def regexp_source(regexp) [ - 'EXTENDED', - 'IGNORECASE', - 'MULTILINE' + 'EXTENDED', + 'IGNORECASE', + 'MULTILINE' ].each do |option_name| option = Regexp.const_get(option_name) if regexp.options & option != 0 diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_parser_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_parser_spec.rb new file mode 100644 index 0000000000..94d1afeeb9 --- /dev/null +++ b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_parser_spec.rb @@ -0,0 +1,24 @@ +require 'yaml' +require 'json' +require 'cucumber/cucumber_expressions/cucumber_expression_parser' +require 'cucumber/cucumber_expressions/errors' + +module Cucumber + module CucumberExpressions + describe 'Cucumber expression parser' do + Dir['testdata/ast/*.yaml'].each do |testcase| + expectation = YAML.load_file(testcase) # encoding? + it "#{testcase}" do + parser = CucumberExpressionParser.new + if expectation['exception'].nil? + node = parser.parse(expectation['expression']) + node_hash = node.to_hash + expect(node_hash).to eq(JSON.parse(expectation['expected'])) + else + expect { parser.parse(expectation['expression']) }.to raise_error(expectation['exception']) + end + end + end + end + end +end diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb deleted file mode 100644 index 0e7ba95cb3..0000000000 --- a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'cucumber/cucumber_expressions/cucumber_expression' -require 'cucumber/cucumber_expressions/parameter_type_registry' - -module Cucumber - module CucumberExpressions - describe CucumberExpression do - context "Regexp translation" do - def assert_regexp(expression, regexp) - cucumber_expression = CucumberExpression.new(expression, ParameterTypeRegistry.new) - expect(regexp).to eq(cucumber_expression.regexp) - end - - it "translates no arguments" do - assert_regexp( - "I have 10 cukes in my belly now", - /^I have 10 cukes in my belly now$/ - ) - end - - it "translates alternation" do - assert_regexp( - "I had/have a great/nice/charming friend", - /^I (?:had|have) a (?:great|nice|charming) friend$/ - ) - end - - it "translates alternation with non-alpha" do - assert_regexp( - "I said Alpha1/Beta1", - /^I said (?:Alpha1|Beta1)$/ - ) - end - - it "translates parameters" do - assert_regexp( - "I have {float} cukes at {int} o'clock", - /^I have ((?=.*\d.*)[-+]?\d*(?:\.(?=\d.*))?\d*(?:\d+[E][-+]?\d+)?) cukes at ((?:-?\d+)|(?:\d+)) o'clock$/ - ) - end - - it "translates parenthesis to non-capturing optional capture group" do - assert_regexp( - "I have many big(ish) cukes", - /^I have many big(?:ish)? cukes$/ - ) - end - - it "translates parenthesis with alpha unicode" do - assert_regexp( - "Привет, Мир(ы)!", - /^Привет, Мир(?:ы)?!$/ - ) - end - end - end - end -end diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb index 1de4ed4eef..e975bfa917 100644 --- a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb +++ b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb @@ -1,9 +1,39 @@ +require 'yaml' +require 'json' require 'cucumber/cucumber_expressions/cucumber_expression' require 'cucumber/cucumber_expressions/parameter_type_registry' module Cucumber module CucumberExpressions describe CucumberExpression do + + Dir['testdata/expression/*.yaml'].each do |testcase| + expectation = YAML.load_file(testcase) # encoding? + it "#{testcase}" do + parameter_registry = ParameterTypeRegistry.new + if expectation['exception'].nil? + cucumber_expression = CucumberExpression.new(expectation['expression'], parameter_registry) + matches = cucumber_expression.match(expectation['text']) + values = matches.nil? ? nil : matches.map { |arg| arg.value(nil) } + expect(values).to eq(JSON.parse(expectation['expected'])) + else + expect { + cucumber_expression = CucumberExpression.new(expectation['expression'], parameter_registry) + cucumber_expression.match(expectation['text']) + }.to raise_error(expectation['exception']) + end + end + end + + Dir['testdata/regex/*.yaml'].each do |testcase| + expectation = YAML.load_file(testcase) # encoding? + it "#{testcase}" do + parameter_registry = ParameterTypeRegistry.new + cucumber_expression = CucumberExpression.new(expectation['expression'], parameter_registry) + expect(cucumber_expression.regexp.source).to eq(expectation['expected']) + end + end + it "documents match arguments" do parameter_registry = ParameterTypeRegistry.new @@ -15,82 +45,6 @@ module CucumberExpressions ### [capture-match-arguments] end - it "matches word" do - expect(match("three {word} mice", "three blind mice")).to eq(['blind']) - end - - it('matches double quoted string') do - expect(match('three {string} mice', 'three "blind" mice')).to eq(['blind']) - end - - it('matches multiple double quoted strings') do - expect(match('three {string} and {string} mice', 'three "blind" and "crippled" mice')).to eq(['blind', 'crippled']) - end - - it('matches single quoted string') do - expect(match('three {string} mice', "three 'blind' mice")).to eq(['blind']) - end - - it('matches multiple single quoted strings') do - expect(match('three {string} and {string} mice', "three 'blind' and 'crippled' mice")).to eq(['blind', 'crippled']) - end - - it('does not match misquoted string') do - expect(match('three {string} mice', 'three "blind\' mice')).to eq(nil) - end - - it('matches single quoted string with double quotes') do - expect(match('three {string} mice', 'three \'"blind"\' mice')).to eq(['"blind"']) - end - - it('matches double quoted string with single quotes') do - expect(match('three {string} mice', 'three "\'blind\'" mice')).to eq(["'blind'"]) - end - - it('matches double quoted string with escaped double quote') do - expect(match('three {string} mice', 'three "bl\\"nd" mice')).to eq(['bl"nd']) - end - - it('matches single quoted string with escaped single quote') do - expect(match('three {string} mice', "three 'bl\\'nd' mice")).to eq(["bl'nd"]) - end - - it('matches single quoted empty string as empty string') do - expect(match('three {string} mice', "three '' mice")).to eq(['']) - end - - it('matches double quoted empty string as empty string') do - expect(match('three {string} mice', 'three "" mice')).to eq(['']) - end - - it('matches single quoted empty string as empty string, along with other strings') do - expect(match('three {string} and {string} mice', "three '' and 'handsome' mice")).to eq(['', 'handsome']) - end - - it('matches double quoted empty string as empty string, along with other strings') do - expect(match('three {string} and {string} mice', 'three "" and "handsome" mice')).to eq(['', 'handsome']) - end - - it 'matches escaped parentheses' do - expect(match('three \\(exceptionally) {string} mice', 'three (exceptionally) "blind" mice')).to eq(['blind']) - end - - it "matches escaped slash" do - expect(match("12\\/2020", "12/2020")).to eq([]) - end - - it "matches int" do - expect(match("{int}", "22")).to eq([22]) - end - - it "doesn't match float as int" do - expect(match("{int}", "1.22")).to be_nil - end - - it "matches int as float" do - expect(match("{float}", "0")).to eq([0.0]) - end - it "matches float" do expect(match("{float}", "")).to eq(nil) expect(match("{float}", ".")).to eq(nil) @@ -126,34 +80,12 @@ module CucumberExpressions expect(match("{float}", "-.1E2")).to eq([-10]) end - it "matches anonymous" do - expect(match("{}", "0.22")).to eq(["0.22"]) - end - - '[]()$.|?*+'.split('').each do |char| - it "does not allow parameter type with #{char}" do - expect {match("{#{char}string}", "something")}.to raise_error("Illegal character '#{char}' in parameter name {#{char}string}") - end - end - - it "throws unknown parameter type" do - expect {match("{unknown}", "something")}.to raise_error('Undefined parameter type {unknown}') - end - - it "does not allow optional parameter types" do - expect {match("({int})", "3")}.to raise_error('Parameter types cannot be optional: ({int})') - end - - it "does allow escaped optional parameter types" do - expect(match("\\({int})", "(3)")).to eq([3]) - end - - it "does not allow text/parameter type alternation" do - expect {match("x/{int}", "3")}.to raise_error('Parameter types cannot be alternative: x/{int}') + it "float with zero" do + expect(match("{float}", "0")).to eq([0.0]) end - it "does not allow parameter type/text alternation" do - expect {match("{int}/x", "3")}.to raise_error('Parameter types cannot be alternative: {int}/x') + it "matches anonymous" do + expect(match("{}", "0.22")).to eq(["0.22"]) end it "exposes source" do @@ -161,78 +93,68 @@ module CucumberExpressions expect(CucumberExpression.new(expr, ParameterTypeRegistry.new).source).to eq(expr) end - it "delegates transform to custom object" do + it "unmatched optional groups have undefined values" do parameter_type_registry = ParameterTypeRegistry.new parameter_type_registry.define_parameter_type( - ParameterType.new( - 'widget', - /\w+/, - Object, - -> (s) { - self.create_widget(s) - }, - false, - true - ) + ParameterType.new( + 'textAndOrNumber', + /([A-Z]+)?(?: )?([0-9]+)?/, + Object, + -> (s1, s2) { + [s1, s2] + }, + false, + true + ) ) expression = CucumberExpression.new( - 'I have a {widget}', - parameter_type_registry + '{textAndOrNumber}', + parameter_type_registry ) class World - def create_widget(s) - "widget:#{s}" - end end - args = expression.match("I have a bolt") - expect(args[0].value(World.new)).to eq('widget:bolt') - end - - describe "escapes special characters" do - %w(\\ [ ] ^ $ . | ? * +).each do |character| - it "escapes #{character}" do - expr = "I have {int} cuke(s) and #{character}" - expression = CucumberExpression.new(expr, ParameterTypeRegistry.new) - arg1 = expression.match("I have 800 cukes and #{character}")[0] - expect(arg1.value(nil)).to eq(800) - end - end + expect(expression.match("TLA")[0].value(World.new)).to eq(["TLA", nil]) + expect(expression.match("123")[0].value(World.new)).to eq([nil, "123"]) end + # Ruby specific - it "unmatched optional groups have undefined values" do + it "delegates transform to custom object" do parameter_type_registry = ParameterTypeRegistry.new parameter_type_registry.define_parameter_type( ParameterType.new( - 'textAndOrNumber', - /([A-Z]+)?(?: )?([0-9]+)?/, + 'widget', + /\w+/, Object, - -> (s1, s2) { - [s1, s2] + -> (s) { + self.create_widget(s) }, false, true ) ) expression = CucumberExpression.new( - '{textAndOrNumber}', + 'I have a {widget}', parameter_type_registry ) class World + def create_widget(s) + "widget:#{s}" + end end - expect(expression.match("TLA")[0].value(World.new)).to eq(["TLA", nil]) - expect(expression.match("123")[0].value(World.new)).to eq([nil, "123"]) + args = expression.match("I have a bolt") + expect(args[0].value(World.new)).to eq('widget:bolt') end def match(expression, text) cucumber_expression = CucumberExpression.new(expression, ParameterTypeRegistry.new) args = cucumber_expression.match(text) return nil if args.nil? - args.map {|arg| arg.value(nil)} + args.map { |arg| arg.value(nil) } end end end diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_tokenizer_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_tokenizer_spec.rb new file mode 100644 index 0000000000..ce64229352 --- /dev/null +++ b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/cucumber_expression_tokenizer_spec.rb @@ -0,0 +1,24 @@ +require 'yaml' +require 'json' +require 'cucumber/cucumber_expressions/cucumber_expression_tokenizer' +require 'cucumber/cucumber_expressions/errors' + +module Cucumber + module CucumberExpressions + describe 'Cucumber expression tokenizer' do + Dir['testdata/tokens/*.yaml'].each do |testcase| + expectation = YAML.load_file(testcase) # encoding? + it "#{testcase}" do + tokenizer = CucumberExpressionTokenizer.new + if expectation['exception'].nil? + tokens = tokenizer.tokenize(expectation['expression']) + token_hashes = tokens.map{|token| token.to_hash} + expect(token_hashes).to eq(JSON.parse(expectation['expected'])) + else + expect { tokenizer.tokenize(expectation['expression']) }.to raise_error(expectation['exception']) + end + end + end + end + end +end diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb index 29f1780406..847155e465 100644 --- a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb +++ b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb @@ -69,7 +69,7 @@ def ==(other) true, false ) - end.to raise_error("Illegal character '[' in parameter name {[string]}") + end.to raise_error("Illegal character in parameter name {[string]}. Parameter names may not contain '[]()$.|?*+'") end describe CucumberExpression do diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_examples_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_examples_spec.rb index cc63e0942e..adaf020fa0 100644 --- a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_examples_spec.rb +++ b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_examples_spec.rb @@ -7,7 +7,7 @@ module Cucumber module CucumberExpressions describe 'examples.txt' do def match(expression_text, text) - expression = expression_text =~ /\/(.*)\// ? + expression = expression_text =~ /^\/(.*)\/$/ ? RegularExpression.new(Regexp.new($1), ParameterTypeRegistry.new) : CucumberExpression.new(expression_text, ParameterTypeRegistry.new) diff --git a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_factory_spec.rb b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_factory_spec.rb index e33188d2e9..167040cb4e 100644 --- a/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_factory_spec.rb +++ b/cucumber-expressions/ruby/spec/cucumber/cucumber_expressions/expression_factory_spec.rb @@ -14,10 +14,6 @@ module CucumberExpressions it 'creates a CucumberExpression' do expect(@expression_factory.create_expression('{int}').class).to eq(CucumberExpression) end - - it 'creates a XXXRegularExpression' do - expect {@expression_factory.create_expression('hello {x}')}.to raise_error("Undefined parameter type {x}") - end end end end diff --git a/cucumber-expressions/ruby/testdata/ast/alternation-followed-by-optional.yaml b/cucumber-expressions/ruby/testdata/ast/alternation-followed-by-optional.yaml new file mode 100644 index 0000000000..0bfb53a534 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation-followed-by-optional.yaml @@ -0,0 +1,17 @@ +expression: three blind\ rat/cat(s) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 16, "token": "blind rat"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 17, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 17, "end": 20, "token": "cat"}, + {"type": "OPTIONAL_NODE", "start": 20, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": "s"} + ]} + ]} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/alternation-phrase.yaml b/cucumber-expressions/ruby/testdata/ast/alternation-phrase.yaml new file mode 100644 index 0000000000..9a243822d5 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation-phrase.yaml @@ -0,0 +1,16 @@ +expression: three hungry/blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 12, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 12, "token": "hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 13, "end": 18, "token": "blind"} + ]} + ]}, + {"type": "TEXT_NODE", "start": 18, "end": 19, "token": " "}, + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "mice"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/alternation-with-parameter.yaml b/cucumber-expressions/ruby/testdata/ast/alternation-with-parameter.yaml new file mode 100644 index 0000000000..c5daf32bd7 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation-with-parameter.yaml @@ -0,0 +1,27 @@ +expression: I select the {int}st/nd/rd/th +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "I"}, + {"type": "TEXT_NODE", "start": 1, "end": 2, "token": " "}, + {"type": "TEXT_NODE", "start": 2, "end": 8, "token": "select"}, + {"type": "TEXT_NODE", "start": 8, "end": 9, "token": " "}, + {"type": "TEXT_NODE", "start": 9, "end": 12, "token": "the"}, + {"type": "TEXT_NODE", "start": 12, "end": 13, "token": " "}, + {"type": "PARAMETER_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 14, "end": 17, "token": "int"} + ]}, + {"type": "ALTERNATION_NODE", "start": 18, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 18, "end": 20, "nodes": [ + {"type": "TEXT_NODE", "start": 18, "end": 20, "token": "st"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 21, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 23, "token": "nd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 24, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 24, "end": 26, "token": "rd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 27, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 27, "end": 29, "token": "th"} + ]} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/alternation-with-unused-end-optional.yaml b/cucumber-expressions/ruby/testdata/ast/alternation-with-unused-end-optional.yaml new file mode 100644 index 0000000000..842838b75f --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation-with-unused-end-optional.yaml @@ -0,0 +1,15 @@ +expression: three )blind\ mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 7, "token": ")"}, + {"type": "TEXT_NODE", "start": 7, "end": 18, "token": "blind mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 19, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/alternation-with-unused-start-optional.yaml b/cucumber-expressions/ruby/testdata/ast/alternation-with-unused-start-optional.yaml new file mode 100644 index 0000000000..e2f0584556 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation-with-unused-start-optional.yaml @@ -0,0 +1,8 @@ +expression: three blind\ mice/rats( +exception: |- + This Cucumber Expression has a problem at column 23: + + three blind\ mice/rats( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/ruby/testdata/ast/alternation-with-white-space.yaml b/cucumber-expressions/ruby/testdata/ast/alternation-with-white-space.yaml new file mode 100644 index 0000000000..eedd57dd21 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation-with-white-space.yaml @@ -0,0 +1,12 @@ +expression: '\ three\ hungry/blind\ mice\ ' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 15, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 15, "token": " three hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 16, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 16, "end": 29, "token": "blind mice "} + ]} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/alternation.yaml b/cucumber-expressions/ruby/testdata/ast/alternation.yaml new file mode 100644 index 0000000000..88df8325fe --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/alternation.yaml @@ -0,0 +1,12 @@ +expression: mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 4, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 4, "token": "mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 5, "end": 9, "nodes": [ + {"type": "TEXT_NODE", "start": 5, "end": 9, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/anonymous-parameter.yaml b/cucumber-expressions/ruby/testdata/ast/anonymous-parameter.yaml new file mode 100644 index 0000000000..2c4d339333 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/anonymous-parameter.yaml @@ -0,0 +1,5 @@ +expression: "{}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 2, "nodes": []} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/closing-brace.yaml b/cucumber-expressions/ruby/testdata/ast/closing-brace.yaml new file mode 100644 index 0000000000..1bafd9c6a8 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/closing-brace.yaml @@ -0,0 +1,5 @@ +expression: "}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "}"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/closing-parenthesis.yaml b/cucumber-expressions/ruby/testdata/ast/closing-parenthesis.yaml new file mode 100644 index 0000000000..23daf7bcd3 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/closing-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: ) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": ")"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/empty-alternation.yaml b/cucumber-expressions/ruby/testdata/ast/empty-alternation.yaml new file mode 100644 index 0000000000..6d810fc8f3 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/empty-alternation.yaml @@ -0,0 +1,8 @@ +expression: / +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/empty-alternations.yaml b/cucumber-expressions/ruby/testdata/ast/empty-alternations.yaml new file mode 100644 index 0000000000..f8d4dd4cf8 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/empty-alternations.yaml @@ -0,0 +1,9 @@ +expression: '//' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 2, "end": 2, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/empty-string.yaml b/cucumber-expressions/ruby/testdata/ast/empty-string.yaml new file mode 100644 index 0000000000..4d33c2dc76 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/empty-string.yaml @@ -0,0 +1,3 @@ +expression: "" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 0, "nodes": []} diff --git a/cucumber-expressions/ruby/testdata/ast/escaped-alternation.yaml b/cucumber-expressions/ruby/testdata/ast/escaped-alternation.yaml new file mode 100644 index 0000000000..3ed9c37674 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/escaped-alternation.yaml @@ -0,0 +1,5 @@ +expression: 'mice\/rats' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 10, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 10, "token": "mice/rats"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/escaped-backslash.yaml b/cucumber-expressions/ruby/testdata/ast/escaped-backslash.yaml new file mode 100644 index 0000000000..da2d008e1e --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/escaped-backslash.yaml @@ -0,0 +1,5 @@ +expression: '\\' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "\\"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/escaped-opening-parenthesis.yaml b/cucumber-expressions/ruby/testdata/ast/escaped-opening-parenthesis.yaml new file mode 100644 index 0000000000..afafc59eb8 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/escaped-opening-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: '\(' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "("} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/escaped-optional-followed-by-optional.yaml b/cucumber-expressions/ruby/testdata/ast/escaped-optional-followed-by-optional.yaml new file mode 100644 index 0000000000..1e4746291b --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/escaped-optional-followed-by-optional.yaml @@ -0,0 +1,15 @@ +expression: three \((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 8, "token": "("}, + {"type": "OPTIONAL_NODE", "start": 8, "end": 14, "nodes": [ + {"type": "TEXT_NODE", "start": 9, "end": 13, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 20, "token": "blind"}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": ")"}, + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": " "}, + {"type": "TEXT_NODE", "start": 22, "end": 26, "token": "mice"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/escaped-optional-phrase.yaml b/cucumber-expressions/ruby/testdata/ast/escaped-optional-phrase.yaml new file mode 100644 index 0000000000..832249e2a7 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/escaped-optional-phrase.yaml @@ -0,0 +1,10 @@ +expression: three \(blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 19, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 13, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": ")"}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 19, "token": "mice"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/escaped-optional.yaml b/cucumber-expressions/ruby/testdata/ast/escaped-optional.yaml new file mode 100644 index 0000000000..4c2b457d6f --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: '\(blind)' + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 7, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 7, "end": 8, "token": ")"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/opening-brace.yaml b/cucumber-expressions/ruby/testdata/ast/opening-brace.yaml new file mode 100644 index 0000000000..916a674a36 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/opening-brace.yaml @@ -0,0 +1,8 @@ +expression: '{' +exception: |- + This Cucumber Expression has a problem at column 1: + + { + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/ruby/testdata/ast/opening-parenthesis.yaml b/cucumber-expressions/ruby/testdata/ast/opening-parenthesis.yaml new file mode 100644 index 0000000000..929d6ae304 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/opening-parenthesis.yaml @@ -0,0 +1,8 @@ +expression: ( +exception: |- + This Cucumber Expression has a problem at column 1: + + ( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/ruby/testdata/ast/optional-containing-nested-optional.yaml b/cucumber-expressions/ruby/testdata/ast/optional-containing-nested-optional.yaml new file mode 100644 index 0000000000..0fdd55d46b --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/optional-containing-nested-optional.yaml @@ -0,0 +1,15 @@ +expression: three ((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 25, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 20, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 7, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 8, "end": 12, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 19, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": " "}, + {"type": "TEXT_NODE", "start": 21, "end": 25, "token": "mice"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/optional-phrase.yaml b/cucumber-expressions/ruby/testdata/ast/optional-phrase.yaml new file mode 100644 index 0000000000..0ef4fb3f95 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/optional-phrase.yaml @@ -0,0 +1,12 @@ +expression: three (blind) mice + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 7, "end": 12, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 18, "token": "mice"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/optional.yaml b/cucumber-expressions/ruby/testdata/ast/optional.yaml new file mode 100644 index 0000000000..6ce2b632e7 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/optional.yaml @@ -0,0 +1,7 @@ +expression: (blind) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 6, "token": "blind"} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/parameter.yaml b/cucumber-expressions/ruby/testdata/ast/parameter.yaml new file mode 100644 index 0000000000..92ac8c147e --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/parameter.yaml @@ -0,0 +1,7 @@ +expression: "{string}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 7, "token": "string"} + ]} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/phrase.yaml b/cucumber-expressions/ruby/testdata/ast/phrase.yaml new file mode 100644 index 0000000000..ba340d0122 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/phrase.yaml @@ -0,0 +1,9 @@ +expression: three blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 11, "token": "blind"}, + {"type": "TEXT_NODE", "start": 11, "end": 12, "token": " "}, + {"type": "TEXT_NODE", "start": 12, "end": 16, "token": "mice"} + ]} diff --git a/cucumber-expressions/ruby/testdata/ast/unfinished-parameter.yaml b/cucumber-expressions/ruby/testdata/ast/unfinished-parameter.yaml new file mode 100644 index 0000000000..d02f9b4ccf --- /dev/null +++ b/cucumber-expressions/ruby/testdata/ast/unfinished-parameter.yaml @@ -0,0 +1,8 @@ +expression: "{string" +exception: |- + This Cucumber Expression has a problem at column 1: + + {string + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/ruby/testdata/expression/allows-escaped-optional-parameter-types.yaml b/cucumber-expressions/ruby/testdata/expression/allows-escaped-optional-parameter-types.yaml new file mode 100644 index 0000000000..a00b45acef --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/allows-escaped-optional-parameter-types.yaml @@ -0,0 +1,4 @@ +expression: \({int}) +text: (3) +expected: |- + [3] diff --git a/cucumber-expressions/ruby/testdata/expression/allows-parameter-type-in-alternation-1.yaml b/cucumber-expressions/ruby/testdata/expression/allows-parameter-type-in-alternation-1.yaml new file mode 100644 index 0000000000..bb1a6f21b1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/allows-parameter-type-in-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: i18n +expected: |- + [18] diff --git a/cucumber-expressions/ruby/testdata/expression/allows-parameter-type-in-alternation-2.yaml b/cucumber-expressions/ruby/testdata/expression/allows-parameter-type-in-alternation-2.yaml new file mode 100644 index 0000000000..cdddce7d84 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/allows-parameter-type-in-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: a11y +expected: |- + [11] diff --git a/cucumber-expressions/ruby/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml b/cucumber-expressions/ruby/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml new file mode 100644 index 0000000000..9e2ecdfbe1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml @@ -0,0 +1,5 @@ +expression: |- + {int}st/nd/rd/th +text: 3rd +expected: |- + [3] diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-in-optional.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-in-optional.yaml new file mode 100644 index 0000000000..b507e27220 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-in-optional.yaml @@ -0,0 +1,9 @@ +expression: three( brown/black) mice +text: three brown/black mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three( brown/black) mice + ^ + An alternation can not be used inside an optional. + You can use '\/' to escape the the '/' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml new file mode 100644 index 0000000000..b32540a4a9 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml @@ -0,0 +1,10 @@ +expression: |- + {int}/x +text: '3' +exception: |- + This Cucumber Expression has a problem at column 6: + + {int}/x + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml new file mode 100644 index 0000000000..a0aab0e5a9 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml @@ -0,0 +1,9 @@ +expression: three (brown)/black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (brown)/black mice + ^-----^ + An alternative may not exclusively contain optionals. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml new file mode 100644 index 0000000000..50250f00aa --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml @@ -0,0 +1,9 @@ +expression: x/{int} +text: '3' +exception: |- + This Cucumber Expression has a problem at column 3: + + x/{int} + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml new file mode 100644 index 0000000000..b724cfa77f --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml @@ -0,0 +1,9 @@ +expression: three brown//black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three brown//black mice + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-empty-optional.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-empty-optional.yaml new file mode 100644 index 0000000000..00e341af0b --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-empty-optional.yaml @@ -0,0 +1,9 @@ +expression: three () mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three () mice + ^^ + An optional must contain some text. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-nested-optional.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-nested-optional.yaml new file mode 100644 index 0000000000..017c3be25d --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-nested-optional.yaml @@ -0,0 +1,8 @@ +expression: "(a(b))" +exception: |- + This Cucumber Expression has a problem at column 3: + + (a(b)) + ^-^ + 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. diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-optional-parameter-types.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-optional-parameter-types.yaml new file mode 100644 index 0000000000..b88061e9b4 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-optional-parameter-types.yaml @@ -0,0 +1,9 @@ +expression: ({int}) +text: '3' +exception: |- + This Cucumber Expression has a problem at column 2: + + ({int}) + ^---^ + 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 '{' diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml new file mode 100644 index 0000000000..d1c89689e9 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml @@ -0,0 +1,10 @@ +expression: |- + {(string)} +text: something +exception: |- + This Cucumber Expression has a problem at column 2: + + {(string)} + ^ + Parameter names may not contain '{', '}', '(', ')', '\' or '/'. + Did you mean to use a regular expression? diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml new file mode 100644 index 0000000000..e033648972 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string\} mice +exception: |- + This Cucumber Expression has a problem at column 24: + + three (exceptionally\) {string\} mice + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml new file mode 100644 index 0000000000..7cb9c6d56a --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string} mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (exceptionally\) {string} mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml new file mode 100644 index 0000000000..029c4e63bd --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml @@ -0,0 +1,8 @@ +expression: three ((exceptionally\) strong) mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three ((exceptionally\) strong) mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/ruby/testdata/expression/does-not-match-misquoted-string.yaml b/cucumber-expressions/ruby/testdata/expression/does-not-match-misquoted-string.yaml new file mode 100644 index 0000000000..18023180af --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/does-not-match-misquoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind' mice +expected: |- + null diff --git a/cucumber-expressions/ruby/testdata/expression/doesnt-match-float-as-int.yaml b/cucumber-expressions/ruby/testdata/expression/doesnt-match-float-as-int.yaml new file mode 100644 index 0000000000..d66b586430 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/doesnt-match-float-as-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '1.22' +expected: |- + null diff --git a/cucumber-expressions/ruby/testdata/expression/matches-alternation.yaml b/cucumber-expressions/ruby/testdata/expression/matches-alternation.yaml new file mode 100644 index 0000000000..20a9b9a728 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-alternation.yaml @@ -0,0 +1,4 @@ +expression: mice/rats and rats\/mice +text: rats and rats/mice +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-anonymous-parameter-type.yaml b/cucumber-expressions/ruby/testdata/expression/matches-anonymous-parameter-type.yaml new file mode 100644 index 0000000000..fc954960df --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-anonymous-parameter-type.yaml @@ -0,0 +1,5 @@ +expression: |- + {} +text: '0.22' +expected: |- + ["0.22"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c3e1962e22 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "" and "handsome" mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..89315b62ef --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "" mice +expected: |- + [""] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml new file mode 100644 index 0000000000..fe30a044c1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "bl\"nd" mice +expected: |- + ["bl\"nd"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml new file mode 100644 index 0000000000..25fcf304c1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "'blind'" mice +expected: |- + ["'blind'"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string.yaml b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string.yaml new file mode 100644 index 0000000000..8b0560f332 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-double-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-parenthesis.yaml b/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-parenthesis.yaml new file mode 100644 index 0000000000..902a084103 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-parenthesis.yaml @@ -0,0 +1,4 @@ +expression: three \\(exceptionally) \\{string} mice +text: three \exceptionally \"blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-slash-1.yaml b/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-slash-1.yaml new file mode 100644 index 0000000000..94e531eca7 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-slash-1.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: 12\ +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-slash-2.yaml b/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-slash-2.yaml new file mode 100644 index 0000000000..9c9c735b86 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-doubly-escaped-slash-2.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: '2020' +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-1.yaml b/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-1.yaml new file mode 100644 index 0000000000..171df89ee1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-1.yaml @@ -0,0 +1,4 @@ +expression: three \(exceptionally) \{string} mice +text: three (exceptionally) {string} mice +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-2.yaml b/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-2.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-2.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-3.yaml b/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-3.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-escaped-parenthesis-3.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-escaped-slash.yaml b/cucumber-expressions/ruby/testdata/expression/matches-escaped-slash.yaml new file mode 100644 index 0000000000..d8b3933399 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-escaped-slash.yaml @@ -0,0 +1,4 @@ +expression: 12\/2020 +text: 12/2020 +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-float-1.yaml b/cucumber-expressions/ruby/testdata/expression/matches-float-1.yaml new file mode 100644 index 0000000000..fe7e8b1869 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-float-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '0.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-float-2.yaml b/cucumber-expressions/ruby/testdata/expression/matches-float-2.yaml new file mode 100644 index 0000000000..c1e5894eac --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-float-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-int.yaml b/cucumber-expressions/ruby/testdata/expression/matches-int.yaml new file mode 100644 index 0000000000..bcd3763886 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '22' +expected: |- + [22] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-multiple-double-quoted-strings.yaml b/cucumber-expressions/ruby/testdata/expression/matches-multiple-double-quoted-strings.yaml new file mode 100644 index 0000000000..6c74bc2350 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-multiple-double-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "blind" and "crippled" mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-multiple-single-quoted-strings.yaml b/cucumber-expressions/ruby/testdata/expression/matches-multiple-single-quoted-strings.yaml new file mode 100644 index 0000000000..5037821c14 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-multiple-single-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three 'blind' and 'crippled' mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-1.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-1.yaml new file mode 100644 index 0000000000..821776715b --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three brown mice +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-2.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-2.yaml new file mode 100644 index 0000000000..71b3a341f1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three rats +expected: |- + [] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml new file mode 100644 index 0000000000..2632f410ce --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 2 seconds? +expected: |- + [2] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml new file mode 100644 index 0000000000..7b30f667bc --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 1 second. +expected: |- + [1] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-1.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-1.yaml new file mode 100644 index 0000000000..6574bb4bdf --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 3 rats +expected: |- + [3] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-2.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-2.yaml new file mode 100644 index 0000000000..4eb0f0e1c6 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 2 mice +expected: |- + [2] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-3.yaml b/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-3.yaml new file mode 100644 index 0000000000..964fa6489e --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-optional-in-alternation-3.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 1 mouse +expected: |- + [1] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c963dcf1c7 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three '' and 'handsome' mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..cff2a2d1ec --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '' mice +expected: |- + [""] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml new file mode 100644 index 0000000000..eb9ed537cd --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '"blind"' mice +expected: |- + ["\"blind\""] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml new file mode 100644 index 0000000000..4c2b0055b9 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'bl\'nd' mice +expected: |- + ["bl'nd"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string.yaml b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string.yaml new file mode 100644 index 0000000000..6c8f4652a5 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-single-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'blind' mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/ruby/testdata/expression/matches-word.yaml b/cucumber-expressions/ruby/testdata/expression/matches-word.yaml new file mode 100644 index 0000000000..358fd3afd1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/matches-word.yaml @@ -0,0 +1,4 @@ +expression: three {word} mice +text: three blind mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/ruby/testdata/expression/throws-unknown-parameter-type.yaml b/cucumber-expressions/ruby/testdata/expression/throws-unknown-parameter-type.yaml new file mode 100644 index 0000000000..384e3a48c3 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/expression/throws-unknown-parameter-type.yaml @@ -0,0 +1,10 @@ +expression: |- + {unknown} +text: something +exception: |- + This Cucumber Expression has a problem at column 1: + + {unknown} + ^-------^ + Undefined parameter type 'unknown'. + Please register a ParameterType for 'unknown' diff --git a/cucumber-expressions/ruby/testdata/regex/alternation-with-optional.yaml b/cucumber-expressions/ruby/testdata/regex/alternation-with-optional.yaml new file mode 100644 index 0000000000..73787b2b0a --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/alternation-with-optional.yaml @@ -0,0 +1,2 @@ +expression: "a/b(c)" +expected: ^(?:a|b(?:c)?)$ diff --git a/cucumber-expressions/ruby/testdata/regex/alternation.yaml b/cucumber-expressions/ruby/testdata/regex/alternation.yaml new file mode 100644 index 0000000000..1dc293fb62 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/alternation.yaml @@ -0,0 +1,2 @@ +expression: "a/b c/d/e" +expected: ^(?:a|b) (?:c|d|e)$ diff --git a/cucumber-expressions/ruby/testdata/regex/empty.yaml b/cucumber-expressions/ruby/testdata/regex/empty.yaml new file mode 100644 index 0000000000..bb9a81906c --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/empty.yaml @@ -0,0 +1,2 @@ +expression: "" +expected: ^$ diff --git a/cucumber-expressions/ruby/testdata/regex/escape-regex-characters.yaml b/cucumber-expressions/ruby/testdata/regex/escape-regex-characters.yaml new file mode 100644 index 0000000000..c8ea8c549e --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/escape-regex-characters.yaml @@ -0,0 +1,2 @@ +expression: '^$[]\(\){}\\.|?*+' +expected: ^\^\$\[\]\(\)(.*)\\\.\|\?\*\+$ diff --git a/cucumber-expressions/ruby/testdata/regex/optional.yaml b/cucumber-expressions/ruby/testdata/regex/optional.yaml new file mode 100644 index 0000000000..7d6d84cc14 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/optional.yaml @@ -0,0 +1,2 @@ +expression: "(a)" +expected: ^(?:a)?$ diff --git a/cucumber-expressions/ruby/testdata/regex/parameter.yaml b/cucumber-expressions/ruby/testdata/regex/parameter.yaml new file mode 100644 index 0000000000..f793b21c0f --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/parameter.yaml @@ -0,0 +1,2 @@ +expression: "{int}" +expected: ^((?:-?\d+)|(?:\d+))$ diff --git a/cucumber-expressions/ruby/testdata/regex/text.yaml b/cucumber-expressions/ruby/testdata/regex/text.yaml new file mode 100644 index 0000000000..2af3e41664 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/text.yaml @@ -0,0 +1,2 @@ +expression: "a" +expected: ^a$ diff --git a/cucumber-expressions/ruby/testdata/regex/unicode.yaml b/cucumber-expressions/ruby/testdata/regex/unicode.yaml new file mode 100644 index 0000000000..f93fe35db1 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/regex/unicode.yaml @@ -0,0 +1,2 @@ +expression: "Привет, Мир(ы)!" +expected: ^Привет, Мир(?:ы)?!$ diff --git a/cucumber-expressions/ruby/testdata/tokens/alternation-phrase.yaml b/cucumber-expressions/ruby/testdata/tokens/alternation-phrase.yaml new file mode 100644 index 0000000000..48b107f64e --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/alternation-phrase.yaml @@ -0,0 +1,13 @@ +expression: three blind/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "ALTERNATION", "start": 11, "end": 12, "text": "/"}, + {"type": "TEXT", "start": 12, "end": 19, "text": "cripple"}, + {"type": "WHITE_SPACE", "start": 19, "end": 20, "text": " "}, + {"type": "TEXT", "start": 20, "end": 24, "text": "mice"}, + {"type": "END_OF_LINE", "start": 24, "end": 24, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/alternation.yaml b/cucumber-expressions/ruby/testdata/tokens/alternation.yaml new file mode 100644 index 0000000000..a4920f22e5 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/alternation.yaml @@ -0,0 +1,9 @@ +expression: blind/cripple +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "blind"}, + {"type": "ALTERNATION", "start": 5, "end": 6, "text": "/"}, + {"type": "TEXT", "start": 6, "end": 13, "text": "cripple"}, + {"type": "END_OF_LINE", "start": 13, "end": 13, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/empty-string.yaml b/cucumber-expressions/ruby/testdata/tokens/empty-string.yaml new file mode 100644 index 0000000000..501f7522f2 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/empty-string.yaml @@ -0,0 +1,6 @@ +expression: "" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "END_OF_LINE", "start": 0, "end": 0, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/escape-non-reserved-character.yaml b/cucumber-expressions/ruby/testdata/tokens/escape-non-reserved-character.yaml new file mode 100644 index 0000000000..5e206be084 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escape-non-reserved-character.yaml @@ -0,0 +1,8 @@ +expression: \[ +exception: |- + This Cucumber Expression has a problem at column 2: + + \[ + ^ + Only the characters '{', '}', '(', ')', '\', '/' and whitespace can be escaped. + If you did mean to use an '\' you can use '\\' to escape it diff --git a/cucumber-expressions/ruby/testdata/tokens/escaped-alternation.yaml b/cucumber-expressions/ruby/testdata/tokens/escaped-alternation.yaml new file mode 100644 index 0000000000..7e21f7ad19 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escaped-alternation.yaml @@ -0,0 +1,9 @@ +expression: blind\ and\ famished\/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 29, "text": "blind and famished/cripple"}, + {"type": "WHITE_SPACE", "start": 29, "end": 30, "text": " "}, + {"type": "TEXT", "start": 30, "end": 34, "text": "mice"}, + {"type": "END_OF_LINE", "start": 34, "end": 34, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml b/cucumber-expressions/ruby/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml new file mode 100644 index 0000000000..6375ad52a5 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml @@ -0,0 +1,9 @@ +expression: ' \/ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "WHITE_SPACE", "start": 0, "end": 1, "text": " "}, + {"type": "TEXT", "start": 1, "end": 3, "text": "/"}, + {"type": "WHITE_SPACE", "start": 3, "end": 4, "text": " "}, + {"type": "END_OF_LINE", "start": 4, "end": 4, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/escaped-end-of-line.yaml b/cucumber-expressions/ruby/testdata/tokens/escaped-end-of-line.yaml new file mode 100644 index 0000000000..a1bd00fd98 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escaped-end-of-line.yaml @@ -0,0 +1,8 @@ +expression: \ +exception: |- + This Cucumber Expression has a problem at column 1: + + \ + ^ + The end of line can not be escaped. + You can use '\\' to escape the the '\' diff --git a/cucumber-expressions/ruby/testdata/tokens/escaped-optional.yaml b/cucumber-expressions/ruby/testdata/tokens/escaped-optional.yaml new file mode 100644 index 0000000000..2b365b581c --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: \(blind\) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 9, "text": "(blind)"}, + {"type": "END_OF_LINE", "start": 9, "end": 9, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/escaped-parameter.yaml b/cucumber-expressions/ruby/testdata/tokens/escaped-parameter.yaml new file mode 100644 index 0000000000..2cdac4f35a --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escaped-parameter.yaml @@ -0,0 +1,7 @@ +expression: \{string\} +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 10, "text": "{string}"}, + {"type": "END_OF_LINE", "start": 10, "end": 10, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/escaped-space.yaml b/cucumber-expressions/ruby/testdata/tokens/escaped-space.yaml new file mode 100644 index 0000000000..912827a941 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/escaped-space.yaml @@ -0,0 +1,7 @@ +expression: '\ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 2, "text": " "}, + {"type": "END_OF_LINE", "start": 2, "end": 2, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/optional-phrase.yaml b/cucumber-expressions/ruby/testdata/tokens/optional-phrase.yaml new file mode 100644 index 0000000000..2ddc6bb502 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/optional-phrase.yaml @@ -0,0 +1,13 @@ +expression: three (blind) mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_OPTIONAL", "start": 6, "end": 7, "text": "("}, + {"type": "TEXT", "start": 7, "end": 12, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 12, "end": 13, "text": ")"}, + {"type": "WHITE_SPACE", "start": 13, "end": 14, "text": " "}, + {"type": "TEXT", "start": 14, "end": 18, "text": "mice"}, + {"type": "END_OF_LINE", "start": 18, "end": 18, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/optional.yaml b/cucumber-expressions/ruby/testdata/tokens/optional.yaml new file mode 100644 index 0000000000..35b1474a7c --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/optional.yaml @@ -0,0 +1,9 @@ +expression: (blind) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_OPTIONAL", "start": 0, "end": 1, "text": "("}, + {"type": "TEXT", "start": 1, "end": 6, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 6, "end": 7, "text": ")"}, + {"type": "END_OF_LINE", "start": 7, "end": 7, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/parameter-phrase.yaml b/cucumber-expressions/ruby/testdata/tokens/parameter-phrase.yaml new file mode 100644 index 0000000000..5e98055ee6 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/parameter-phrase.yaml @@ -0,0 +1,13 @@ +expression: three {string} mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_PARAMETER", "start": 6, "end": 7, "text": "{"}, + {"type": "TEXT", "start": 7, "end": 13, "text": "string"}, + {"type": "END_PARAMETER", "start": 13, "end": 14, "text": "}"}, + {"type": "WHITE_SPACE", "start": 14, "end": 15, "text": " "}, + {"type": "TEXT", "start": 15, "end": 19, "text": "mice"}, + {"type": "END_OF_LINE", "start": 19, "end": 19, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/parameter.yaml b/cucumber-expressions/ruby/testdata/tokens/parameter.yaml new file mode 100644 index 0000000000..460363c393 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/parameter.yaml @@ -0,0 +1,9 @@ +expression: "{string}" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_PARAMETER", "start": 0, "end": 1, "text": "{"}, + {"type": "TEXT", "start": 1, "end": 7, "text": "string"}, + {"type": "END_PARAMETER", "start": 7, "end": 8, "text": "}"}, + {"type": "END_OF_LINE", "start": 8, "end": 8, "text": ""} + ] diff --git a/cucumber-expressions/ruby/testdata/tokens/phrase.yaml b/cucumber-expressions/ruby/testdata/tokens/phrase.yaml new file mode 100644 index 0000000000..e2cfccf7b4 --- /dev/null +++ b/cucumber-expressions/ruby/testdata/tokens/phrase.yaml @@ -0,0 +1,11 @@ +expression: three blind mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "WHITE_SPACE", "start": 11, "end": 12, "text": " "}, + {"type": "TEXT", "start": 12, "end": 16, "text": "mice"}, + {"type": "END_OF_LINE", "start": 16, "end": 16, "text": ""} + ] diff --git a/cucumber-expressions/testdata/ast/alternation-followed-by-optional.yaml b/cucumber-expressions/testdata/ast/alternation-followed-by-optional.yaml new file mode 100644 index 0000000000..0bfb53a534 --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation-followed-by-optional.yaml @@ -0,0 +1,17 @@ +expression: three blind\ rat/cat(s) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 16, "token": "blind rat"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 17, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 17, "end": 20, "token": "cat"}, + {"type": "OPTIONAL_NODE", "start": 20, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": "s"} + ]} + ]} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/alternation-phrase.yaml b/cucumber-expressions/testdata/ast/alternation-phrase.yaml new file mode 100644 index 0000000000..9a243822d5 --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation-phrase.yaml @@ -0,0 +1,16 @@ +expression: three hungry/blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 12, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 12, "token": "hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 13, "end": 18, "token": "blind"} + ]} + ]}, + {"type": "TEXT_NODE", "start": 18, "end": 19, "token": " "}, + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "mice"} + ]} diff --git a/cucumber-expressions/testdata/ast/alternation-with-parameter.yaml b/cucumber-expressions/testdata/ast/alternation-with-parameter.yaml new file mode 100644 index 0000000000..c5daf32bd7 --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation-with-parameter.yaml @@ -0,0 +1,27 @@ +expression: I select the {int}st/nd/rd/th +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "I"}, + {"type": "TEXT_NODE", "start": 1, "end": 2, "token": " "}, + {"type": "TEXT_NODE", "start": 2, "end": 8, "token": "select"}, + {"type": "TEXT_NODE", "start": 8, "end": 9, "token": " "}, + {"type": "TEXT_NODE", "start": 9, "end": 12, "token": "the"}, + {"type": "TEXT_NODE", "start": 12, "end": 13, "token": " "}, + {"type": "PARAMETER_NODE", "start": 13, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 14, "end": 17, "token": "int"} + ]}, + {"type": "ALTERNATION_NODE", "start": 18, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 18, "end": 20, "nodes": [ + {"type": "TEXT_NODE", "start": 18, "end": 20, "token": "st"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 21, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 21, "end": 23, "token": "nd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 24, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 24, "end": 26, "token": "rd"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 27, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 27, "end": 29, "token": "th"} + ]} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/alternation-with-unused-end-optional.yaml b/cucumber-expressions/testdata/ast/alternation-with-unused-end-optional.yaml new file mode 100644 index 0000000000..842838b75f --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation-with-unused-end-optional.yaml @@ -0,0 +1,15 @@ +expression: three )blind\ mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "ALTERNATION_NODE", "start": 6, "end": 23, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 6, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 6, "end": 7, "token": ")"}, + {"type": "TEXT_NODE", "start": 7, "end": 18, "token": "blind mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 19, "end": 23, "nodes": [ + {"type": "TEXT_NODE", "start": 19, "end": 23, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/alternation-with-unused-start-optional.yaml b/cucumber-expressions/testdata/ast/alternation-with-unused-start-optional.yaml new file mode 100644 index 0000000000..e2f0584556 --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation-with-unused-start-optional.yaml @@ -0,0 +1,8 @@ +expression: three blind\ mice/rats( +exception: |- + This Cucumber Expression has a problem at column 23: + + three blind\ mice/rats( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/testdata/ast/alternation-with-white-space.yaml b/cucumber-expressions/testdata/ast/alternation-with-white-space.yaml new file mode 100644 index 0000000000..eedd57dd21 --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation-with-white-space.yaml @@ -0,0 +1,12 @@ +expression: '\ three\ hungry/blind\ mice\ ' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 29, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 15, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 15, "token": " three hungry"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 16, "end": 29, "nodes": [ + {"type": "TEXT_NODE", "start": 16, "end": 29, "token": "blind mice "} + ]} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/alternation.yaml b/cucumber-expressions/testdata/ast/alternation.yaml new file mode 100644 index 0000000000..88df8325fe --- /dev/null +++ b/cucumber-expressions/testdata/ast/alternation.yaml @@ -0,0 +1,12 @@ +expression: mice/rats +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 9, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 4, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 4, "token": "mice"} + ]}, + {"type": "ALTERNATIVE_NODE", "start": 5, "end": 9, "nodes": [ + {"type": "TEXT_NODE", "start": 5, "end": 9, "token": "rats"} + ]} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/anonymous-parameter.yaml b/cucumber-expressions/testdata/ast/anonymous-parameter.yaml new file mode 100644 index 0000000000..2c4d339333 --- /dev/null +++ b/cucumber-expressions/testdata/ast/anonymous-parameter.yaml @@ -0,0 +1,5 @@ +expression: "{}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 2, "nodes": []} + ]} diff --git a/cucumber-expressions/testdata/ast/closing-brace.yaml b/cucumber-expressions/testdata/ast/closing-brace.yaml new file mode 100644 index 0000000000..1bafd9c6a8 --- /dev/null +++ b/cucumber-expressions/testdata/ast/closing-brace.yaml @@ -0,0 +1,5 @@ +expression: "}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": "}"} + ]} diff --git a/cucumber-expressions/testdata/ast/closing-parenthesis.yaml b/cucumber-expressions/testdata/ast/closing-parenthesis.yaml new file mode 100644 index 0000000000..23daf7bcd3 --- /dev/null +++ b/cucumber-expressions/testdata/ast/closing-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: ) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 1, "token": ")"} + ]} diff --git a/cucumber-expressions/testdata/ast/empty-alternation.yaml b/cucumber-expressions/testdata/ast/empty-alternation.yaml new file mode 100644 index 0000000000..6d810fc8f3 --- /dev/null +++ b/cucumber-expressions/testdata/ast/empty-alternation.yaml @@ -0,0 +1,8 @@ +expression: / +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 1, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/empty-alternations.yaml b/cucumber-expressions/testdata/ast/empty-alternations.yaml new file mode 100644 index 0000000000..f8d4dd4cf8 --- /dev/null +++ b/cucumber-expressions/testdata/ast/empty-alternations.yaml @@ -0,0 +1,9 @@ +expression: '//' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "ALTERNATIVE_NODE", "start": 0, "end": 0, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 1, "end": 1, "nodes": []}, + {"type": "ALTERNATIVE_NODE", "start": 2, "end": 2, "nodes": []} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/empty-string.yaml b/cucumber-expressions/testdata/ast/empty-string.yaml new file mode 100644 index 0000000000..4d33c2dc76 --- /dev/null +++ b/cucumber-expressions/testdata/ast/empty-string.yaml @@ -0,0 +1,3 @@ +expression: "" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 0, "nodes": []} diff --git a/cucumber-expressions/testdata/ast/escaped-alternation.yaml b/cucumber-expressions/testdata/ast/escaped-alternation.yaml new file mode 100644 index 0000000000..3ed9c37674 --- /dev/null +++ b/cucumber-expressions/testdata/ast/escaped-alternation.yaml @@ -0,0 +1,5 @@ +expression: 'mice\/rats' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 10, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 10, "token": "mice/rats"} + ]} diff --git a/cucumber-expressions/testdata/ast/escaped-backslash.yaml b/cucumber-expressions/testdata/ast/escaped-backslash.yaml new file mode 100644 index 0000000000..da2d008e1e --- /dev/null +++ b/cucumber-expressions/testdata/ast/escaped-backslash.yaml @@ -0,0 +1,5 @@ +expression: '\\' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "\\"} + ]} diff --git a/cucumber-expressions/testdata/ast/escaped-opening-parenthesis.yaml b/cucumber-expressions/testdata/ast/escaped-opening-parenthesis.yaml new file mode 100644 index 0000000000..afafc59eb8 --- /dev/null +++ b/cucumber-expressions/testdata/ast/escaped-opening-parenthesis.yaml @@ -0,0 +1,5 @@ +expression: '\(' +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 2, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 2, "token": "("} + ]} diff --git a/cucumber-expressions/testdata/ast/escaped-optional-followed-by-optional.yaml b/cucumber-expressions/testdata/ast/escaped-optional-followed-by-optional.yaml new file mode 100644 index 0000000000..1e4746291b --- /dev/null +++ b/cucumber-expressions/testdata/ast/escaped-optional-followed-by-optional.yaml @@ -0,0 +1,15 @@ +expression: three \((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 26, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 8, "token": "("}, + {"type": "OPTIONAL_NODE", "start": 8, "end": 14, "nodes": [ + {"type": "TEXT_NODE", "start": 9, "end": 13, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 20, "token": "blind"}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": ")"}, + {"type": "TEXT_NODE", "start": 21, "end": 22, "token": " "}, + {"type": "TEXT_NODE", "start": 22, "end": 26, "token": "mice"} + ]} diff --git a/cucumber-expressions/testdata/ast/escaped-optional-phrase.yaml b/cucumber-expressions/testdata/ast/escaped-optional-phrase.yaml new file mode 100644 index 0000000000..832249e2a7 --- /dev/null +++ b/cucumber-expressions/testdata/ast/escaped-optional-phrase.yaml @@ -0,0 +1,10 @@ +expression: three \(blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 19, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 13, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": ")"}, + {"type": "TEXT_NODE", "start": 14, "end": 15, "token": " "}, + {"type": "TEXT_NODE", "start": 15, "end": 19, "token": "mice"} + ]} diff --git a/cucumber-expressions/testdata/ast/escaped-optional.yaml b/cucumber-expressions/testdata/ast/escaped-optional.yaml new file mode 100644 index 0000000000..4c2b457d6f --- /dev/null +++ b/cucumber-expressions/testdata/ast/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: '\(blind)' + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 7, "token": "(blind"}, + {"type": "TEXT_NODE", "start": 7, "end": 8, "token": ")"} + ]} diff --git a/cucumber-expressions/testdata/ast/opening-brace.yaml b/cucumber-expressions/testdata/ast/opening-brace.yaml new file mode 100644 index 0000000000..916a674a36 --- /dev/null +++ b/cucumber-expressions/testdata/ast/opening-brace.yaml @@ -0,0 +1,8 @@ +expression: '{' +exception: |- + This Cucumber Expression has a problem at column 1: + + { + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/testdata/ast/opening-parenthesis.yaml b/cucumber-expressions/testdata/ast/opening-parenthesis.yaml new file mode 100644 index 0000000000..929d6ae304 --- /dev/null +++ b/cucumber-expressions/testdata/ast/opening-parenthesis.yaml @@ -0,0 +1,8 @@ +expression: ( +exception: |- + This Cucumber Expression has a problem at column 1: + + ( + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/testdata/ast/optional-containing-nested-optional.yaml b/cucumber-expressions/testdata/ast/optional-containing-nested-optional.yaml new file mode 100644 index 0000000000..0fdd55d46b --- /dev/null +++ b/cucumber-expressions/testdata/ast/optional-containing-nested-optional.yaml @@ -0,0 +1,15 @@ +expression: three ((very) blind) mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 25, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 20, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 7, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 8, "end": 12, "token": "very"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 19, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 20, "end": 21, "token": " "}, + {"type": "TEXT_NODE", "start": 21, "end": 25, "token": "mice"} + ]} diff --git a/cucumber-expressions/testdata/ast/optional-phrase.yaml b/cucumber-expressions/testdata/ast/optional-phrase.yaml new file mode 100644 index 0000000000..0ef4fb3f95 --- /dev/null +++ b/cucumber-expressions/testdata/ast/optional-phrase.yaml @@ -0,0 +1,12 @@ +expression: three (blind) mice + +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 18, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "OPTIONAL_NODE", "start": 6, "end": 13, "nodes": [ + {"type": "TEXT_NODE", "start": 7, "end": 12, "token": "blind"} + ]}, + {"type": "TEXT_NODE", "start": 13, "end": 14, "token": " "}, + {"type": "TEXT_NODE", "start": 14, "end": 18, "token": "mice"} + ]} diff --git a/cucumber-expressions/testdata/ast/optional.yaml b/cucumber-expressions/testdata/ast/optional.yaml new file mode 100644 index 0000000000..6ce2b632e7 --- /dev/null +++ b/cucumber-expressions/testdata/ast/optional.yaml @@ -0,0 +1,7 @@ +expression: (blind) +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "OPTIONAL_NODE", "start": 0, "end": 7, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 6, "token": "blind"} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/parameter.yaml b/cucumber-expressions/testdata/ast/parameter.yaml new file mode 100644 index 0000000000..92ac8c147e --- /dev/null +++ b/cucumber-expressions/testdata/ast/parameter.yaml @@ -0,0 +1,7 @@ +expression: "{string}" +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "PARAMETER_NODE", "start": 0, "end": 8, "nodes": [ + {"type": "TEXT_NODE", "start": 1, "end": 7, "token": "string"} + ]} + ]} diff --git a/cucumber-expressions/testdata/ast/phrase.yaml b/cucumber-expressions/testdata/ast/phrase.yaml new file mode 100644 index 0000000000..ba340d0122 --- /dev/null +++ b/cucumber-expressions/testdata/ast/phrase.yaml @@ -0,0 +1,9 @@ +expression: three blind mice +expected: |- + {"type": "EXPRESSION_NODE", "start": 0, "end": 16, "nodes": [ + {"type": "TEXT_NODE", "start": 0, "end": 5, "token": "three"}, + {"type": "TEXT_NODE", "start": 5, "end": 6, "token": " "}, + {"type": "TEXT_NODE", "start": 6, "end": 11, "token": "blind"}, + {"type": "TEXT_NODE", "start": 11, "end": 12, "token": " "}, + {"type": "TEXT_NODE", "start": 12, "end": 16, "token": "mice"} + ]} diff --git a/cucumber-expressions/testdata/ast/unfinished-parameter.yaml b/cucumber-expressions/testdata/ast/unfinished-parameter.yaml new file mode 100644 index 0000000000..d02f9b4ccf --- /dev/null +++ b/cucumber-expressions/testdata/ast/unfinished-parameter.yaml @@ -0,0 +1,8 @@ +expression: "{string" +exception: |- + This Cucumber Expression has a problem at column 1: + + {string + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/testdata/expression/allows-escaped-optional-parameter-types.yaml b/cucumber-expressions/testdata/expression/allows-escaped-optional-parameter-types.yaml new file mode 100644 index 0000000000..a00b45acef --- /dev/null +++ b/cucumber-expressions/testdata/expression/allows-escaped-optional-parameter-types.yaml @@ -0,0 +1,4 @@ +expression: \({int}) +text: (3) +expected: |- + [3] diff --git a/cucumber-expressions/testdata/expression/allows-parameter-type-in-alternation-1.yaml b/cucumber-expressions/testdata/expression/allows-parameter-type-in-alternation-1.yaml new file mode 100644 index 0000000000..bb1a6f21b1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/allows-parameter-type-in-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: i18n +expected: |- + [18] diff --git a/cucumber-expressions/testdata/expression/allows-parameter-type-in-alternation-2.yaml b/cucumber-expressions/testdata/expression/allows-parameter-type-in-alternation-2.yaml new file mode 100644 index 0000000000..cdddce7d84 --- /dev/null +++ b/cucumber-expressions/testdata/expression/allows-parameter-type-in-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: a/i{int}n/y +text: a11y +expected: |- + [11] diff --git a/cucumber-expressions/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml b/cucumber-expressions/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml new file mode 100644 index 0000000000..9e2ecdfbe1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml @@ -0,0 +1,5 @@ +expression: |- + {int}st/nd/rd/th +text: 3rd +expected: |- + [3] diff --git a/cucumber-expressions/testdata/expression/does-not-allow-alternation-in-optional.yaml b/cucumber-expressions/testdata/expression/does-not-allow-alternation-in-optional.yaml new file mode 100644 index 0000000000..b507e27220 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-alternation-in-optional.yaml @@ -0,0 +1,9 @@ +expression: three( brown/black) mice +text: three brown/black mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three( brown/black) mice + ^ + An alternation can not be used inside an optional. + You can use '\/' to escape the the '/' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml new file mode 100644 index 0000000000..b32540a4a9 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml @@ -0,0 +1,10 @@ +expression: |- + {int}/x +text: '3' +exception: |- + This Cucumber Expression has a problem at column 6: + + {int}/x + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml new file mode 100644 index 0000000000..a0aab0e5a9 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml @@ -0,0 +1,9 @@ +expression: three (brown)/black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (brown)/black mice + ^-----^ + An alternative may not exclusively contain optionals. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml new file mode 100644 index 0000000000..50250f00aa --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml @@ -0,0 +1,9 @@ +expression: x/{int} +text: '3' +exception: |- + This Cucumber Expression has a problem at column 3: + + x/{int} + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml new file mode 100644 index 0000000000..b724cfa77f --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml @@ -0,0 +1,9 @@ +expression: three brown//black mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 13: + + three brown//black mice + ^ + Alternative may not be empty. + If you did not mean to use an alternative you can use '\/' to escape the the '/' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-empty-optional.yaml b/cucumber-expressions/testdata/expression/does-not-allow-empty-optional.yaml new file mode 100644 index 0000000000..00e341af0b --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-empty-optional.yaml @@ -0,0 +1,9 @@ +expression: three () mice +text: three brown mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three () mice + ^^ + An optional must contain some text. + If you did not mean to use an optional you can use '\(' to escape the the '(' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-nested-optional.yaml b/cucumber-expressions/testdata/expression/does-not-allow-nested-optional.yaml new file mode 100644 index 0000000000..017c3be25d --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-nested-optional.yaml @@ -0,0 +1,8 @@ +expression: "(a(b))" +exception: |- + This Cucumber Expression has a problem at column 3: + + (a(b)) + ^-^ + 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. diff --git a/cucumber-expressions/testdata/expression/does-not-allow-optional-parameter-types.yaml b/cucumber-expressions/testdata/expression/does-not-allow-optional-parameter-types.yaml new file mode 100644 index 0000000000..b88061e9b4 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-optional-parameter-types.yaml @@ -0,0 +1,9 @@ +expression: ({int}) +text: '3' +exception: |- + This Cucumber Expression has a problem at column 2: + + ({int}) + ^---^ + 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 '{' diff --git a/cucumber-expressions/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml b/cucumber-expressions/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml new file mode 100644 index 0000000000..d1c89689e9 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml @@ -0,0 +1,10 @@ +expression: |- + {(string)} +text: something +exception: |- + This Cucumber Expression has a problem at column 2: + + {(string)} + ^ + Parameter names may not contain '{', '}', '(', ')', '\' or '/'. + Did you mean to use a regular expression? diff --git a/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml b/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml new file mode 100644 index 0000000000..e033648972 --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string\} mice +exception: |- + This Cucumber Expression has a problem at column 24: + + three (exceptionally\) {string\} mice + ^ + The '{' does not have a matching '}'. + If you did not intend to use a parameter you can use '\{' to escape the a parameter diff --git a/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml b/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml new file mode 100644 index 0000000000..7cb9c6d56a --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml @@ -0,0 +1,8 @@ +expression: three (exceptionally\) {string} mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three (exceptionally\) {string} mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml b/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml new file mode 100644 index 0000000000..029c4e63bd --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml @@ -0,0 +1,8 @@ +expression: three ((exceptionally\) strong) mice +exception: |- + This Cucumber Expression has a problem at column 7: + + three ((exceptionally\) strong) mice + ^ + The '(' does not have a matching ')'. + If you did not intend to use optional text you can use '\(' to escape the optional text diff --git a/cucumber-expressions/testdata/expression/does-not-match-misquoted-string.yaml b/cucumber-expressions/testdata/expression/does-not-match-misquoted-string.yaml new file mode 100644 index 0000000000..18023180af --- /dev/null +++ b/cucumber-expressions/testdata/expression/does-not-match-misquoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind' mice +expected: |- + null diff --git a/cucumber-expressions/testdata/expression/doesnt-match-float-as-int.yaml b/cucumber-expressions/testdata/expression/doesnt-match-float-as-int.yaml new file mode 100644 index 0000000000..d66b586430 --- /dev/null +++ b/cucumber-expressions/testdata/expression/doesnt-match-float-as-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '1.22' +expected: |- + null diff --git a/cucumber-expressions/testdata/expression/matches-alternation.yaml b/cucumber-expressions/testdata/expression/matches-alternation.yaml new file mode 100644 index 0000000000..20a9b9a728 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-alternation.yaml @@ -0,0 +1,4 @@ +expression: mice/rats and rats\/mice +text: rats and rats/mice +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-anonymous-parameter-type.yaml b/cucumber-expressions/testdata/expression/matches-anonymous-parameter-type.yaml new file mode 100644 index 0000000000..fc954960df --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-anonymous-parameter-type.yaml @@ -0,0 +1,5 @@ +expression: |- + {} +text: '0.22' +expected: |- + ["0.22"] diff --git a/cucumber-expressions/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c3e1962e22 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "" and "handsome" mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..89315b62ef --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "" mice +expected: |- + [""] diff --git a/cucumber-expressions/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml b/cucumber-expressions/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml new file mode 100644 index 0000000000..fe30a044c1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "bl\"nd" mice +expected: |- + ["bl\"nd"] diff --git a/cucumber-expressions/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml b/cucumber-expressions/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml new file mode 100644 index 0000000000..25fcf304c1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "'blind'" mice +expected: |- + ["'blind'"] diff --git a/cucumber-expressions/testdata/expression/matches-double-quoted-string.yaml b/cucumber-expressions/testdata/expression/matches-double-quoted-string.yaml new file mode 100644 index 0000000000..8b0560f332 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-double-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three "blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/testdata/expression/matches-doubly-escaped-parenthesis.yaml b/cucumber-expressions/testdata/expression/matches-doubly-escaped-parenthesis.yaml new file mode 100644 index 0000000000..902a084103 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-doubly-escaped-parenthesis.yaml @@ -0,0 +1,4 @@ +expression: three \\(exceptionally) \\{string} mice +text: three \exceptionally \"blind" mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/testdata/expression/matches-doubly-escaped-slash-1.yaml b/cucumber-expressions/testdata/expression/matches-doubly-escaped-slash-1.yaml new file mode 100644 index 0000000000..94e531eca7 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-doubly-escaped-slash-1.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: 12\ +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-doubly-escaped-slash-2.yaml b/cucumber-expressions/testdata/expression/matches-doubly-escaped-slash-2.yaml new file mode 100644 index 0000000000..9c9c735b86 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-doubly-escaped-slash-2.yaml @@ -0,0 +1,4 @@ +expression: 12\\/2020 +text: '2020' +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-1.yaml b/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-1.yaml new file mode 100644 index 0000000000..171df89ee1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-1.yaml @@ -0,0 +1,4 @@ +expression: three \(exceptionally) \{string} mice +text: three (exceptionally) {string} mice +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-2.yaml b/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-2.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-2.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-3.yaml b/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-3.yaml new file mode 100644 index 0000000000..340c63e94f --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-escaped-parenthesis-3.yaml @@ -0,0 +1,4 @@ +expression: three \((exceptionally)) \{{string}} mice +text: three (exceptionally) {"blind"} mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/testdata/expression/matches-escaped-slash.yaml b/cucumber-expressions/testdata/expression/matches-escaped-slash.yaml new file mode 100644 index 0000000000..d8b3933399 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-escaped-slash.yaml @@ -0,0 +1,4 @@ +expression: 12\/2020 +text: 12/2020 +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-float-1.yaml b/cucumber-expressions/testdata/expression/matches-float-1.yaml new file mode 100644 index 0000000000..fe7e8b1869 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-float-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '0.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/testdata/expression/matches-float-2.yaml b/cucumber-expressions/testdata/expression/matches-float-2.yaml new file mode 100644 index 0000000000..c1e5894eac --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-float-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {float} +text: '.22' +expected: |- + [0.22] diff --git a/cucumber-expressions/testdata/expression/matches-int.yaml b/cucumber-expressions/testdata/expression/matches-int.yaml new file mode 100644 index 0000000000..bcd3763886 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-int.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} +text: '22' +expected: |- + [22] diff --git a/cucumber-expressions/testdata/expression/matches-multiple-double-quoted-strings.yaml b/cucumber-expressions/testdata/expression/matches-multiple-double-quoted-strings.yaml new file mode 100644 index 0000000000..6c74bc2350 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-multiple-double-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three "blind" and "crippled" mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/testdata/expression/matches-multiple-single-quoted-strings.yaml b/cucumber-expressions/testdata/expression/matches-multiple-single-quoted-strings.yaml new file mode 100644 index 0000000000..5037821c14 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-multiple-single-quoted-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three 'blind' and 'crippled' mice +expected: |- + ["blind","crippled"] diff --git a/cucumber-expressions/testdata/expression/matches-optional-before-alternation-1.yaml b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-1.yaml new file mode 100644 index 0000000000..821776715b --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-1.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three brown mice +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-optional-before-alternation-2.yaml b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-2.yaml new file mode 100644 index 0000000000..71b3a341f1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-2.yaml @@ -0,0 +1,4 @@ +expression: three (brown )mice/rats +text: three rats +expected: |- + [] diff --git a/cucumber-expressions/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml new file mode 100644 index 0000000000..2632f410ce --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 2 seconds? +expected: |- + [2] diff --git a/cucumber-expressions/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml new file mode 100644 index 0000000000..7b30f667bc --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml @@ -0,0 +1,4 @@ +expression: I wait {int} second(s)./second(s)? +text: I wait 1 second. +expected: |- + [1] diff --git a/cucumber-expressions/testdata/expression/matches-optional-in-alternation-1.yaml b/cucumber-expressions/testdata/expression/matches-optional-in-alternation-1.yaml new file mode 100644 index 0000000000..6574bb4bdf --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-in-alternation-1.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 3 rats +expected: |- + [3] diff --git a/cucumber-expressions/testdata/expression/matches-optional-in-alternation-2.yaml b/cucumber-expressions/testdata/expression/matches-optional-in-alternation-2.yaml new file mode 100644 index 0000000000..4eb0f0e1c6 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-in-alternation-2.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 2 mice +expected: |- + [2] diff --git a/cucumber-expressions/testdata/expression/matches-optional-in-alternation-3.yaml b/cucumber-expressions/testdata/expression/matches-optional-in-alternation-3.yaml new file mode 100644 index 0000000000..964fa6489e --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-optional-in-alternation-3.yaml @@ -0,0 +1,5 @@ +expression: |- + {int} rat(s)/mouse/mice +text: 1 mouse +expected: |- + [1] diff --git a/cucumber-expressions/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml b/cucumber-expressions/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml new file mode 100644 index 0000000000..c963dcf1c7 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml @@ -0,0 +1,4 @@ +expression: three {string} and {string} mice +text: three '' and 'handsome' mice +expected: |- + ["","handsome"] diff --git a/cucumber-expressions/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml b/cucumber-expressions/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml new file mode 100644 index 0000000000..cff2a2d1ec --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '' mice +expected: |- + [""] diff --git a/cucumber-expressions/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml b/cucumber-expressions/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml new file mode 100644 index 0000000000..eb9ed537cd --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three '"blind"' mice +expected: |- + ["\"blind\""] diff --git a/cucumber-expressions/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml b/cucumber-expressions/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml new file mode 100644 index 0000000000..4c2b0055b9 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'bl\'nd' mice +expected: |- + ["bl'nd"] diff --git a/cucumber-expressions/testdata/expression/matches-single-quoted-string.yaml b/cucumber-expressions/testdata/expression/matches-single-quoted-string.yaml new file mode 100644 index 0000000000..6c8f4652a5 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-single-quoted-string.yaml @@ -0,0 +1,4 @@ +expression: three {string} mice +text: three 'blind' mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/testdata/expression/matches-word.yaml b/cucumber-expressions/testdata/expression/matches-word.yaml new file mode 100644 index 0000000000..358fd3afd1 --- /dev/null +++ b/cucumber-expressions/testdata/expression/matches-word.yaml @@ -0,0 +1,4 @@ +expression: three {word} mice +text: three blind mice +expected: |- + ["blind"] diff --git a/cucumber-expressions/testdata/expression/throws-unknown-parameter-type.yaml b/cucumber-expressions/testdata/expression/throws-unknown-parameter-type.yaml new file mode 100644 index 0000000000..384e3a48c3 --- /dev/null +++ b/cucumber-expressions/testdata/expression/throws-unknown-parameter-type.yaml @@ -0,0 +1,10 @@ +expression: |- + {unknown} +text: something +exception: |- + This Cucumber Expression has a problem at column 1: + + {unknown} + ^-------^ + Undefined parameter type 'unknown'. + Please register a ParameterType for 'unknown' diff --git a/cucumber-expressions/testdata/regex/alternation-with-optional.yaml b/cucumber-expressions/testdata/regex/alternation-with-optional.yaml new file mode 100644 index 0000000000..73787b2b0a --- /dev/null +++ b/cucumber-expressions/testdata/regex/alternation-with-optional.yaml @@ -0,0 +1,2 @@ +expression: "a/b(c)" +expected: ^(?:a|b(?:c)?)$ diff --git a/cucumber-expressions/testdata/regex/alternation.yaml b/cucumber-expressions/testdata/regex/alternation.yaml new file mode 100644 index 0000000000..1dc293fb62 --- /dev/null +++ b/cucumber-expressions/testdata/regex/alternation.yaml @@ -0,0 +1,2 @@ +expression: "a/b c/d/e" +expected: ^(?:a|b) (?:c|d|e)$ diff --git a/cucumber-expressions/testdata/regex/empty.yaml b/cucumber-expressions/testdata/regex/empty.yaml new file mode 100644 index 0000000000..bb9a81906c --- /dev/null +++ b/cucumber-expressions/testdata/regex/empty.yaml @@ -0,0 +1,2 @@ +expression: "" +expected: ^$ diff --git a/cucumber-expressions/testdata/regex/escape-regex-characters.yaml b/cucumber-expressions/testdata/regex/escape-regex-characters.yaml new file mode 100644 index 0000000000..c8ea8c549e --- /dev/null +++ b/cucumber-expressions/testdata/regex/escape-regex-characters.yaml @@ -0,0 +1,2 @@ +expression: '^$[]\(\){}\\.|?*+' +expected: ^\^\$\[\]\(\)(.*)\\\.\|\?\*\+$ diff --git a/cucumber-expressions/testdata/regex/optional.yaml b/cucumber-expressions/testdata/regex/optional.yaml new file mode 100644 index 0000000000..7d6d84cc14 --- /dev/null +++ b/cucumber-expressions/testdata/regex/optional.yaml @@ -0,0 +1,2 @@ +expression: "(a)" +expected: ^(?:a)?$ diff --git a/cucumber-expressions/testdata/regex/parameter.yaml b/cucumber-expressions/testdata/regex/parameter.yaml new file mode 100644 index 0000000000..f793b21c0f --- /dev/null +++ b/cucumber-expressions/testdata/regex/parameter.yaml @@ -0,0 +1,2 @@ +expression: "{int}" +expected: ^((?:-?\d+)|(?:\d+))$ diff --git a/cucumber-expressions/testdata/regex/text.yaml b/cucumber-expressions/testdata/regex/text.yaml new file mode 100644 index 0000000000..2af3e41664 --- /dev/null +++ b/cucumber-expressions/testdata/regex/text.yaml @@ -0,0 +1,2 @@ +expression: "a" +expected: ^a$ diff --git a/cucumber-expressions/testdata/regex/unicode.yaml b/cucumber-expressions/testdata/regex/unicode.yaml new file mode 100644 index 0000000000..f93fe35db1 --- /dev/null +++ b/cucumber-expressions/testdata/regex/unicode.yaml @@ -0,0 +1,2 @@ +expression: "Привет, Мир(ы)!" +expected: ^Привет, Мир(?:ы)?!$ diff --git a/cucumber-expressions/testdata/tokens/alternation-phrase.yaml b/cucumber-expressions/testdata/tokens/alternation-phrase.yaml new file mode 100644 index 0000000000..48b107f64e --- /dev/null +++ b/cucumber-expressions/testdata/tokens/alternation-phrase.yaml @@ -0,0 +1,13 @@ +expression: three blind/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "ALTERNATION", "start": 11, "end": 12, "text": "/"}, + {"type": "TEXT", "start": 12, "end": 19, "text": "cripple"}, + {"type": "WHITE_SPACE", "start": 19, "end": 20, "text": " "}, + {"type": "TEXT", "start": 20, "end": 24, "text": "mice"}, + {"type": "END_OF_LINE", "start": 24, "end": 24, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/alternation.yaml b/cucumber-expressions/testdata/tokens/alternation.yaml new file mode 100644 index 0000000000..a4920f22e5 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/alternation.yaml @@ -0,0 +1,9 @@ +expression: blind/cripple +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "blind"}, + {"type": "ALTERNATION", "start": 5, "end": 6, "text": "/"}, + {"type": "TEXT", "start": 6, "end": 13, "text": "cripple"}, + {"type": "END_OF_LINE", "start": 13, "end": 13, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/empty-string.yaml b/cucumber-expressions/testdata/tokens/empty-string.yaml new file mode 100644 index 0000000000..501f7522f2 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/empty-string.yaml @@ -0,0 +1,6 @@ +expression: "" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "END_OF_LINE", "start": 0, "end": 0, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/escape-non-reserved-character.yaml b/cucumber-expressions/testdata/tokens/escape-non-reserved-character.yaml new file mode 100644 index 0000000000..5e206be084 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escape-non-reserved-character.yaml @@ -0,0 +1,8 @@ +expression: \[ +exception: |- + This Cucumber Expression has a problem at column 2: + + \[ + ^ + Only the characters '{', '}', '(', ')', '\', '/' and whitespace can be escaped. + If you did mean to use an '\' you can use '\\' to escape it diff --git a/cucumber-expressions/testdata/tokens/escaped-alternation.yaml b/cucumber-expressions/testdata/tokens/escaped-alternation.yaml new file mode 100644 index 0000000000..7e21f7ad19 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escaped-alternation.yaml @@ -0,0 +1,9 @@ +expression: blind\ and\ famished\/cripple mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 29, "text": "blind and famished/cripple"}, + {"type": "WHITE_SPACE", "start": 29, "end": 30, "text": " "}, + {"type": "TEXT", "start": 30, "end": 34, "text": "mice"}, + {"type": "END_OF_LINE", "start": 34, "end": 34, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml b/cucumber-expressions/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml new file mode 100644 index 0000000000..6375ad52a5 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml @@ -0,0 +1,9 @@ +expression: ' \/ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "WHITE_SPACE", "start": 0, "end": 1, "text": " "}, + {"type": "TEXT", "start": 1, "end": 3, "text": "/"}, + {"type": "WHITE_SPACE", "start": 3, "end": 4, "text": " "}, + {"type": "END_OF_LINE", "start": 4, "end": 4, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/escaped-end-of-line.yaml b/cucumber-expressions/testdata/tokens/escaped-end-of-line.yaml new file mode 100644 index 0000000000..a1bd00fd98 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escaped-end-of-line.yaml @@ -0,0 +1,8 @@ +expression: \ +exception: |- + This Cucumber Expression has a problem at column 1: + + \ + ^ + The end of line can not be escaped. + You can use '\\' to escape the the '\' diff --git a/cucumber-expressions/testdata/tokens/escaped-optional.yaml b/cucumber-expressions/testdata/tokens/escaped-optional.yaml new file mode 100644 index 0000000000..2b365b581c --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escaped-optional.yaml @@ -0,0 +1,7 @@ +expression: \(blind\) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 9, "text": "(blind)"}, + {"type": "END_OF_LINE", "start": 9, "end": 9, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/escaped-parameter.yaml b/cucumber-expressions/testdata/tokens/escaped-parameter.yaml new file mode 100644 index 0000000000..2cdac4f35a --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escaped-parameter.yaml @@ -0,0 +1,7 @@ +expression: \{string\} +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 10, "text": "{string}"}, + {"type": "END_OF_LINE", "start": 10, "end": 10, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/escaped-space.yaml b/cucumber-expressions/testdata/tokens/escaped-space.yaml new file mode 100644 index 0000000000..912827a941 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/escaped-space.yaml @@ -0,0 +1,7 @@ +expression: '\ ' +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 2, "text": " "}, + {"type": "END_OF_LINE", "start": 2, "end": 2, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/optional-phrase.yaml b/cucumber-expressions/testdata/tokens/optional-phrase.yaml new file mode 100644 index 0000000000..2ddc6bb502 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/optional-phrase.yaml @@ -0,0 +1,13 @@ +expression: three (blind) mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_OPTIONAL", "start": 6, "end": 7, "text": "("}, + {"type": "TEXT", "start": 7, "end": 12, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 12, "end": 13, "text": ")"}, + {"type": "WHITE_SPACE", "start": 13, "end": 14, "text": " "}, + {"type": "TEXT", "start": 14, "end": 18, "text": "mice"}, + {"type": "END_OF_LINE", "start": 18, "end": 18, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/optional.yaml b/cucumber-expressions/testdata/tokens/optional.yaml new file mode 100644 index 0000000000..35b1474a7c --- /dev/null +++ b/cucumber-expressions/testdata/tokens/optional.yaml @@ -0,0 +1,9 @@ +expression: (blind) +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_OPTIONAL", "start": 0, "end": 1, "text": "("}, + {"type": "TEXT", "start": 1, "end": 6, "text": "blind"}, + {"type": "END_OPTIONAL", "start": 6, "end": 7, "text": ")"}, + {"type": "END_OF_LINE", "start": 7, "end": 7, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/parameter-phrase.yaml b/cucumber-expressions/testdata/tokens/parameter-phrase.yaml new file mode 100644 index 0000000000..5e98055ee6 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/parameter-phrase.yaml @@ -0,0 +1,13 @@ +expression: three {string} mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "BEGIN_PARAMETER", "start": 6, "end": 7, "text": "{"}, + {"type": "TEXT", "start": 7, "end": 13, "text": "string"}, + {"type": "END_PARAMETER", "start": 13, "end": 14, "text": "}"}, + {"type": "WHITE_SPACE", "start": 14, "end": 15, "text": " "}, + {"type": "TEXT", "start": 15, "end": 19, "text": "mice"}, + {"type": "END_OF_LINE", "start": 19, "end": 19, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/parameter.yaml b/cucumber-expressions/testdata/tokens/parameter.yaml new file mode 100644 index 0000000000..460363c393 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/parameter.yaml @@ -0,0 +1,9 @@ +expression: "{string}" +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "BEGIN_PARAMETER", "start": 0, "end": 1, "text": "{"}, + {"type": "TEXT", "start": 1, "end": 7, "text": "string"}, + {"type": "END_PARAMETER", "start": 7, "end": 8, "text": "}"}, + {"type": "END_OF_LINE", "start": 8, "end": 8, "text": ""} + ] diff --git a/cucumber-expressions/testdata/tokens/phrase.yaml b/cucumber-expressions/testdata/tokens/phrase.yaml new file mode 100644 index 0000000000..e2cfccf7b4 --- /dev/null +++ b/cucumber-expressions/testdata/tokens/phrase.yaml @@ -0,0 +1,11 @@ +expression: three blind mice +expected: |- + [ + {"type": "START_OF_LINE", "start": 0, "end": 0, "text": ""}, + {"type": "TEXT", "start": 0, "end": 5, "text": "three"}, + {"type": "WHITE_SPACE", "start": 5, "end": 6, "text": " "}, + {"type": "TEXT", "start": 6, "end": 11, "text": "blind"}, + {"type": "WHITE_SPACE", "start": 11, "end": 12, "text": " "}, + {"type": "TEXT", "start": 12, "end": 16, "text": "mice"}, + {"type": "END_OF_LINE", "start": 16, "end": 16, "text": ""} + ]