├── .github ├── renovate.json └── workflows │ ├── new-issues.yml │ ├── release-github.yaml │ ├── release-mvn.yml │ ├── release-npm.yml │ ├── release-nuget.yaml │ ├── release-pypi.yaml │ ├── release-rubygem.yml │ ├── stryker-javascript.yml │ ├── test-dotnet.yml │ ├── test-go.yml │ ├── test-java.yml │ ├── test-javascript.yml │ ├── test-python.yml │ └── test-ruby.yml ├── .gitignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── docs └── index.html ├── dotnet ├── .gitattributes ├── .gitignore ├── CucucmberExpressions.snk ├── CucumberExpressions.Tests │ ├── CombinatorialGeneratedExpressionFactoryTest.cs │ ├── CucumberExpressionGeneratorTest.cs │ ├── CucumberExpressionParserTest.cs │ ├── CucumberExpressionTest.cs │ ├── CucumberExpressionTestBase.cs │ ├── CucumberExpressionTokenizerTest.cs │ ├── CucumberExpressionTransformationTest.cs │ ├── CucumberExpressions.Tests.csproj │ ├── ExpressionFactoryTest.cs │ ├── RegularExpressionTest.cs │ ├── TestBase.cs │ └── TreeRegexpTest.cs ├── CucumberExpressions.sln ├── CucumberExpressions.sln.DotSettings ├── CucumberExpressions │ ├── AssemblyAttributes.cs │ ├── Ast │ │ ├── AstExtensions.cs │ │ ├── ILocated.cs │ │ ├── Node.cs │ │ ├── NodeType.cs │ │ ├── Token.cs │ │ └── TokenType.cs │ ├── CucumberExpression.cs │ ├── CucumberExpressionException.cs │ ├── CucumberExpressions.csproj │ ├── ExpressionFactory.cs │ ├── Generation │ │ ├── CombinatorialGeneratedExpressionFactory.cs │ │ ├── CucumberExpressionGenerator.cs │ │ ├── GeneratedExpression.cs │ │ └── ParameterTypeMatcher.cs │ ├── IExpression.cs │ ├── IParameterType.cs │ ├── IParameterTypeRegistry.cs │ ├── ParameterTypeConstants.cs │ ├── Parsing │ │ ├── CucumberExpressionParser.cs │ │ ├── CucumberExpressionParserResult.cs │ │ ├── CucumberExpressionTokenizer.cs │ │ ├── Group.cs │ │ ├── GroupBuilder.cs │ │ ├── RegexCaptureGroupRemover.cs │ │ └── TreeRegexp.cs │ ├── RegularExpression.cs │ ├── Resources │ │ └── cucumber-mark-green-128.png │ └── UndefinedParameterTypeException.cs ├── LICENSE └── heuristics.adoc ├── go ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── argument.go ├── argument_test.go ├── ast.go ├── combinatorial_generated_expression_factory.go ├── combinatorial_generated_expression_factory_test.go ├── cucumber_expression.go ├── cucumber_expression_generator.go ├── cucumber_expression_generator_test.go ├── cucumber_expression_parser.go ├── cucumber_expression_parser_test.go ├── cucumber_expression_test.go ├── cucumber_expression_tokenizer.go ├── cucumber_expression_tokenizer_test.go ├── cucumber_expression_transformation_test.go ├── custom_parameter_type_test.go ├── errors.go ├── expression.go ├── generated_expression.go ├── go.mod ├── go.sum ├── group.go ├── group_builder.go ├── iterator.go ├── parameter_by_type_transformer.go ├── parameter_by_type_transformer_test.go ├── parameter_type.go ├── parameter_type_matcher.go ├── parameter_type_registry.go ├── parameter_type_registry_test.go ├── parameter_type_test.go ├── regular_expression.go ├── regular_expression_test.go ├── stack.go ├── submatch.go ├── tree_regexp.go └── tree_regexp_test.go ├── java ├── .gitignore ├── LICENSE ├── heuristics.adoc ├── pom.xml └── src │ ├── main │ └── java │ │ └── io │ │ └── cucumber │ │ └── cucumberexpressions │ │ ├── AmbiguousParameterTypeException.java │ │ ├── Argument.java │ │ ├── Ast.java │ │ ├── BuiltInParameterTransformer.java │ │ ├── CaptureGroupTransformer.java │ │ ├── CombinatorialGeneratedExpressionFactory.java │ │ ├── CucumberExpression.java │ │ ├── CucumberExpressionException.java │ │ ├── CucumberExpressionGenerator.java │ │ ├── CucumberExpressionParser.java │ │ ├── CucumberExpressionTokenizer.java │ │ ├── DefaultPatternCompiler.java │ │ ├── DuplicateTypeNameException.java │ │ ├── Expression.java │ │ ├── ExpressionFactory.java │ │ ├── GeneratedExpression.java │ │ ├── Group.java │ │ ├── GroupBuilder.java │ │ ├── KeyboardFriendlyDecimalFormatSymbols.java │ │ ├── NumberParser.java │ │ ├── ParameterByTypeTransformer.java │ │ ├── ParameterType.java │ │ ├── ParameterTypeMatcher.java │ │ ├── ParameterTypeRegistry.java │ │ ├── PatternCompiler.java │ │ ├── PatternCompilerProvider.java │ │ ├── RegexpUtils.java │ │ ├── RegularExpression.java │ │ ├── Transformer.java │ │ ├── TreeRegexp.java │ │ ├── TypeReference.java │ │ └── UndefinedParameterTypeException.java │ └── test │ └── java │ └── io │ └── cucumber │ └── cucumberexpressions │ ├── ArgumentTest.java │ ├── BuiltInParameterTransformerTest.java │ ├── CombinatorialGeneratedExpressionFactoryTest.java │ ├── CucumberExpressionGeneratorTest.java │ ├── CucumberExpressionParserTest.java │ ├── CucumberExpressionTest.java │ ├── CucumberExpressionTokenizerTest.java │ ├── CucumberExpressionTransformationTest.java │ ├── CustomMatchers.java │ ├── CustomParameterTypeTest.java │ ├── EnumParameterTypeTest.java │ ├── ExpressionFactoryTest.java │ ├── GenericParameterTypeTest.java │ ├── KeyboardFriendlyDecimalFormatSymbolsTest.java │ ├── NumberParserTest.java │ ├── ParameterByTypeTransformerTest.java │ ├── ParameterTypeComparatorTest.java │ ├── ParameterTypeRegistryTest.java │ ├── PatternCompilerProviderTest.java │ ├── RegexpUtilsTest.java │ ├── RegularExpressionTest.java │ └── TreeRegexpTest.java ├── javascript ├── .gitignore ├── .mocharc.json ├── .prettierrc.json ├── LICENSE ├── eslint.config.mjs ├── package-lock.json ├── package.cjs.json ├── package.json ├── src │ ├── Argument.ts │ ├── Ast.ts │ ├── CombinatorialGeneratedExpressionFactory.ts │ ├── CucumberExpression.ts │ ├── CucumberExpressionError.ts │ ├── CucumberExpressionGenerator.ts │ ├── CucumberExpressionParser.ts │ ├── CucumberExpressionTokenizer.ts │ ├── Errors.ts │ ├── ExpressionFactory.ts │ ├── GeneratedExpression.ts │ ├── Group.ts │ ├── GroupBuilder.ts │ ├── ParameterType.ts │ ├── ParameterTypeMatcher.ts │ ├── ParameterTypeRegistry.ts │ ├── RegularExpression.ts │ ├── TreeRegexp.ts │ ├── defineDefaultParameterTypes.ts │ ├── index.ts │ └── types.ts ├── stryker.conf.json ├── test │ ├── ArgumentTest.ts │ ├── CombinatorialGeneratedExpressionFactoryTest.ts │ ├── CucumberExpressionGeneratorTest.ts │ ├── CucumberExpressionParserTest.ts │ ├── CucumberExpressionTest.ts │ ├── CucumberExpressionTokenizerTest.ts │ ├── CucumberExpressionTransformationTest.ts │ ├── CustomParameterTypeTest.ts │ ├── ExpressionFactoryTest.ts │ ├── ParameterTypeRegistryTest.ts │ ├── ParameterTypeTest.ts │ ├── RegularExpressionTest.ts │ ├── TreeRegexpTest.ts │ └── testDataDir.ts ├── tsconfig.build-cjs.json ├── tsconfig.build-esm.json ├── tsconfig.build.json └── tsconfig.json ├── python ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── cucumber_expressions │ ├── __init__.py │ ├── argument.py │ ├── ast.py │ ├── combinatorial_generated_expression_factory.py │ ├── errors.py │ ├── expression.py │ ├── expression_generator.py │ ├── expression_parser.py │ ├── expression_tokenizer.py │ ├── generated_expression.py │ ├── group.py │ ├── group_builder.py │ ├── parameter_type.py │ ├── parameter_type_matcher.py │ ├── parameter_type_registry.py │ ├── regular_expression.py │ └── tree_regexp.py ├── poetry.lock ├── pyproject.toml └── tests │ ├── __init__.py │ ├── conftest.py │ ├── definitions.py │ ├── test_argument.py │ ├── test_combinatorial_generated_expression_factory.py │ ├── test_custom_parameter_type.py │ ├── test_expression.py │ ├── test_expression_generator.py │ ├── test_expression_parser.py │ ├── test_expression_tokenizer.py │ ├── test_expression_transformation.py │ ├── test_parameter_type_registry.py │ ├── test_regular_expression.py │ └── test_tree_regex.py ├── ruby ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── Gemfile ├── LICENSE ├── Rakefile ├── VERSION ├── cucumber-cucumber-expressions.gemspec ├── lib │ └── cucumber │ │ └── cucumber_expressions │ │ ├── argument.rb │ │ ├── ast.rb │ │ ├── combinatorial_generated_expression_factory.rb │ │ ├── cucumber_expression.rb │ │ ├── cucumber_expression_generator.rb │ │ ├── cucumber_expression_parser.rb │ │ ├── cucumber_expression_tokenizer.rb │ │ ├── errors.rb │ │ ├── expression_factory.rb │ │ ├── generated_expression.rb │ │ ├── group.rb │ │ ├── group_builder.rb │ │ ├── parameter_type.rb │ │ ├── parameter_type_matcher.rb │ │ ├── parameter_type_registry.rb │ │ ├── regular_expression.rb │ │ └── tree_regexp.rb └── spec │ └── cucumber │ └── cucumber_expressions │ ├── argument_spec.rb │ ├── combinatorial_generated_expression_factory_spec.rb │ ├── cucumber_expression_generator_spec.rb │ ├── cucumber_expression_parser_spec.rb │ ├── cucumber_expression_spec.rb │ ├── cucumber_expression_tokenizer_spec.rb │ ├── cucumber_expression_transformation_spec.rb │ ├── custom_parameter_type_spec.rb │ ├── expression_factory_spec.rb │ ├── parameter_type_registry_spec.rb │ ├── parameter_type_spec.rb │ ├── regular_expression_spec.rb │ └── tree_regexp_spec.rb └── testdata ├── cucumber-expression ├── matching │ ├── allows-escaped-optional-parameter-types.yaml │ ├── allows-parameter-type-in-alternation-1.yaml │ ├── allows-parameter-type-in-alternation-2.yaml │ ├── does-allow-parameter-adjacent-to-alternation.yaml │ ├── does-not-allow-alternation-in-optional.yaml │ ├── does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml │ ├── does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml │ ├── does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml │ ├── does-not-allow-alternation-with-empty-alternative.yaml │ ├── does-not-allow-empty-optional.yaml │ ├── does-not-allow-nested-optional.yaml │ ├── does-not-allow-optional-parameter-types.yaml │ ├── does-not-allow-parameter-name-with-reserved-characters.yaml │ ├── does-not-allow-unfinished-parenthesis-1.yaml │ ├── does-not-allow-unfinished-parenthesis-2.yaml │ ├── does-not-allow-unfinished-parenthesis-3.yaml │ ├── does-not-match-misquoted-string.yaml │ ├── doesnt-match-float-as-int.yaml │ ├── matches-alternation.yaml │ ├── matches-anonymous-parameter-type.yaml │ ├── matches-bigdecimal.yaml │ ├── matches-biginteger.yaml │ ├── matches-byte.yaml │ ├── matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml │ ├── matches-double-quoted-empty-string-as-empty-string.yaml │ ├── matches-double-quoted-string-with-escaped-double-quote.yaml │ ├── matches-double-quoted-string-with-single-quotes.yaml │ ├── matches-double-quoted-string.yaml │ ├── matches-double.yaml │ ├── matches-doubly-escaped-parenthesis.yaml │ ├── matches-doubly-escaped-slash-1.yaml │ ├── matches-doubly-escaped-slash-2.yaml │ ├── matches-escaped-parenthesis-1.yaml │ ├── matches-escaped-parenthesis-2.yaml │ ├── matches-escaped-parenthesis-3.yaml │ ├── matches-escaped-slash.yaml │ ├── matches-float-with-integer-part.yaml │ ├── matches-float-without-integer-part.yaml │ ├── matches-float.yaml │ ├── matches-int.yaml │ ├── matches-long.yaml │ ├── matches-multiple-double-quoted-strings.yaml │ ├── matches-multiple-single-quoted-strings.yaml │ ├── matches-optional-before-alternation-1.yaml │ ├── matches-optional-before-alternation-2.yaml │ ├── matches-optional-before-alternation-with-regex-characters-1.yaml │ ├── matches-optional-before-alternation-with-regex-characters-2.yaml │ ├── matches-optional-in-alternation-1.yaml │ ├── matches-optional-in-alternation-2.yaml │ ├── matches-optional-in-alternation-3.yaml │ ├── matches-short.yaml │ ├── matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml │ ├── matches-single-quoted-empty-string-as-empty-string.yaml │ ├── matches-single-quoted-string-with-double-quotes.yaml │ ├── matches-single-quoted-string-with-escaped-single-quote.yaml │ ├── matches-single-quoted-string.yaml │ ├── matches-word.yaml │ └── throws-unknown-parameter-type.yaml ├── parser │ ├── alternation-followed-by-optional.yaml │ ├── alternation-phrase.yaml │ ├── alternation-with-parameter.yaml │ ├── alternation-with-unused-end-optional.yaml │ ├── alternation-with-unused-start-optional.yaml │ ├── alternation-with-white-space.yaml │ ├── alternation.yaml │ ├── anonymous-parameter.yaml │ ├── closing-brace.yaml │ ├── closing-parenthesis.yaml │ ├── empty-alternation.yaml │ ├── empty-alternations.yaml │ ├── empty-string.yaml │ ├── escaped-alternation.yaml │ ├── escaped-backslash.yaml │ ├── escaped-opening-parenthesis.yaml │ ├── escaped-optional-followed-by-optional.yaml │ ├── escaped-optional-phrase.yaml │ ├── escaped-optional.yaml │ ├── opening-brace.yaml │ ├── opening-parenthesis.yaml │ ├── optional-containing-nested-optional.yaml │ ├── optional-phrase.yaml │ ├── optional.yaml │ ├── parameter.yaml │ ├── phrase.yaml │ └── unfinished-parameter.yaml ├── tokenizer │ ├── alternation-phrase.yaml │ ├── alternation.yaml │ ├── empty-string.yaml │ ├── escape-non-reserved-character.yaml │ ├── escaped-alternation.yaml │ ├── escaped-char-has-start-index-of-text-token.yaml │ ├── escaped-end-of-line.yaml │ ├── escaped-optional.yaml │ ├── escaped-parameter.yaml │ ├── escaped-space.yaml │ ├── optional-phrase.yaml │ ├── optional.yaml │ ├── parameter-phrase.yaml │ ├── parameter.yaml │ └── phrase.yaml └── transformation │ ├── alternation-with-optional.yaml │ ├── alternation.yaml │ ├── empty.yaml │ ├── escape-regex-characters.yaml │ ├── optional.yaml │ ├── parameter.yaml │ ├── text.yaml │ └── unicode.yaml └── regular-expression └── matching ├── optional-capture-groups-all.yaml ├── optional-capture-groups-issue.yaml └── optional-capture-groups-some.yaml /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>cucumber/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/new-issues.yml: -------------------------------------------------------------------------------- 1 | name: New issues 2 | on: 3 | issues: 4 | types: [ opened, edited, reopened ] 5 | jobs: 6 | add-to-project-board: 7 | name: Add issue to project board 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Add new issues to project board 11 | uses: cucumber/action-add-issues-to-project@main 12 | with: 13 | gitub_token: ${{ secrets.NEW_ISSUES_GITHUB_TOKEN }} 14 | github_project_title: New issues 15 | github_org: cucumber 16 | -------------------------------------------------------------------------------- /.github/workflows/release-github.yaml: -------------------------------------------------------------------------------- 1 | name: Release GitHub 2 | 3 | on: 4 | push: 5 | branches: [release/*] 6 | 7 | jobs: 8 | create-github-release: 9 | name: Create GitHub Release and Git tag 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | permissions: 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: cucumber/action-create-github-release@v1.1.1 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/release-mvn.yml: -------------------------------------------------------------------------------- 1 | name: Release Maven 2 | 3 | on: 4 | push: 5 | branches: [release/*] 6 | 7 | jobs: 8 | publish-mvn: 9 | name: Publish Maven Package 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '11' 18 | cache: 'maven' 19 | - uses: cucumber/action-publish-mvn@v3.0.0 20 | with: 21 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 22 | gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} 23 | nexus-username: ${{ secrets.SONATYPE_USERNAME }} 24 | nexus-password: ${{ secrets.SONATYPE_PASSWORD }} 25 | working-directory: java 26 | -------------------------------------------------------------------------------- /.github/workflows/release-npm.yml: -------------------------------------------------------------------------------- 1 | name: Release NPM 2 | 3 | on: 4 | push: 5 | branches: [release/*] 6 | 7 | jobs: 8 | publish-npm: 9 | name: Publish NPM module 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: '22.x' 17 | cache: 'npm' 18 | cache-dependency-path: javascript/package-lock.json 19 | - run: npm install-test 20 | working-directory: javascript 21 | - uses: cucumber/action-publish-npm@v1.1.1 22 | with: 23 | npm-token: ${{ secrets.NPM_TOKEN }} 24 | working-directory: javascript 25 | -------------------------------------------------------------------------------- /.github/workflows/release-nuget.yaml: -------------------------------------------------------------------------------- 1 | name: Release NuGet 2 | 3 | on: 4 | push: 5 | branches: 6 | - "release/*" 7 | 8 | jobs: 9 | publish-nuget: 10 | name: Publish package to NuGet.org 11 | runs-on: ubuntu-latest 12 | environment: Release 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: 6.0.x 19 | - uses: cucumber/action-publish-nuget@v1.0.0 20 | with: 21 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 22 | working-directory: "dotnet" 23 | -------------------------------------------------------------------------------- /.github/workflows/release-pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Release Python 2 | 3 | on: 4 | push: 5 | branches: [ release/* ] 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | permissions: 13 | id-token: write 14 | defaults: 15 | run: 16 | working-directory: python 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | - name: Set up Python 3.10 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: "3.13" 24 | 25 | - name: Show Python version 26 | run: python --version 27 | 28 | - name: Build package 29 | run: | 30 | python -m pip install build twine 31 | python -m build 32 | twine check --strict dist/* 33 | 34 | - name: Publish package distributions to PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | with: 37 | packages-dir: python/dist 38 | -------------------------------------------------------------------------------- /.github/workflows/release-rubygem.yml: -------------------------------------------------------------------------------- 1 | name: Release RubyGems 2 | 3 | on: 4 | push: 5 | branches: [release/*] 6 | 7 | jobs: 8 | publish-rubygem: 9 | name: Publish Ruby Gem 10 | runs-on: ubuntu-latest 11 | environment: Release 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: '3.3' 17 | bundler-cache: true 18 | - uses: cucumber/action-publish-rubygem@v1.0.0 19 | with: 20 | rubygems_api_key: ${{ secrets.RUBYGEMS_API_KEY }} 21 | working_directory: ruby 22 | -------------------------------------------------------------------------------- /.github/workflows/stryker-javascript.yml: -------------------------------------------------------------------------------- 1 | name: stryker-javascript 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - renovate/** 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | stryker: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '22' 19 | cache: 'npm' 20 | cache-dependency-path: javascript/package-lock.json 21 | - run: npm install 22 | working-directory: javascript 23 | - run: npm run stryker 24 | working-directory: javascript 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: stryker-report 28 | path: | 29 | javascript/reports/mutation/html/index.html 30 | javascript/reports/mutation/mutation.json 31 | -------------------------------------------------------------------------------- /.github/workflows/test-dotnet.yml: -------------------------------------------------------------------------------- 1 | name: test-dotnet 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: 6.0.x 22 | - name: Restore dependencies 23 | run: dotnet restore 24 | working-directory: dotnet 25 | - name: Build 26 | run: dotnet build --no-restore 27 | working-directory: dotnet 28 | - name: Test 29 | run: dotnet test --no-build --verbosity normal 30 | working-directory: dotnet 31 | -------------------------------------------------------------------------------- /.github/workflows/test-go.yml: -------------------------------------------------------------------------------- 1 | name: test-go 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | go: ['1.18.x'] 21 | include: 22 | - os: windows-latest 23 | go: '1.18.x' 24 | - os: macos-latest 25 | go: '1.18.x' 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Go 30 | uses: actions/setup-go@v5 31 | with: 32 | go-version: ${{ matrix.go }} 33 | - name: lint 34 | working-directory: go 35 | run: gofmt -w . 36 | - name: test 37 | working-directory: go 38 | run: go test ./... 39 | -------------------------------------------------------------------------------- /.github/workflows/test-java.yml: -------------------------------------------------------------------------------- 1 | name: test-java 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | java: ['11', '17'] 21 | include: 22 | - os: windows-latest 23 | java: '17' 24 | - os: macos-latest 25 | java: '17' 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-java@v4 30 | with: 31 | distribution: 'temurin' 32 | java-version: ${{ matrix.java }} 33 | cache: 'maven' 34 | - run: mvn install 35 | working-directory: java 36 | - run: mvn test 37 | working-directory: java 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/test-javascript.yml: -------------------------------------------------------------------------------- 1 | name: test-javascript 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | node-version: ['18.x', '20.x', '22.x', '23.x'] 21 | include: 22 | - os: windows-latest 23 | node-version: '22.x' 24 | - os: macos-latest 25 | node-version: '22.x' 26 | 27 | steps: 28 | - name: set git core.autocrlf to 'input' 29 | run: git config --global core.autocrlf input 30 | - uses: actions/checkout@v4 31 | - name: with Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | cache: 'npm' 36 | cache-dependency-path: javascript/package-lock.json 37 | - run: npm install-test 38 | working-directory: javascript 39 | - run: npm run lint 40 | working-directory: javascript 41 | -------------------------------------------------------------------------------- /.github/workflows/test-ruby.yml: -------------------------------------------------------------------------------- 1 | name: test-ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/** 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: 19 | - ubuntu-latest 20 | ruby: ['2.7', '3.0', '3.1', '3.2', '3.3'] 21 | include: 22 | - os: windows-latest 23 | ruby: '3.3' 24 | - os: macos-latest 25 | ruby: '3.3' 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | working-directory: ruby 34 | - name: rspec 35 | working-directory: ruby 36 | run: bundle exec rspec 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Everyone contributing to this repo is expected to abide by the [Cucumber Community Code of Conduct](https://cucumber.io/conduct). 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | See [.github/RELEASING](https://github.com/cucumber/.github/blob/main/RELEASING.md). 4 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Cucumber Expressions 9 | 10 | 11 |

You are being redirected to

12 | 13 | 14 | -------------------------------------------------------------------------------- /dotnet/.gitattributes: -------------------------------------------------------------------------------- 1 | # Set line endings to LF, even on Windows. Otherwise, execution within Docker fails. 2 | # See https://help.github.com/articles/dealing-with-line-endings/ 3 | *.sh text eol=lf 4 | docker-run text eol=lf 5 | gherkin text eol=lf 6 | gherkin-generate-tokens text eol=lf 7 | -------------------------------------------------------------------------------- /dotnet/CucucmberExpressions.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-expressions/51272cff745a7065f794768ab8a3d9630ea770eb/dotnet/CucucmberExpressions.snk -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/CombinatorialGeneratedExpressionFactoryTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CucumberExpressions.Generation; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace CucumberExpressions.Tests; 8 | 9 | public class CombinatorialGeneratedExpressionFactoryTest : CucumberExpressionTestBase 10 | { 11 | private const string WORD = "\\w+"; 12 | 13 | [Fact] 14 | public void Generates_multiple_expressions() 15 | { 16 | var first = new List(); 17 | first.Add(new StubParameterType("color", WORD)); 18 | first.Add(new StubParameterType("csscolor", WORD)); 19 | 20 | var second = new List(); 21 | second.Add(new StubParameterType("date", WORD)); 22 | second.Add(new StubParameterType("datetime", WORD)); 23 | second.Add(new StubParameterType("timestamp", WORD)); 24 | var parameterTypeCombinations = new List>() 25 | { 26 | first, second 27 | }; 28 | 29 | var factory = new CombinatorialGeneratedExpressionFactory( 30 | "I bought a {{{0}}} ball on {{{1}}}", 31 | parameterTypeCombinations 32 | ); 33 | var generatedExpressions = factory.GenerateExpressions(); 34 | var expressions = new List(); 35 | foreach (var generatedExpression in generatedExpressions) 36 | { 37 | var source = generatedExpression.GetSource(); 38 | expressions.Add(source); 39 | } 40 | expressions.Should().BeEquivalentTo(new[] { 41 | "I bought a {color} ball on {date}", 42 | "I bought a {color} ball on {datetime}", 43 | "I bought a {color} ball on {timestamp}", 44 | "I bought a {csscolor} ball on {date}", 45 | "I bought a {csscolor} ball on {datetime}", 46 | "I bought a {csscolor} ball on {timestamp}" 47 | }); 48 | } 49 | 50 | public class Color 51 | { 52 | } 53 | 54 | public class CssColor 55 | { 56 | } 57 | 58 | public class Date 59 | { 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/CucumberExpressionParserTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using CucumberExpressions.Parsing; 6 | using FluentAssertions; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace CucumberExpressions.Tests; 11 | 12 | public class CucumberExpressionParserTest : TestBase 13 | { 14 | private readonly ITestOutputHelper _testOutputHelper; 15 | private readonly CucumberExpressionParser _parser = new(); 16 | 17 | public CucumberExpressionParserTest(ITestOutputHelper testOutputHelper) 18 | { 19 | _testOutputHelper = testOutputHelper; 20 | } 21 | 22 | public static IEnumerable acceptance_tests_pass_data() 23 | => GetTestDataFiles("cucumber-expression", "parser") 24 | .Select(file => new object[] 25 | { 26 | Path.GetFileNameWithoutExtension(file), 27 | ParseYaml(file) 28 | }); 29 | 30 | [Theory, MemberData(nameof(acceptance_tests_pass_data))] 31 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "")] 32 | public void acceptance_tests_pass(string testCase, Expectation expectation) 33 | { 34 | _testOutputHelper.WriteLine(ToYaml(expectation)); 35 | if (expectation.exception == null) 36 | { 37 | Ast.Node node = _parser.Parse(expectation.expression); 38 | node.Should().Be(expectation.expected_ast.ToNode()); 39 | } 40 | else 41 | { 42 | FluentActions.Invoking(() => _parser.Parse(expectation.expression)) 43 | .Should().Throw().WithMessage(expectation.exception); 44 | } 45 | } 46 | 47 | public class Expectation 48 | { 49 | public string expression; 50 | // ReSharper disable once InconsistentNaming 51 | public YamlableNode expected_ast; 52 | public string exception; 53 | } 54 | public class YamlableNode 55 | { 56 | public Ast.NodeType type; 57 | public List nodes; 58 | public string token; 59 | public int start; 60 | public int end; 61 | 62 | public Ast.Node ToNode() 63 | { 64 | if (token != null) 65 | { 66 | return new Ast.Node(type, start, end, token); 67 | } 68 | else 69 | { 70 | return new Ast.Node(type, start, end, nodes.Select(n => n.ToNode()).ToArray()); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/CucumberExpressionTokenizerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using CucumberExpressions.Parsing; 6 | using FluentAssertions; 7 | using Xunit; 8 | 9 | namespace CucumberExpressions.Tests; 10 | 11 | public class CucumberExpressionTokenizerTest : TestBase 12 | { 13 | private readonly CucumberExpressionTokenizer _tokenizer = new(); 14 | 15 | public static IEnumerable acceptance_tests_pass_data() 16 | => GetTestDataFiles("cucumber-expression", "tokenizer") 17 | .Select(file => new object[] 18 | { 19 | Path.GetFileNameWithoutExtension(file), 20 | ParseYaml(file) 21 | }); 22 | 23 | [Theory, MemberData(nameof(acceptance_tests_pass_data))] 24 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "")] 25 | public void Acceptance_tests_pass(string testCase, Expectation expectation) 26 | { 27 | if (expectation.Exception == null) 28 | { 29 | var tokens = _tokenizer.Tokenize(expectation.Expression); 30 | var expectedTokens = expectation.ExpectedTokens 31 | .Select(t => t.ToToken()); 32 | tokens.Should().BeEquivalentTo(expectedTokens); 33 | } 34 | else 35 | { 36 | FluentActions.Invoking(() => _tokenizer.Tokenize(expectation.Expression)) 37 | .Should().Throw().WithMessage(expectation.Exception); 38 | } 39 | } 40 | 41 | public class Expectation 42 | { 43 | public string Expression; 44 | public List ExpectedTokens; 45 | public string Exception; 46 | } 47 | 48 | public class YamlableToken 49 | { 50 | public string Text; 51 | public Ast.TokenType Type; 52 | public int Start; 53 | public int End; 54 | 55 | public Ast.Token ToToken() 56 | { 57 | return new Ast.Token(Text, Type, Start, End); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/CucumberExpressionTransformationTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace CucumberExpressions.Tests; 9 | 10 | public class CucumberExpressionTransformationTest : CucumberExpressionTestBase 11 | { 12 | private readonly StubParameterTypeRegistry _parameterTypeRegistry = new(); 13 | private readonly ITestOutputHelper _testOutputHelper; 14 | 15 | public CucumberExpressionTransformationTest(ITestOutputHelper testOutputHelper) 16 | { 17 | _testOutputHelper = testOutputHelper; 18 | var oldIntParameterType = _parameterTypeRegistry.LookupByTypeName("int"); 19 | _parameterTypeRegistry.Remove(oldIntParameterType); 20 | _parameterTypeRegistry.DefineParameterType(new StubParameterType(ParameterTypeConstants.IntParameterName, 21 | new []{ ParameterTypeConstants.IntParameterRegex, "\\d+" }, weight: 1000)); 22 | } 23 | 24 | public static IEnumerable acceptance_tests_pass_data() 25 | => GetTestDataFiles("cucumber-expression", "transformation") 26 | //.Where(file => file.Contains("matches-single-quoted-string-with-escaped-single-quote")) 27 | .Select(file => new object[] 28 | { 29 | Path.GetFileNameWithoutExtension(file), 30 | ParseYaml(file) 31 | }); 32 | 33 | 34 | [Theory, MemberData(nameof(acceptance_tests_pass_data))] 35 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "")] 36 | public void Acceptance_tests_pass(string testCase, Expectation expectation) { 37 | _testOutputHelper.WriteLine(testCase); 38 | _testOutputHelper.WriteLine(ToYaml(expectation)); 39 | CucumberExpression expression = new CucumberExpression(expectation.expression, _parameterTypeRegistry); 40 | Assert.Equal(expectation.expected_regex, expression.Regex.ToString()); 41 | } 42 | 43 | public class Expectation { 44 | public string expression; 45 | // ReSharper disable once InconsistentNaming 46 | public string expected_regex; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/CucumberExpressions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | latest 6 | false 7 | True 8 | ..\CucucmberExpressions.snk 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/RegularExpressionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Xunit; 4 | 5 | namespace CucumberExpressions.Tests; 6 | 7 | public class RegularExpressionTest : CucumberExpressionTestBase 8 | { 9 | [Fact] 10 | public void documentation_match_arguments() 11 | { 12 | var expr = new Regex("I have (\\d+) cukes? in my (\\w+) now"); 13 | IExpression expression = new RegularExpression(expr); 14 | var match = MatchExpression(expression, "I have 7 cukes in my belly now"); 15 | Assert.Equal("7", match[0]); 16 | Assert.Equal("belly", match[1]); 17 | } 18 | 19 | [Fact] 20 | public void exposes_source_and_regexp() 21 | { 22 | String regexp = "I have (\\d+) cukes? in my (.+) now"; 23 | RegularExpression expression = new RegularExpression(new Regex(regexp)); 24 | Assert.Equal(regexp, expression.Source); 25 | Assert.Equal(regexp, expression.Regex.ToString()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using YamlDotNet.Serialization; 4 | using YamlDotNet.Serialization.NamingConventions; 5 | 6 | namespace CucumberExpressions.Tests; 7 | 8 | public abstract class TestBase 9 | { 10 | protected static string GetTestDataFilePath(string fileName, params string[] sections) 11 | { 12 | var testDataFolder = GetTestDataFolder(sections); 13 | var filePath = Path.Combine(testDataFolder, fileName); 14 | return filePath; 15 | } 16 | 17 | protected static string[] GetTestDataFiles(params string[] sections) 18 | { 19 | var testDataFolder = GetTestDataFolder(sections); 20 | return Directory.GetFiles(testDataFolder).OrderBy(f => f).ToArray(); 21 | } 22 | 23 | protected static string GetTestDataFolder(params string[] sections) 24 | { 25 | var testAssemblyFolder = Path.GetDirectoryName(typeof(TestBase).Assembly.Location); 26 | var testDataFolder = Path.Combine(testAssemblyFolder!, "..", "..", "..", "..", "..", "testdata"); 27 | if (sections != null && sections.Length > 0) 28 | testDataFolder = Path.Combine(new[] { testDataFolder }.Concat(sections).ToArray()); 29 | return testDataFolder; 30 | } 31 | 32 | protected static T ParseYaml(string filePath) 33 | { 34 | var fileContent = File.ReadAllText(filePath); 35 | 36 | var deserializer = new DeserializerBuilder() 37 | .WithNamingConvention(UnderscoredNamingConvention.Instance) 38 | .Build(); 39 | 40 | return deserializer.Deserialize(fileContent); 41 | } 42 | 43 | protected static string ToYaml(object obj) 44 | { 45 | var serializer = new SerializerBuilder() 46 | .WithNamingConvention(UnderscoredNamingConvention.Instance) 47 | .Build(); 48 | return serializer.Serialize(obj); 49 | } 50 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31808.319 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberExpressions", "CucumberExpressions\CucumberExpressions.csproj", "{0ECCA9AF-995C-4FC9-A304-11E22F39558D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CucumberExpressions.Tests", "CucumberExpressions.Tests\CucumberExpressions.Tests.csproj", "{FFDCFB98-192A-483B-9FAB-540B748B7824}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "repo", "repo", "{A63CE1D7-957D-4D8C-890D-051A754AFD0A}" 11 | ProjectSection(SolutionItems) = preProject 12 | ..\CHANGELOG.md = ..\CHANGELOG.md 13 | ..\README.md = ..\README.md 14 | ..\.github\workflows\test-dotnet.yml = ..\.github\workflows\test-dotnet.yml 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {0ECCA9AF-995C-4FC9-A304-11E22F39558D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {0ECCA9AF-995C-4FC9-A304-11E22F39558D}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {0ECCA9AF-995C-4FC9-A304-11E22F39558D}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {0ECCA9AF-995C-4FC9-A304-11E22F39558D}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {FFDCFB98-192A-483B-9FAB-540B748B7824}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {FFDCFB98-192A-483B-9FAB-540B748B7824}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {FFDCFB98-192A-483B-9FAB-540B748B7824}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {FFDCFB98-192A-483B-9FAB-540B748B7824}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {5395AB64-CD7A-465D-847F-AC17D1D600B2} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/AssemblyAttributes.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("CucumberExpressions.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001003d3d7a4a4bb40fd4ad4b18dea92bd57f04946bbce990a7e72a406f026c0af1544510e1069718f7bdc8134fca21b4fb61d8ff139af7c19f2d855a0bf7539667334371478c323ff84e91ccb6a5bc3027fea39ca84658087b7f7f76c30af7adacb315442a0cbec817b71017f363fc4d8a751b98b6e60b00149b08c84ca984ab5ddd")] -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Ast/AstExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace CucumberExpressions.Ast; 2 | 3 | public static class AstExtensions 4 | { 5 | public static string GetSymbol(this TokenType tokenType) 6 | { 7 | switch (tokenType) 8 | { 9 | case TokenType.BEGIN_OPTIONAL: 10 | return Token.BeginOptionalCharacter.ToString(); 11 | case TokenType.END_OPTIONAL: 12 | return Token.EndOptionalCharacter.ToString(); 13 | case TokenType.BEGIN_PARAMETER: 14 | return Token.BeginParameterCharacter.ToString(); 15 | case TokenType.END_PARAMETER: 16 | return Token.EndParameterCharacter.ToString(); 17 | case TokenType.ALTERNATION: 18 | return Token.AlternationCharacter.ToString(); 19 | } 20 | 21 | return null; 22 | } 23 | 24 | public static string GetPurpose(this TokenType tokenType) 25 | { 26 | switch (tokenType) 27 | { 28 | case TokenType.BEGIN_OPTIONAL: 29 | case TokenType.END_OPTIONAL: 30 | return "optional text"; 31 | case TokenType.BEGIN_PARAMETER: 32 | case TokenType.END_PARAMETER: 33 | return "a parameter"; 34 | case TokenType.ALTERNATION: 35 | return "alternation"; 36 | } 37 | 38 | return null; 39 | } 40 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Ast/ILocated.cs: -------------------------------------------------------------------------------- 1 | namespace CucumberExpressions.Ast; 2 | 3 | public interface ILocated 4 | { 5 | int Start { get; } 6 | int End { get; } 7 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Ast/NodeType.cs: -------------------------------------------------------------------------------- 1 | namespace CucumberExpressions.Ast; 2 | 3 | public enum NodeType 4 | { 5 | TEXT_NODE, 6 | OPTIONAL_NODE, 7 | ALTERNATION_NODE, 8 | ALTERNATIVE_NODE, 9 | PARAMETER_NODE, 10 | EXPRESSION_NODE 11 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Ast/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace CucumberExpressions.Ast; 2 | 3 | public enum TokenType 4 | { 5 | UNKNOWN, 6 | START_OF_LINE, 7 | END_OF_LINE, 8 | WHITE_SPACE, 9 | BEGIN_OPTIONAL, 10 | END_OPTIONAL, 11 | BEGIN_PARAMETER, 12 | END_PARAMETER, 13 | ALTERNATION, 14 | TEXT 15 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/CucumberExpressions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latest 6 | false 7 | True 8 | ..\CucucmberExpressions.snk 9 | 10 | 11 | 12 | 18.0.1 13 | $(VersionNumber)-$(SnapshotSuffix) 14 | $(VersionNumber) 15 | 16 | 17 | 18 | Cucumber Expressions 19 | Cucumber.CucumberExpressions 20 | Cucumber Ltd, Gaspar Nagy 21 | Copyright © Cucumber Ltd, Gaspar Nagy 22 | Cucumber Expressions implementation for .NET. 23 | specflow cucumber 24 | https://github.com/cucumber/cucumber-expressions/tree/main/dotnet 25 | https://github.com/cucumber/cucumber-expressions 26 | git 27 | cucumber-mark-green-128.png 28 | MIT 29 | 30 | true 31 | bin/$(Configuration)/NuGet 32 | 33 | 34 | 35 | 36 | True 37 | . 38 | true 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Generation/CombinatorialGeneratedExpressionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace CucumberExpressions.Generation; 6 | 7 | internal class CombinatorialGeneratedExpressionFactory 8 | { 9 | // 256 generated expressions ought to be enough for anybody 10 | private const int MAX_EXPRESSIONS = 256; 11 | private readonly string _expressionTemplate; 12 | private readonly List> _parameterTypeCombinations; 13 | 14 | public CombinatorialGeneratedExpressionFactory(string expressionTemplate, 15 | List> parameterTypeCombinations) 16 | { 17 | _expressionTemplate = expressionTemplate; 18 | _parameterTypeCombinations = parameterTypeCombinations; 19 | } 20 | 21 | public GeneratedExpression[] GenerateExpressions() 22 | { 23 | var generatedExpressions = new List(); 24 | var permutation = new Stack(_parameterTypeCombinations.Count); 25 | GeneratePermutations(generatedExpressions, permutation); 26 | return generatedExpressions.ToArray(); 27 | } 28 | 29 | private void GeneratePermutations(List generatedExpressions, 30 | Stack permutation) 31 | { 32 | if (generatedExpressions.Count >= MAX_EXPRESSIONS) 33 | { 34 | return; 35 | } 36 | 37 | if (permutation.Count == _parameterTypeCombinations.Count) 38 | { 39 | var permutationCopy = permutation.Reverse().ToArray(); 40 | generatedExpressions.Add(new GeneratedExpression(_expressionTemplate, permutationCopy)); 41 | return; 42 | } 43 | 44 | var parameterTypes = _parameterTypeCombinations[permutation.Count]; 45 | foreach (var parameterType in parameterTypes) 46 | { 47 | // Avoid recursion if no elements can be added. 48 | if (generatedExpressions.Count >= MAX_EXPRESSIONS) 49 | { 50 | return; 51 | } 52 | permutation.Push(parameterType); 53 | GeneratePermutations(generatedExpressions, permutation); 54 | permutation.Pop(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/IExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace CucumberExpressions; 5 | 6 | public interface IExpression 7 | { 8 | string Source { get; } 9 | Regex Regex { get; } 10 | } 11 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/IParameterType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CucumberExpressions; 4 | 5 | public interface IParameterType 6 | { 7 | string[] RegexStrings { get; } 8 | string Name { get; } 9 | Type ParameterType { get; } 10 | int Weight { get; } 11 | bool UseForSnippets { get; } 12 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/IParameterTypeRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CucumberExpressions; 4 | 5 | public interface IParameterTypeRegistry 6 | { 7 | IParameterType LookupByTypeName(string name); 8 | IEnumerable GetParameterTypes(); 9 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Parsing/CucumberExpressionParserResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CucumberExpressions.Ast; 3 | 4 | namespace CucumberExpressions.Parsing; 5 | 6 | public class CucumberExpressionParserResult 7 | { 8 | public int Consumed { get; } 9 | public Node[] Ast { get; } 10 | 11 | public CucumberExpressionParserResult(int consumed, params Node[] ast) 12 | { 13 | Consumed = consumed; 14 | Ast = ast ?? Array.Empty(); 15 | } 16 | } -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Parsing/Group.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace CucumberExpressions.Parsing; 7 | 8 | public class Group 9 | { 10 | public string Value { get; } 11 | public int Start { get; } 12 | public int End { get; } 13 | public List Children { get; } 14 | 15 | public Group(string value, int start, int end, List children) 16 | { 17 | Value = value; 18 | Start = start; 19 | End = end; 20 | Children = children; 21 | } 22 | 23 | public string[] GetValues() 24 | { 25 | List groups = !Children.Any() ? 26 | new List { this } : Children; 27 | return groups.Select(g => g.Value).ToArray(); 28 | } 29 | 30 | /** 31 | * Parse a {@link Pattern} into collection of {@link Group}s 32 | * 33 | * @param expression the expression to decompose 34 | * @return A collection of {@link Group}s, possibly empty but never 35 | * null 36 | */ 37 | public static Group[] Parse(Regex expression) 38 | { 39 | GroupBuilder builder = TreeRegexp.CreateGroupBuilder(expression); 40 | return ToGroups(builder.Children).ToArray(); 41 | } 42 | 43 | private static List ToGroups(IEnumerable children) 44 | { 45 | var list = new List(); 46 | if (children != null) 47 | { 48 | foreach (GroupBuilder child in children) 49 | { 50 | list.Add(new Group(child.Source, child.StartIndex, child.EndIndex, 51 | ToGroups(child.Children))); 52 | } 53 | } 54 | return list; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Parsing/GroupBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace CucumberExpressions.Parsing; 6 | 7 | public class GroupBuilder 8 | { 9 | private readonly List _groupBuilders = new(); 10 | 11 | public string Source { get; set; } 12 | 13 | public int StartIndex { get; } 14 | 15 | public int EndIndex { get; set; } 16 | 17 | public bool IsCapturing { get; private set; } = true; 18 | 19 | public IEnumerable Children => _groupBuilders; 20 | 21 | public GroupBuilder(int startIndex) 22 | { 23 | StartIndex = startIndex; 24 | } 25 | 26 | public void Add(GroupBuilder groupBuilder) 27 | { 28 | _groupBuilders.Add(groupBuilder); 29 | } 30 | 31 | public Group Build(Match matcher, IEnumerator groupIndices) 32 | { 33 | groupIndices.MoveNext(); 34 | int groupIndex = groupIndices.Current; 35 | var children = new List(_groupBuilders.Count); 36 | foreach (GroupBuilder childGroupBuilder in _groupBuilders) 37 | { 38 | children.Add(childGroupBuilder.Build(matcher, groupIndices)); 39 | } 40 | 41 | var matcherGroup = matcher.Groups[groupIndex]; 42 | return new Group(matcherGroup.Success ? matcherGroup.Value : null, matcherGroup.Index, matcherGroup.Index + matcherGroup.Length, children); 43 | } 44 | 45 | public void SetNonCapturing() 46 | { 47 | IsCapturing = false; 48 | } 49 | 50 | public void MoveChildrenTo(GroupBuilder groupBuilder) 51 | { 52 | foreach (GroupBuilder child in _groupBuilders) 53 | { 54 | groupBuilder.Add(child); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Parsing/RegexCaptureGroupRemover.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace CucumberExpressions.Parsing 7 | { 8 | internal static class RegexCaptureGroupRemover 9 | { 10 | public static string RemoveCaptureGroups(string regex) 11 | { 12 | return RemoveCaptureGroupsInternal(regex, 0); 13 | } 14 | public static string RemoveInnerCaptureGroups(string regex) 15 | { 16 | return RemoveCaptureGroupsInternal(regex, 1); 17 | } 18 | 19 | private static string RemoveCaptureGroupsInternal(string regex, int skipLevels) 20 | { 21 | if (!regex.Contains("(")) 22 | return regex; // surely no groups 23 | var treeRegexp = new TreeRegexp(regex); 24 | var rootGroupBuilder = treeRegexp.GroupBuilder; 25 | if (!rootGroupBuilder.Children.Any()) 26 | return regex; 27 | 28 | var result = new StringBuilder(regex); 29 | var groupStarts = GetGroupStarts(rootGroupBuilder, skipLevels, 0) 30 | .OrderByDescending(i => i); 31 | foreach (var groupStart in groupStarts) 32 | { 33 | result.Insert(groupStart + 1, "?:"); 34 | } 35 | 36 | return result.ToString(); 37 | } 38 | 39 | private static IEnumerable GetGroupStarts(GroupBuilder groupBuilder, int skipLevels, int level) 40 | { 41 | foreach (var innerBuilder in groupBuilder.Children) 42 | { 43 | if (level >= skipLevels) 44 | yield return innerBuilder.StartIndex; 45 | foreach (var groupStart in GetGroupStarts(innerBuilder, skipLevels, level + 1)) 46 | yield return groupStart; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/RegularExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace CucumberExpressions; 4 | 5 | public class RegularExpression : IExpression 6 | { 7 | public virtual Regex Regex { get; } 8 | 9 | public virtual string Source => Regex.ToString(); 10 | 11 | public RegularExpression(Regex expressionRegexp) 12 | { 13 | Regex = expressionRegexp; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/Resources/cucumber-mark-green-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-expressions/51272cff745a7065f794768ab8a3d9630ea770eb/dotnet/CucumberExpressions/Resources/cucumber-mark-green-128.png -------------------------------------------------------------------------------- /dotnet/CucumberExpressions/UndefinedParameterTypeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CucumberExpressions.Ast; 3 | 4 | namespace CucumberExpressions; 5 | 6 | public class UndefinedParameterTypeException : CucumberExpressionException 7 | { 8 | public string UndefinedParameterTypeName { get; } 9 | 10 | public UndefinedParameterTypeException(string message, string undefinedParameterTypeName) : base(message) 11 | { 12 | UndefinedParameterTypeName = undefinedParameterTypeName; 13 | } 14 | 15 | internal static CucumberExpressionException CreateUndefinedParameterType(Node node, string expression, string undefinedParameterTypeName) 16 | { 17 | return new UndefinedParameterTypeException(GetMessage( 18 | node.Start, 19 | expression, 20 | PointAt(node), 21 | "Undefined parameter type '" + undefinedParameterTypeName + "'", 22 | "Please register a ParameterType for '" + undefinedParameterTypeName + "'"), undefinedParameterTypeName); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dotnet/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dotnet/heuristics.adoc: -------------------------------------------------------------------------------- 1 | = Cucumber Expression - Java Heuristics 2 | 3 | Java doesn't provide a literal syntax for regular expressions, so they 4 | have to be specified as strings. Cucumber uses heuristics to determine 5 | if a string should be interpreted as a Cucumber Expression or a Regular Expression. 6 | 7 | The table below describes the heuristics, along with some examples 8 | 9 | |=== 10 | |Expression|Type|Explanation 11 | 12 | |strings are cukexp by default 13 | |CucumberExpression 14 | |When there are no special characters, a Cucumber Expression is always assumed. 15 | 16 | |^definitely a regexp$ 17 | |RegularExpression 18 | |The presence of anchors assumes a Regular Expression, even if only one of the anchors are present. 19 | 20 | |/surely a regexp/ 21 | |RegularExpression 22 | |Forward slashes always assumes Regular Expression. The slashes themselves are removed. 23 | 24 | |=== 25 | -------------------------------------------------------------------------------- /go/.gitignore: -------------------------------------------------------------------------------- 1 | .linted 2 | .tested 3 | -------------------------------------------------------------------------------- /go/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go/Makefile: -------------------------------------------------------------------------------- 1 | GO_SOURCE_FILES := $(wildcard *.go) 2 | 3 | default: .linted .tested 4 | .PHONY: default 5 | 6 | .linted: $(GO_SOURCE_FILES) 7 | gofmt -w $^ 8 | touch $@ 9 | 10 | .tested: $(GO_SOURCE_FILES) 11 | go test ./... 12 | touch $@ 13 | 14 | update-dependencies: 15 | go get -u && go mod tidy 16 | .PHONY: update-dependencies 17 | 18 | clean: 19 | rm -rf .linted .tested 20 | .PHONY: clean 21 | 22 | -------------------------------------------------------------------------------- /go/README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Expressions for Go 2 | 3 | [The docs are here](https://github.com/cucumber/cucumber-expressions#readme). 4 | -------------------------------------------------------------------------------- /go/argument.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import "fmt" 4 | 5 | type Argument struct { 6 | group *Group 7 | parameterType *ParameterType 8 | } 9 | 10 | func BuildArguments(treeRegexp *TreeRegexp, text string, parameterTypes []*ParameterType) []*Argument { 11 | group := treeRegexp.Match(text) 12 | if group == nil { 13 | return nil 14 | } 15 | argGroups := group.Children() 16 | if len(argGroups) != len(parameterTypes) { 17 | panic(fmt.Errorf("%s has %d capture groups (%v), but there were %d parameter types (%v)", treeRegexp.Regexp().String(), len(argGroups), argGroups, len(parameterTypes), parameterTypes)) 18 | } 19 | arguments := make([]*Argument, len(parameterTypes)) 20 | for i, parameterType := range parameterTypes { 21 | arguments[i] = NewArgument(argGroups[i], parameterType) 22 | } 23 | return arguments 24 | } 25 | 26 | func NewArgument(group *Group, parameterType *ParameterType) *Argument { 27 | return &Argument{ 28 | group: group, 29 | parameterType: parameterType, 30 | } 31 | } 32 | 33 | func (a *Argument) Group() *Group { 34 | return a.group 35 | } 36 | 37 | func (a *Argument) GetValue() interface{} { 38 | values := a.group.Values() 39 | if values == nil { 40 | return nil 41 | } 42 | return a.parameterType.Transform(values) 43 | } 44 | 45 | func (a *Argument) ParameterType() *ParameterType { 46 | return a.parameterType 47 | } 48 | -------------------------------------------------------------------------------- /go/argument_test.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestArgument(t *testing.T) { 11 | t.Run("exposes ParameterType", func(t *testing.T) { 12 | treeRegexp := NewTreeRegexp(regexp.MustCompile("three (.*) mice")) 13 | parameterTypeRegistry := NewParameterTypeRegistry() 14 | parameterType := parameterTypeRegistry.LookupByTypeName("string") 15 | parameterTypes := []*ParameterType{parameterType} 16 | arguments := BuildArguments(treeRegexp, "three blind mice", parameterTypes) 17 | argument := arguments[0] 18 | 19 | require.Equal(t, argument.ParameterType().name, "string") 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /go/combinatorial_generated_expression_factory.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | // 256 generated expressions ought to be enough for anybody 4 | const maxExpressions = 256 5 | 6 | type CombinatorialGeneratedExpressionFactory struct { 7 | expressionTemplate string 8 | parameterTypeCombinations [][]*ParameterType 9 | } 10 | 11 | func NewCombinatorialGeneratedExpressionFactory(expressionTemplate string, parameterTypeCombinations [][]*ParameterType) *CombinatorialGeneratedExpressionFactory { 12 | return &CombinatorialGeneratedExpressionFactory{ 13 | expressionTemplate: expressionTemplate, 14 | parameterTypeCombinations: parameterTypeCombinations, 15 | } 16 | } 17 | 18 | func (c *CombinatorialGeneratedExpressionFactory) GenerateExpressions() []*GeneratedExpression { 19 | generatedExpressions := &GeneratedExpressionList{} 20 | c.generatePermutations(generatedExpressions, 0, nil) 21 | return generatedExpressions.ToArray() 22 | } 23 | 24 | func (c *CombinatorialGeneratedExpressionFactory) generatePermutations(generatedExpressions *GeneratedExpressionList, depth int, currentParameterTypes []*ParameterType) { 25 | if len(generatedExpressions.elements) >= maxExpressions { 26 | return 27 | } 28 | 29 | if depth == len(c.parameterTypeCombinations) { 30 | generatedExpressions.Push( 31 | NewGeneratedExpression(c.expressionTemplate, currentParameterTypes), 32 | ) 33 | return 34 | } 35 | 36 | for _, parameterType := range c.parameterTypeCombinations[depth] { 37 | // Avoid recursion if no elements can be added. 38 | if len(generatedExpressions.elements) >= maxExpressions { 39 | return 40 | } 41 | 42 | c.generatePermutations( 43 | generatedExpressions, 44 | depth+1, 45 | append(currentParameterTypes, parameterType), 46 | ) 47 | } 48 | } 49 | 50 | type GeneratedExpressionList struct { 51 | elements []*GeneratedExpression 52 | } 53 | 54 | func (g *GeneratedExpressionList) Push(expr *GeneratedExpression) { 55 | g.elements = append(g.elements, expr) 56 | } 57 | 58 | func (g *GeneratedExpressionList) ToArray() []*GeneratedExpression { 59 | return g.elements 60 | } 61 | -------------------------------------------------------------------------------- /go/cucumber_expression_parser_test.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/require" 6 | "gopkg.in/yaml.v3" 7 | "io/ioutil" 8 | "testing" 9 | ) 10 | 11 | type ParserExpectation struct { 12 | Expression string `yaml:"expression"` 13 | ExpectedAst node `yaml:"expected_ast"` 14 | Exception string `yaml:"exception"` 15 | } 16 | 17 | func TestCucumberExpressionParser(t *testing.T) { 18 | var assertAst = func(t *testing.T, expected node, expression string) { 19 | ast, err := parse(expression) 20 | require.NoError(t, err) 21 | require.Equal(t, expected, ast) 22 | } 23 | var assertThrows = func(t *testing.T, expected string, expression string) { 24 | _, err := parse(expression) 25 | require.Error(t, err) 26 | require.Equal(t, expected, err.Error()) 27 | } 28 | 29 | directory := "../testdata/cucumber-expression/parser/" 30 | files, err := ioutil.ReadDir(directory) 31 | require.NoError(t, err) 32 | 33 | for _, file := range files { 34 | contents, err := ioutil.ReadFile(directory + file.Name()) 35 | require.NoError(t, err) 36 | t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { 37 | var expectation ParserExpectation 38 | err = yaml.Unmarshal(contents, &expectation) 39 | require.NoError(t, err) 40 | 41 | if expectation.Exception == "" { 42 | assertAst(t, expectation.ExpectedAst, expectation.Expression) 43 | } else { 44 | assertThrows(t, expectation.Exception, expectation.Expression) 45 | } 46 | }) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /go/cucumber_expression_tokenizer_test.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/require" 6 | "gopkg.in/yaml.v3" 7 | "io/ioutil" 8 | "testing" 9 | ) 10 | 11 | type TokenizerExpectation struct { 12 | Expression string `yaml:"expression"` 13 | ExpectedTokens []token `yaml:"expected_tokens"` 14 | Exception string `yaml:"exception"` 15 | } 16 | 17 | func TestCucumberExpressionTokenizer(t *testing.T) { 18 | 19 | directory := "../testdata/cucumber-expression/tokenizer/" 20 | files, err := ioutil.ReadDir(directory) 21 | require.NoError(t, err) 22 | 23 | for _, file := range files { 24 | contents, err := ioutil.ReadFile(directory + file.Name()) 25 | require.NoError(t, err) 26 | t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { 27 | var expectation TokenizerExpectation 28 | err = yaml.Unmarshal(contents, &expectation) 29 | require.NoError(t, err) 30 | 31 | if expectation.Exception == "" { 32 | assertTokenizes(t, expectation.ExpectedTokens, expectation.Expression) 33 | } else { 34 | assertThrows(t, expectation.Exception, expectation.Expression) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func assertTokenizes(t *testing.T, expected []token, expression string) { 41 | tokens, err := tokenize(expression) 42 | require.NoError(t, err) 43 | require.Equal(t, expected, tokens) 44 | } 45 | 46 | func assertThrows(t *testing.T, expected string, expression string) { 47 | _, err := tokenize(expression) 48 | require.Error(t, err) 49 | require.Equal(t, expected, err.Error()) 50 | } 51 | -------------------------------------------------------------------------------- /go/cucumber_expression_transformation_test.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/require" 6 | "gopkg.in/yaml.v3" 7 | "io/ioutil" 8 | "testing" 9 | ) 10 | 11 | type TransformationExpectation struct { 12 | Expression string `yaml:"expression"` 13 | ExpectedRegex string `yaml:"expected_regex"` 14 | } 15 | 16 | func TestCucumberExpressionTransformation(t *testing.T) { 17 | 18 | t.Run("acceptance tests pass", func(t *testing.T) { 19 | assertRegex := func(t *testing.T, expected string, expr string) { 20 | parameterTypeRegistry := NewParameterTypeRegistry() 21 | expression, err := NewCucumberExpression(expr, parameterTypeRegistry) 22 | require.NoError(t, err) 23 | require.Equal(t, expected, expression.Regexp().String()) 24 | } 25 | 26 | directory := "../testdata/cucumber-expression/transformation/" 27 | files, err := ioutil.ReadDir(directory) 28 | require.NoError(t, err) 29 | 30 | for _, file := range files { 31 | contents, err := ioutil.ReadFile(directory + file.Name()) 32 | require.NoError(t, err) 33 | t.Run(fmt.Sprintf("%s", file.Name()), func(t *testing.T) { 34 | var expectation TransformationExpectation 35 | err = yaml.Unmarshal(contents, &expectation) 36 | require.NoError(t, err) 37 | assertRegex(t, expectation.ExpectedRegex, expectation.Expression) 38 | }) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /go/expression.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | ) 7 | 8 | type Expression interface { 9 | Match(text string, typeHints ...reflect.Type) ([]*Argument, error) 10 | Regexp() *regexp.Regexp 11 | Source() string 12 | } 13 | -------------------------------------------------------------------------------- /go/generated_expression.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import "fmt" 4 | 5 | type GeneratedExpression struct { 6 | expressionTemplate string 7 | parameterTypes []*ParameterType 8 | } 9 | 10 | func NewGeneratedExpression(expressionTemplate string, parameterTypes []*ParameterType) *GeneratedExpression { 11 | return &GeneratedExpression{ 12 | expressionTemplate: expressionTemplate, 13 | parameterTypes: parameterTypes, 14 | } 15 | } 16 | 17 | func (g *GeneratedExpression) Source() string { 18 | names := make([]interface{}, len(g.parameterTypes)) 19 | for i, p := range g.parameterTypes { 20 | names[i] = p.Name() 21 | } 22 | return fmt.Sprintf(g.expressionTemplate, names...) 23 | } 24 | 25 | func (g *GeneratedExpression) ParameterNames() []string { 26 | usageByTypeName := map[string]int{} 27 | result := make([]string, len(g.parameterTypes)) 28 | for i, p := range g.parameterTypes { 29 | result[i] = getParameterName(p.Name(), usageByTypeName) 30 | } 31 | return result 32 | } 33 | 34 | func (g *GeneratedExpression) ParameterTypes() []*ParameterType { 35 | return g.parameterTypes 36 | } 37 | 38 | func getParameterName(typeName string, usageByTypeName map[string]int) string { 39 | count, ok := usageByTypeName[typeName] 40 | if !ok { 41 | count = 1 42 | } else { 43 | count++ 44 | } 45 | usageByTypeName[typeName] = count 46 | if count == 1 { 47 | return typeName 48 | } 49 | return fmt.Sprintf("%s%d", typeName, count) 50 | } 51 | -------------------------------------------------------------------------------- /go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cucumber/cucumber-expressions/go/v18 2 | 3 | require ( 4 | github.com/stretchr/testify v1.10.0 5 | gopkg.in/yaml.v3 v3.0.1 6 | ) 7 | 8 | require ( 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/pmezard/go-difflib v1.0.0 // indirect 11 | ) 12 | 13 | go 1.19 14 | -------------------------------------------------------------------------------- /go/group.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | type Group struct { 4 | value *string 5 | start int 6 | end int 7 | children []*Group 8 | } 9 | 10 | func NewGroup(value *string, start, end int, children []*Group) *Group { 11 | return &Group{ 12 | value: value, 13 | start: start, 14 | end: end, 15 | children: children, 16 | } 17 | } 18 | 19 | func (g *Group) Value() *string { 20 | return g.value 21 | } 22 | 23 | func (g *Group) Start() int { 24 | return g.start 25 | } 26 | 27 | func (g *Group) End() int { 28 | return g.end 29 | } 30 | 31 | func (g *Group) Children() []*Group { 32 | return g.children 33 | } 34 | 35 | func (g *Group) Values() []*string { 36 | if g.value == nil { 37 | return nil 38 | } 39 | if len(g.children) == 0 { 40 | return []*string{g.value} 41 | } 42 | result := make([]*string, len(g.children)) 43 | for i, child := range g.children { 44 | result[i] = child.Value() 45 | } 46 | return result 47 | } 48 | -------------------------------------------------------------------------------- /go/group_builder.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | type GroupBuilder struct { 4 | groupBuilders []*GroupBuilder 5 | capturing bool 6 | source string 7 | } 8 | 9 | func NewGroupBuilder() *GroupBuilder { 10 | return &GroupBuilder{ 11 | capturing: true, 12 | } 13 | } 14 | 15 | func (g *GroupBuilder) Add(groupBuilder *GroupBuilder) { 16 | g.groupBuilders = append(g.groupBuilders, groupBuilder) 17 | } 18 | 19 | func (g *GroupBuilder) Build(submatches []*Submatch, indexIterator *IntIterator) *Group { 20 | submatch := submatches[indexIterator.Next()] 21 | children := make([]*Group, len(g.groupBuilders)) 22 | for i, child := range g.groupBuilders { 23 | children[i] = child.Build(submatches, indexIterator) 24 | } 25 | return NewGroup(submatch.value, submatch.start, submatch.end, children) 26 | } 27 | 28 | func (g *GroupBuilder) SetNonCapturing() { 29 | g.capturing = false 30 | } 31 | 32 | func (g *GroupBuilder) Capturing() bool { 33 | return g.capturing 34 | } 35 | 36 | func (g *GroupBuilder) Children() []*GroupBuilder { 37 | return g.groupBuilders 38 | } 39 | 40 | func (g *GroupBuilder) MoveChildrenTo(groupBuilder *GroupBuilder) { 41 | for _, child := range g.groupBuilders { 42 | groupBuilder.Add(child) 43 | } 44 | } 45 | 46 | func (g *GroupBuilder) SetSource(value string) { 47 | g.source = value 48 | } 49 | 50 | func (g *GroupBuilder) Source() string { 51 | return g.source 52 | } 53 | -------------------------------------------------------------------------------- /go/iterator.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | type InterfaceIterator struct { 4 | elements []interface{} 5 | index int 6 | } 7 | 8 | func (i *InterfaceIterator) Next() interface{} { 9 | if i.index >= len(i.elements) { 10 | panic("cannot get next") 11 | } 12 | oldIndex := i.index 13 | i.index++ 14 | return i.elements[oldIndex] 15 | } 16 | 17 | type IntIterator struct { 18 | iterator InterfaceIterator 19 | } 20 | 21 | func NewIntIterator(size int) *IntIterator { 22 | elements := make([]interface{}, size) 23 | for i := 0; i < size; i++ { 24 | elements[i] = i 25 | } 26 | return &IntIterator{ 27 | iterator: InterfaceIterator{ 28 | elements: elements, 29 | }, 30 | } 31 | } 32 | 33 | func (s *IntIterator) Next() int { 34 | return s.iterator.Next().(int) 35 | } 36 | -------------------------------------------------------------------------------- /go/parameter_type_test.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParameterType(t *testing.T) { 11 | t.Run("does not allow ignore flag on regexp", func(t *testing.T) { 12 | _, err := NewParameterType( 13 | "case-insensitive", 14 | []*regexp.Regexp{regexp.MustCompile("(?i)[a-z]+")}, 15 | "case-insensitive", 16 | nil, 17 | true, 18 | true, 19 | false, 20 | ) 21 | require.EqualError(t, err, "ParameterType Regexps can't use flags") 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /go/stack.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | type InterfaceStack struct { 4 | elements []interface{} 5 | } 6 | 7 | func (i *InterfaceStack) Len() int { 8 | return len(i.elements) 9 | } 10 | 11 | func (i *InterfaceStack) Peek() interface{} { 12 | if i.Len() == 0 { 13 | panic("cannot peek") 14 | } 15 | return i.elements[i.Len()-1] 16 | } 17 | 18 | func (i *InterfaceStack) Pop() interface{} { 19 | if i.Len() == 0 { 20 | panic("cannot pop") 21 | } 22 | value := i.elements[i.Len()-1] 23 | i.elements = i.elements[:i.Len()-1] 24 | return value 25 | } 26 | 27 | func (i *InterfaceStack) Push(value interface{}) { 28 | i.elements = append(i.elements, value) 29 | } 30 | 31 | type GroupBuilderStack struct { 32 | interfaceStack InterfaceStack 33 | } 34 | 35 | func (s *GroupBuilderStack) Len() int { 36 | return s.interfaceStack.Len() 37 | } 38 | 39 | func (s *GroupBuilderStack) Peek() *GroupBuilder { 40 | return s.interfaceStack.Peek().(*GroupBuilder) 41 | } 42 | 43 | func (s *GroupBuilderStack) Pop() *GroupBuilder { 44 | return s.interfaceStack.Pop().(*GroupBuilder) 45 | } 46 | 47 | func (s *GroupBuilderStack) Push(value *GroupBuilder) { 48 | s.interfaceStack.Push(value) 49 | } 50 | 51 | type IntStack struct { 52 | interfaceStack InterfaceStack 53 | } 54 | 55 | func (s *IntStack) Len() int { 56 | return s.interfaceStack.Len() 57 | } 58 | 59 | func (s *IntStack) Peek() int { 60 | return s.interfaceStack.Peek().(int) 61 | } 62 | 63 | func (s *IntStack) Pop() int { 64 | return s.interfaceStack.Pop().(int) 65 | } 66 | 67 | func (s *IntStack) Push(value int) { 68 | s.interfaceStack.Push(value) 69 | } 70 | -------------------------------------------------------------------------------- /go/submatch.go: -------------------------------------------------------------------------------- 1 | package cucumberexpressions 2 | 3 | type Submatch struct { 4 | value *string 5 | start int 6 | end int 7 | } 8 | -------------------------------------------------------------------------------- /java/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | target/ 4 | release.properties 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | dependency-reduced-pom.xml 8 | .classpath 9 | .deps 10 | .project 11 | .settings/ 12 | .tested* 13 | .compared 14 | .built 15 | # Approval tests 16 | acceptance/ 17 | -------------------------------------------------------------------------------- /java/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /java/heuristics.adoc: -------------------------------------------------------------------------------- 1 | = Cucumber Expression - Java Heuristics 2 | 3 | Java doesn't provide a literal syntax for regular expressions, so they 4 | have to be specified as strings. Cucumber uses heuristics to determine 5 | if a string should be interpreted as a Cucumber Expression or a Regular Expression. 6 | 7 | The table below describes the heuristics, along with some examples 8 | 9 | |=== 10 | |Expression|Type|Explanation 11 | 12 | |strings are cukexp by default 13 | |CucumberExpression 14 | |When there are no special characters, a Cucumber Expression is always assumed. 15 | 16 | |^definitely a regexp$ 17 | |RegularExpression 18 | |The presence of anchors assumes a Regular Expression, even if only one of the anchors are present. 19 | 20 | |/surely a regexp/ 21 | |RegularExpression 22 | |Forward slashes always assumes Regular Expression. The slashes themselves are removed. 23 | 24 | |=== 25 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/Argument.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.reflect.Type; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | @API(status = API.Status.STABLE) 10 | public final class Argument { 11 | private final ParameterType parameterType; 12 | private final Group group; 13 | 14 | static List> build(Group group, List> parameterTypes) { 15 | List argGroups = group.getChildren(); 16 | 17 | if (argGroups.size() != parameterTypes.size()) { 18 | // This requires regex injection through a Cucumber expression. 19 | // Regex injection should be be possible any more. 20 | throw new IllegalArgumentException(String.format("Group has %s capture groups, but there were %s parameter types", argGroups.size(), parameterTypes.size())); 21 | } 22 | List> args = new ArrayList<>(argGroups.size()); 23 | for (int i = 0; i < parameterTypes.size(); i++) { 24 | Group argGroup = argGroups.get(i); 25 | ParameterType parameterType = parameterTypes.get(i); 26 | args.add(new Argument<>(argGroup, parameterType)); 27 | } 28 | 29 | return args; 30 | } 31 | 32 | private Argument(Group group, ParameterType parameterType) { 33 | this.group = group; 34 | this.parameterType = parameterType; 35 | } 36 | 37 | public Group getGroup() { 38 | return group; 39 | } 40 | 41 | public T getValue() { 42 | return parameterType.transform(group.getValues()); 43 | } 44 | 45 | public Type getType() { 46 | return parameterType.getType(); 47 | } 48 | 49 | public ParameterType getParameterType() { 50 | return parameterType; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/CaptureGroupTransformer.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | /** 6 | * Transformer for a @{@link ParameterType} with (multiple) capture groups. 7 | * 8 | * @param the type to transform to. 9 | */ 10 | @API(status = API.Status.STABLE) 11 | @FunctionalInterface 12 | public interface CaptureGroupTransformer { 13 | /** 14 | * Transforms multiple strings into to an object. The strings are taken from 15 | * the capture groups in the regular expressions in order. Nested capture 16 | * groups are ignored. If a capture group is optional the corresponding element 17 | * in the array may be null. 18 | * 19 | * @param args the values of the top level capture groups 20 | * @return the transformed object 21 | * @throws Throwable if transformation failed 22 | */ 23 | T transform(String[] args) throws Throwable; 24 | } 25 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/DefaultPatternCompiler.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * Default {@link PatternCompiler} 7 | */ 8 | final class DefaultPatternCompiler implements PatternCompiler { 9 | 10 | @Override 11 | public Pattern compile(String regexp, int flags) { 12 | return Pattern.compile(regexp, flags); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/DuplicateTypeNameException.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | @API(status = API.Status.STABLE) 6 | public class DuplicateTypeNameException extends CucumberExpressionException { 7 | DuplicateTypeNameException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/Expression.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.reflect.Type; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.regex.Pattern; 9 | 10 | @API(status = API.Status.STABLE) 11 | public interface Expression { 12 | List> match(String text, Type... typeHints); 13 | 14 | Pattern getRegexp(); 15 | 16 | String getSource(); 17 | } 18 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/Group.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.util.List; 6 | import java.util.regex.Pattern; 7 | import java.util.stream.Collectors; 8 | 9 | import static java.util.Collections.singletonList; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | 14 | @API(status = API.Status.STABLE) 15 | public class Group { 16 | private final List children; 17 | private final String value; 18 | private final int start; 19 | private final int end; 20 | 21 | Group(String value, int start, int end, List children) { 22 | this.value = value; 23 | this.start = start; 24 | this.end = end; 25 | this.children = children; 26 | } 27 | 28 | public String getValue() { 29 | return value; 30 | } 31 | 32 | public int getStart() { 33 | return start; 34 | } 35 | 36 | public int getEnd() { 37 | return end; 38 | } 39 | 40 | public List getChildren() { 41 | return children; 42 | } 43 | 44 | public List getValues() { 45 | List groups = getChildren().isEmpty() ? singletonList(this) : getChildren(); 46 | return groups.stream() 47 | .map(Group::getValue) 48 | .collect(Collectors.toList()); 49 | } 50 | 51 | /** 52 | * Parse a {@link Pattern} into collection of {@link Group}s 53 | * 54 | * @param expression the expression to decompose 55 | * @return A collection of {@link Group}s, possibly empty but never 56 | * null 57 | */ 58 | public static Collection parse(Pattern expression) { 59 | GroupBuilder builder = TreeRegexp.createGroupBuilder(expression); 60 | return toGroups(builder.getChildren()); 61 | } 62 | 63 | private static List toGroups(List children) { 64 | List list = new ArrayList<>(); 65 | if (children != null) { 66 | for (GroupBuilder child : children) { 67 | list.add(new Group(child.getSource(), child.getStartIndex(), child.getEndIndex(), 68 | toGroups(child.getChildren()))); 69 | } 70 | } 71 | return list; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/GroupBuilder.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.regex.Matcher; 7 | 8 | final class GroupBuilder { 9 | private final List groupBuilders = new ArrayList<>(); 10 | private boolean capturing = true; 11 | private String source; 12 | private int startIndex; 13 | private int endIndex; 14 | 15 | GroupBuilder(int startIndex) { 16 | this.startIndex = startIndex; 17 | } 18 | 19 | void add(GroupBuilder groupBuilder) { 20 | groupBuilders.add(groupBuilder); 21 | } 22 | 23 | Group build(Matcher matcher, Iterator groupIndices) { 24 | int groupIndex = groupIndices.next(); 25 | List children = new ArrayList<>(groupBuilders.size()); 26 | for (GroupBuilder childGroupBuilder : groupBuilders) { 27 | children.add(childGroupBuilder.build(matcher, groupIndices)); 28 | } 29 | return new Group(matcher.group(groupIndex), matcher.start(groupIndex), matcher.end(groupIndex), children); 30 | } 31 | 32 | void setNonCapturing() { 33 | this.capturing = false; 34 | } 35 | 36 | boolean isCapturing() { 37 | return capturing; 38 | } 39 | 40 | void moveChildrenTo(GroupBuilder groupBuilder) { 41 | for (GroupBuilder child : groupBuilders) { 42 | groupBuilder.add(child); 43 | } 44 | } 45 | 46 | List getChildren() { 47 | return groupBuilders; 48 | } 49 | 50 | String getSource() { 51 | return source; 52 | } 53 | 54 | void setSource(String source) { 55 | this.source = source; 56 | } 57 | 58 | int getStartIndex() { 59 | return startIndex; 60 | } 61 | 62 | int getEndIndex() { 63 | return endIndex; 64 | } 65 | 66 | void setEndIndex(int endIndex) { 67 | this.endIndex = endIndex; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/KeyboardFriendlyDecimalFormatSymbols.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import java.text.DecimalFormatSymbols; 4 | import java.util.Locale; 5 | 6 | /** 7 | * A set of localized decimal symbols that can be written on a regular keyboard. 8 | *

9 | * Note quite complete, feel free to make a suggestion. 10 | */ 11 | class KeyboardFriendlyDecimalFormatSymbols { 12 | 13 | static DecimalFormatSymbols getInstance(Locale locale) { 14 | DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); 15 | 16 | // Replace the minus sign with minus-hyphen as available on most keyboards. 17 | if (symbols.getMinusSign() == '\u2212') { 18 | symbols.setMinusSign('-'); 19 | } 20 | 21 | if (symbols.getDecimalSeparator() == '.') { 22 | // For locales that use the period as the decimal separator 23 | // always use the comma for thousands. The alternatives are 24 | // not available on a keyboard 25 | symbols.setGroupingSeparator(','); 26 | } else if (symbols.getDecimalSeparator() == ',') { 27 | // For locales that use the comma as the decimal separator 28 | // always use the period for thousands. The alternatives are 29 | // not available on a keyboard 30 | symbols.setGroupingSeparator('.'); 31 | } 32 | return symbols; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/NumberParser.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import java.math.BigDecimal; 4 | import java.text.DecimalFormat; 5 | import java.text.DecimalFormatSymbols; 6 | import java.text.NumberFormat; 7 | import java.text.ParseException; 8 | import java.util.Locale; 9 | 10 | final class NumberParser { 11 | private final NumberFormat numberFormat; 12 | 13 | NumberParser(Locale locale) { 14 | numberFormat = DecimalFormat.getNumberInstance(locale); 15 | if (numberFormat instanceof DecimalFormat) { 16 | DecimalFormat decimalFormat = (DecimalFormat) numberFormat; 17 | decimalFormat.setParseBigDecimal(true); 18 | DecimalFormatSymbols symbols = KeyboardFriendlyDecimalFormatSymbols.getInstance(locale); 19 | decimalFormat.setDecimalFormatSymbols(symbols); 20 | } 21 | } 22 | 23 | double parseDouble(String s) { 24 | return parse(s).doubleValue(); 25 | } 26 | 27 | float parseFloat(String s) { 28 | return parse(s).floatValue(); 29 | } 30 | 31 | BigDecimal parseBigDecimal(String s) { 32 | if (numberFormat instanceof DecimalFormat) { 33 | return (BigDecimal) parse(s); 34 | } 35 | // Fall back to default big decimal format 36 | // if the locale does not have a DecimalFormat 37 | return new BigDecimal(s); 38 | } 39 | 40 | private Number parse(String s) { 41 | try { 42 | return numberFormat.parse(s); 43 | } catch (ParseException e) { 44 | throw new CucumberExpressionException("Failed to parse number", e); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/ParameterByTypeTransformer.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.lang.reflect.Type; 6 | 7 | /** 8 | * The {@link ParameterTypeRegistry} uses the default transformer 9 | * to execute all transforms for built-in parameter types and all 10 | * anonymous types. 11 | */ 12 | @API(status = API.Status.STABLE) 13 | @FunctionalInterface 14 | public interface ParameterByTypeTransformer { 15 | 16 | Object transform(String fromValue, Type toValueType) throws Throwable; 17 | } 18 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/PatternCompiler.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.apiguardian.api.API; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * Abstracts creation of new {@link Pattern}. In some platforms and Java versions some flags are not supported (e.g {@link Pattern#UNICODE_CHARACTER_CLASS} on Android) - clients for those platforms should provide resource {@code META-INF/services/io.cucumber.cucumberexpressions.PatternCompiler} pointing to implementation of this interface. 9 | * 10 | * @see DefaultPatternCompiler 11 | * @see java.util.ServiceLoader 12 | */ 13 | @API(status = API.Status.STABLE) 14 | @FunctionalInterface 15 | public interface PatternCompiler { 16 | 17 | /** 18 | * @param regexp regular expression 19 | * @param flags additional flags (e.g. {@link Pattern#UNICODE_CHARACTER_CLASS}) 20 | * @return new {@link Pattern} instance from provided {@code regexp} 21 | */ 22 | Pattern compile(String regexp, int flags); 23 | } 24 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/PatternCompilerProvider.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.ServiceLoader; 7 | 8 | final class PatternCompilerProvider { 9 | // visible from tests 10 | static PatternCompiler service; 11 | 12 | private PatternCompilerProvider() { 13 | } 14 | 15 | static synchronized PatternCompiler getCompiler() { 16 | if (service == null) { 17 | ServiceLoader loader = ServiceLoader.load(PatternCompiler.class); 18 | Iterator iterator = loader.iterator(); 19 | findPatternCompiler(iterator); 20 | } 21 | return service; 22 | } 23 | 24 | static void findPatternCompiler(Iterator iterator) { 25 | if (iterator.hasNext()) { 26 | service = iterator.next(); 27 | if (iterator.hasNext()) { 28 | throwMoreThanOneCompilerException(iterator); 29 | } 30 | } else { 31 | service = new DefaultPatternCompiler(); 32 | } 33 | } 34 | 35 | private static void throwMoreThanOneCompilerException(Iterator iterator) { 36 | List> allCompilers = new ArrayList<>(); 37 | allCompilers.add(service.getClass()); 38 | while (iterator.hasNext()) { 39 | allCompilers.add(iterator.next().getClass()); 40 | } 41 | throw new IllegalStateException("More than one PatternCompiler: " + allCompilers); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/Transformer.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | /** 4 | * Transformer for a @{@link ParameterType} with zero or one capture groups. 5 | * 6 | * @param the type to transform to. 7 | */ 8 | @FunctionalInterface 9 | public interface Transformer { 10 | /** 11 | * Transforms a string into to an object. The string is either taken 12 | * from the sole capture group or matches the whole expression. Nested 13 | * capture groups are ignored. 14 | *

15 | * If the capture group is optional arg may be null. 16 | * 17 | * @param arg the value of the single capture group 18 | * @return the transformed object 19 | * @throws Throwable if transformation failed 20 | */ 21 | T transform(String arg) throws Throwable; 22 | } 23 | -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/TypeReference.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | 6 | public abstract class TypeReference { 7 | 8 | private final Type type; 9 | 10 | protected TypeReference() { 11 | Type superclass = getClass().getGenericSuperclass(); 12 | if (superclass instanceof Class) { 13 | throw new CucumberExpressionException("Missing type parameter: " + superclass); 14 | } 15 | this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; 16 | } 17 | 18 | public Type getType() { 19 | return this.type; 20 | } 21 | } -------------------------------------------------------------------------------- /java/src/main/java/io/cucumber/cucumberexpressions/UndefinedParameterTypeException.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import io.cucumber.cucumberexpressions.Ast.Node; 4 | import org.apiguardian.api.API; 5 | 6 | @API(status = API.Status.STABLE) 7 | public final class UndefinedParameterTypeException extends CucumberExpressionException { 8 | private final String undefinedParameterTypeName; 9 | 10 | UndefinedParameterTypeException(String message, String undefinedParameterTypeName) { 11 | super(message); 12 | this.undefinedParameterTypeName = undefinedParameterTypeName; 13 | } 14 | 15 | public String getUndefinedParameterTypeName() { 16 | return undefinedParameterTypeName; 17 | } 18 | 19 | static CucumberExpressionException createUndefinedParameterType(Node node, String expression, String undefinedParameterTypeName) { 20 | return new UndefinedParameterTypeException(message( 21 | node.start(), 22 | expression, 23 | pointAt(node), 24 | "Undefined parameter type '" +undefinedParameterTypeName+ "'", 25 | "Please register a ParameterType for '"+undefinedParameterTypeName+"'"), undefinedParameterTypeName); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /java/src/test/java/io/cucumber/cucumberexpressions/ArgumentTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.Locale; 7 | 8 | import static java.util.Collections.singletonList; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class ArgumentTest { 12 | @Test 13 | public void exposes_parameter_type() { 14 | TreeRegexp treeRegexp = new TreeRegexp("three (.*) mice"); 15 | ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); 16 | List> arguments = Argument.build( 17 | treeRegexp.match("three blind mice"), 18 | singletonList(parameterTypeRegistry.lookupByTypeName("string"))); 19 | Argument argument = arguments.get(0); 20 | assertEquals("string", argument.getParameterType().getName()); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /java/src/test/java/io/cucumber/cucumberexpressions/EnumParameterTypeTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.Locale; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | public class EnumParameterTypeTest { 11 | 12 | public enum Mood { 13 | happy, 14 | meh, 15 | sad 16 | } 17 | 18 | @Test 19 | public void converts_to_enum() { 20 | ParameterTypeRegistry registry = new ParameterTypeRegistry(Locale.ENGLISH); 21 | registry.defineParameterType(ParameterType.fromEnum(Mood.class)); 22 | 23 | CucumberExpression expression = new CucumberExpression("I am {Mood}", registry); 24 | List> args = expression.match("I am happy"); 25 | assertEquals(Mood.happy, args.get(0).getValue()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /java/src/test/java/io/cucumber/cucumberexpressions/GenericParameterTypeTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | import java.util.Locale; 7 | 8 | import static java.util.Arrays.asList; 9 | import static java.util.Collections.singletonList; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | public class GenericParameterTypeTest { 13 | 14 | @Test 15 | public void transforms_to_a_list_of_string() { 16 | ParameterTypeRegistry parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH); 17 | parameterTypeRegistry.defineParameterType(new ParameterType<>( 18 | "stringlist", 19 | singletonList(".*"), 20 | new TypeReference>() { 21 | }.getType(), 22 | new CaptureGroupTransformer>() { 23 | @Override 24 | public List transform(String... args) { 25 | return asList(args[0].split(",")); 26 | } 27 | }, 28 | false, 29 | false) 30 | ); 31 | Expression expression = new CucumberExpression("I have {stringlist} yay", parameterTypeRegistry); 32 | List> args = expression.match("I have three,blind,mice yay"); 33 | assertEquals(asList("three", "blind", "mice"), args.get(0).getValue()); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /java/src/test/java/io/cucumber/cucumberexpressions/ParameterTypeComparatorTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.SortedSet; 8 | import java.util.TreeSet; 9 | 10 | import static java.util.Arrays.asList; 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertNotNull; 13 | 14 | public class ParameterTypeComparatorTest { 15 | 16 | public static class A { 17 | A(String s) { 18 | assertNotNull(s); 19 | } 20 | } 21 | 22 | public static class B { 23 | B(String s) { 24 | assertNotNull(s); 25 | } 26 | } 27 | 28 | public static class C { 29 | C(String s) { 30 | assertNotNull(s); 31 | } 32 | } 33 | 34 | public static class D { 35 | D(String s) { 36 | assertNotNull(s); 37 | } 38 | } 39 | 40 | @Test 41 | public void sorts_parameter_types_by_preferential_then_name() { 42 | SortedSet> set = new TreeSet<>(); 43 | set.add(new ParameterType<>("c", "c", C.class, C::new, false, true)); 44 | set.add(new ParameterType<>("a", "a", A.class, A::new, false, false)); 45 | set.add(new ParameterType<>("d", "d", D.class, D::new, false, false)); 46 | set.add(new ParameterType<>("b", "b", B.class, B::new, false, true)); 47 | 48 | List names = new ArrayList<>(); 49 | for (ParameterType parameterType : set) { 50 | names.add(parameterType.getName()); 51 | } 52 | assertEquals(asList("b", "c", "a", "d"), names); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /java/src/test/java/io/cucumber/cucumberexpressions/RegexpUtilsTest.java: -------------------------------------------------------------------------------- 1 | package io.cucumber.cucumberexpressions; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static io.cucumber.cucumberexpressions.RegexpUtils.escapeRegex; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | class RegexpUtilsTest { 9 | 10 | @Test 11 | void escape_regex_characters(){ 12 | assertEquals("hello \\$world", escapeRegex("hello $world")); 13 | } 14 | 15 | @Test 16 | void escape_all_regexp_characters() { 17 | assertEquals("\\^\\$\\[\\]\\(\\)\\{\\}\\.\\|\\?\\*\\+\\\\", escapeRegex("^$[](){}.|?*+\\")); 18 | } 19 | 20 | @Test 21 | void escape_escaped_regexp_characters() { 22 | assertEquals("\\^\\$\\[\\]\\\\\\(\\\\\\)\\{\\}\\\\\\\\\\.\\|\\?\\*\\+", escapeRegex("^$[]\\(\\){}\\\\.|?*+")); 23 | } 24 | 25 | 26 | @Test 27 | void do_not_escape_when_there_is_nothing_to_escape() { 28 | assertEquals("hello world", escapeRegex("hello world")); 29 | } 30 | 31 | @Test 32 | void gives_no_error_for_unicode_characters() { 33 | assertEquals("🥒", escapeRegex("🥒")); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /javascript/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | dist-try/ 3 | .idea/ 4 | .nyc_output/ 5 | coverage/ 6 | node_modules/ 7 | yarn.lock 8 | *.log 9 | .deps 10 | .tested* 11 | .linted 12 | .built* 13 | .compared 14 | .codegen 15 | acceptance/ 16 | storybook-static 17 | *-go 18 | *.iml 19 | .vscode-test 20 | 21 | # stryker temp files 22 | .stryker-tmp 23 | reports 24 | -------------------------------------------------------------------------------- /javascript/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "loader": "ts-node/esm", 3 | "extension": ["ts", "tsx"], 4 | "recursive": true, 5 | "timeout": 10000 6 | } 7 | -------------------------------------------------------------------------------- /javascript/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /javascript/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /javascript/package.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cucumber/cucumber-expressions", 3 | "type": "commonjs" 4 | } 5 | -------------------------------------------------------------------------------- /javascript/src/Argument.ts: -------------------------------------------------------------------------------- 1 | import CucumberExpressionError from './CucumberExpressionError.js' 2 | import Group from './Group.js' 3 | import ParameterType from './ParameterType.js' 4 | 5 | export default class Argument { 6 | public static build( 7 | group: Group, 8 | parameterTypes: readonly ParameterType[] 9 | ): readonly Argument[] { 10 | const argGroups = group.children 11 | 12 | if (argGroups.length !== parameterTypes.length) { 13 | throw new CucumberExpressionError( 14 | `Group has ${argGroups.length} capture groups (${argGroups.map( 15 | (g) => g.value 16 | )}), but there were ${parameterTypes.length} parameter types (${parameterTypes.map( 17 | (p) => p.name 18 | )})` 19 | ) 20 | } 21 | 22 | return parameterTypes.map((parameterType, i) => new Argument(argGroups[i], parameterType)) 23 | } 24 | 25 | constructor( 26 | public readonly group: Group, 27 | public readonly parameterType: ParameterType 28 | ) { 29 | this.group = group 30 | this.parameterType = parameterType 31 | } 32 | 33 | /** 34 | * Get the value returned by the parameter type's transformer function. 35 | * 36 | * @param thisObj the object in which the transformer function is applied. 37 | */ 38 | public getValue(thisObj: unknown): T | null { 39 | const groupValues = this.group ? this.group.values : null 40 | return this.parameterType.transform(thisObj, groupValues) 41 | } 42 | 43 | public getParameterType() { 44 | return this.parameterType 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /javascript/src/CombinatorialGeneratedExpressionFactory.ts: -------------------------------------------------------------------------------- 1 | import GeneratedExpression from './GeneratedExpression.js' 2 | import ParameterType from './ParameterType.js' 3 | 4 | // 256 generated expressions ought to be enough for anybody 5 | const MAX_EXPRESSIONS = 256 6 | 7 | export default class CombinatorialGeneratedExpressionFactory { 8 | constructor( 9 | private readonly expressionTemplate: string, 10 | private readonly parameterTypeCombinations: Array>> 11 | ) { 12 | this.expressionTemplate = expressionTemplate 13 | } 14 | 15 | public generateExpressions(): readonly GeneratedExpression[] { 16 | const generatedExpressions: GeneratedExpression[] = [] 17 | this.generatePermutations(generatedExpressions, 0, []) 18 | return generatedExpressions 19 | } 20 | 21 | private generatePermutations( 22 | generatedExpressions: GeneratedExpression[], 23 | depth: number, 24 | currentParameterTypes: Array> 25 | ) { 26 | if (generatedExpressions.length >= MAX_EXPRESSIONS) { 27 | return 28 | } 29 | 30 | if (depth === this.parameterTypeCombinations.length) { 31 | generatedExpressions.push( 32 | new GeneratedExpression(this.expressionTemplate, currentParameterTypes) 33 | ) 34 | return 35 | } 36 | 37 | // tslint:disable-next-line:prefer-for-of 38 | for (let i = 0; i < this.parameterTypeCombinations[depth].length; ++i) { 39 | // Avoid recursion if no elements can be added. 40 | if (generatedExpressions.length >= MAX_EXPRESSIONS) { 41 | return 42 | } 43 | 44 | const newCurrentParameterTypes = currentParameterTypes.slice() // clone 45 | newCurrentParameterTypes.push(this.parameterTypeCombinations[depth][i]) 46 | this.generatePermutations(generatedExpressions, depth + 1, newCurrentParameterTypes) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /javascript/src/CucumberExpressionError.ts: -------------------------------------------------------------------------------- 1 | export default class CucumberExpressionError extends Error {} 2 | -------------------------------------------------------------------------------- /javascript/src/ExpressionFactory.ts: -------------------------------------------------------------------------------- 1 | import CucumberExpression from './CucumberExpression.js' 2 | import ParameterTypeRegistry from './ParameterTypeRegistry.js' 3 | import RegularExpression from './RegularExpression.js' 4 | import { Expression } from './types.js' 5 | 6 | export default class ExpressionFactory { 7 | public constructor(private readonly parameterTypeRegistry: ParameterTypeRegistry) {} 8 | 9 | public createExpression(expression: string | RegExp): Expression { 10 | return typeof expression === 'string' 11 | ? new CucumberExpression(expression, this.parameterTypeRegistry) 12 | : new RegularExpression(expression, this.parameterTypeRegistry) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /javascript/src/GeneratedExpression.ts: -------------------------------------------------------------------------------- 1 | import ParameterType from './ParameterType.js' 2 | import { ParameterInfo } from './types.js' 3 | 4 | export default class GeneratedExpression { 5 | constructor( 6 | private readonly expressionTemplate: string, 7 | public readonly parameterTypes: readonly ParameterType[] 8 | ) {} 9 | 10 | get source() { 11 | return format(this.expressionTemplate, ...this.parameterTypes.map((t) => t.name || '')) 12 | } 13 | 14 | /** 15 | * Returns an array of parameter names to use in generated function/method signatures 16 | * 17 | * @returns {ReadonlyArray.} 18 | */ 19 | get parameterNames(): readonly string[] { 20 | return this.parameterInfos.map((i) => `${i.name}${i.count === 1 ? '' : i.count.toString()}`) 21 | } 22 | 23 | /** 24 | * Returns an array of ParameterInfo to use in generated function/method signatures 25 | */ 26 | get parameterInfos(): readonly ParameterInfo[] { 27 | const usageByTypeName: { [key: string]: number } = {} 28 | return this.parameterTypes.map((t) => getParameterInfo(t, usageByTypeName)) 29 | } 30 | } 31 | 32 | function getParameterInfo( 33 | parameterType: ParameterType, 34 | usageByName: { [key: string]: number } 35 | ): ParameterInfo { 36 | const name = parameterType.name || '' 37 | let counter = usageByName[name] 38 | counter = counter ? counter + 1 : 1 39 | usageByName[name] = counter 40 | let type: string | null 41 | if (parameterType.type) { 42 | if (typeof parameterType.type === 'string') { 43 | type = parameterType.type 44 | } else if ('name' in parameterType.type) { 45 | type = parameterType.type.name 46 | } else { 47 | type = null 48 | } 49 | } else { 50 | type = null 51 | } 52 | return { 53 | type, 54 | name, 55 | count: counter, 56 | } 57 | } 58 | 59 | function format(pattern: string, ...args: readonly string[]): string { 60 | return pattern.replace(/{(\d+)}/g, (match, number) => args[number]) 61 | } 62 | -------------------------------------------------------------------------------- /javascript/src/Group.ts: -------------------------------------------------------------------------------- 1 | export default class Group { 2 | constructor( 3 | public readonly value: string, 4 | public readonly start: number | undefined, 5 | public readonly end: number | undefined, 6 | public readonly children: readonly Group[] 7 | ) {} 8 | 9 | get values(): string[] | null { 10 | return (this.children.length === 0 ? [this] : this.children).map((g) => g.value) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /javascript/src/GroupBuilder.ts: -------------------------------------------------------------------------------- 1 | import { RegExpExecArray } from 'regexp-match-indices' 2 | 3 | import Group from './Group.js' 4 | 5 | export default class GroupBuilder { 6 | public source: string 7 | public capturing = true 8 | private readonly groupBuilders: GroupBuilder[] = [] 9 | 10 | public add(groupBuilder: GroupBuilder) { 11 | this.groupBuilders.push(groupBuilder) 12 | } 13 | 14 | public build(match: RegExpExecArray, nextGroupIndex: () => number): Group { 15 | const groupIndex = nextGroupIndex() 16 | const children = this.groupBuilders.map((gb) => gb.build(match, nextGroupIndex)) 17 | const value = match[groupIndex] 18 | const index = match.indices[groupIndex] 19 | const start = index ? index[0] : undefined 20 | const end = index ? index[1] : undefined 21 | return new Group(value, start, end, children) 22 | } 23 | 24 | public setNonCapturing() { 25 | this.capturing = false 26 | } 27 | 28 | get children() { 29 | return this.groupBuilders 30 | } 31 | 32 | public moveChildrenTo(groupBuilder: GroupBuilder) { 33 | this.groupBuilders.forEach((child) => groupBuilder.add(child)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /javascript/src/RegularExpression.ts: -------------------------------------------------------------------------------- 1 | import Argument from './Argument.js' 2 | import ParameterType from './ParameterType.js' 3 | import ParameterTypeRegistry from './ParameterTypeRegistry.js' 4 | import TreeRegexp from './TreeRegexp.js' 5 | import { Expression } from './types.js' 6 | 7 | export default class RegularExpression implements Expression { 8 | private readonly treeRegexp: TreeRegexp 9 | 10 | constructor( 11 | public readonly regexp: RegExp, 12 | private readonly parameterTypeRegistry: ParameterTypeRegistry 13 | ) { 14 | this.treeRegexp = new TreeRegexp(regexp) 15 | } 16 | 17 | public match(text: string): readonly Argument[] | null { 18 | const group = this.treeRegexp.match(text) 19 | if (!group) { 20 | return null 21 | } 22 | 23 | const parameterTypes = this.treeRegexp.groupBuilder.children.map((groupBuilder) => { 24 | const parameterTypeRegexp = groupBuilder.source 25 | 26 | const parameterType = this.parameterTypeRegistry.lookupByRegexp( 27 | parameterTypeRegexp, 28 | this.regexp, 29 | text 30 | ) 31 | return ( 32 | parameterType || 33 | new ParameterType( 34 | undefined, 35 | parameterTypeRegexp, 36 | String, 37 | (s) => (s === undefined ? null : s), 38 | false, 39 | false 40 | ) 41 | ) 42 | }) 43 | 44 | return Argument.build(group, parameterTypes) 45 | } 46 | 47 | get source(): string { 48 | return this.regexp.source 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /javascript/src/index.ts: -------------------------------------------------------------------------------- 1 | import Argument from './Argument.js' 2 | import { Located, Node, NodeType, Token, TokenType } from './Ast.js' 3 | import CucumberExpression from './CucumberExpression.js' 4 | import CucumberExpressionGenerator from './CucumberExpressionGenerator.js' 5 | import ExpressionFactory from './ExpressionFactory.js' 6 | import GeneratedExpression from './GeneratedExpression.js' 7 | import Group from './Group.js' 8 | import ParameterType, { RegExps, StringOrRegExp } from './ParameterType.js' 9 | import ParameterTypeRegistry from './ParameterTypeRegistry.js' 10 | import RegularExpression from './RegularExpression.js' 11 | import { Expression } from './types.js' 12 | 13 | export { 14 | Argument, 15 | CucumberExpression, 16 | CucumberExpressionGenerator, 17 | Expression, 18 | ExpressionFactory, 19 | GeneratedExpression, 20 | Group, 21 | Located, 22 | Node, 23 | NodeType, 24 | ParameterType, 25 | ParameterTypeRegistry, 26 | RegExps, 27 | RegularExpression, 28 | StringOrRegExp, 29 | Token, 30 | TokenType, 31 | } 32 | -------------------------------------------------------------------------------- /javascript/src/types.ts: -------------------------------------------------------------------------------- 1 | import Argument from './Argument.js' 2 | import ParameterType from './ParameterType.js' 3 | 4 | export interface DefinesParameterType { 5 | defineParameterType(parameterType: ParameterType): void 6 | } 7 | 8 | export interface Expression { 9 | readonly source: string 10 | match(text: string): readonly Argument[] | null 11 | } 12 | 13 | export type ParameterInfo = { 14 | /** 15 | * The string representation of the original ParameterType#type property 16 | */ 17 | type: string | null 18 | /** 19 | * The parameter type name 20 | */ 21 | name: string 22 | /** 23 | * The number of times this name has been used so far 24 | */ 25 | count: number 26 | } 27 | -------------------------------------------------------------------------------- /javascript/stryker.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", 3 | "packageManager": "npm", 4 | "reporters": [ 5 | "html", 6 | "clear-text", 7 | "progress", 8 | "json" 9 | ], 10 | "cleanTempDir": false, 11 | "testRunner": "mocha", 12 | "buildCommand": "npm run build:cjs", 13 | "mochaOptions": { 14 | "spec": [ "dist/cjs/test/**/*.js" ] 15 | }, 16 | "coverageAnalysis": "perTest" 17 | } 18 | -------------------------------------------------------------------------------- /javascript/test/ArgumentTest.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import Argument from '../src/Argument.js' 4 | import ParameterTypeRegistry from '../src/ParameterTypeRegistry.js' 5 | import TreeRegexp from '../src/TreeRegexp.js' 6 | 7 | describe('Argument', () => { 8 | it('exposes getParameterTypeName()', () => { 9 | const treeRegexp = new TreeRegexp('three (.*) mice') 10 | const parameterTypeRegistry = new ParameterTypeRegistry() 11 | const group = treeRegexp.match('three blind mice')! 12 | const args = Argument.build(group, [parameterTypeRegistry.lookupByTypeName('string')!]) 13 | const argument = args[0] 14 | assert.strictEqual(argument.getParameterType().name, 'string') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /javascript/test/CombinatorialGeneratedExpressionFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | import CombinatorialGeneratedExpressionFactory from '../src/CombinatorialGeneratedExpressionFactory.js' 4 | import ParameterType from '../src/ParameterType.js' 5 | 6 | describe('CucumberExpressionGenerator', () => { 7 | it('generates multiple expressions', () => { 8 | const parameterTypeCombinations = [ 9 | [ 10 | new ParameterType('color', /red|blue|yellow/, null, (s) => s, false, true), 11 | new ParameterType('csscolor', /red|blue|yellow/, null, (s) => s, false, true), 12 | ], 13 | [ 14 | new ParameterType('date', /\d{4}-\d{2}-\d{2}/, null, (s) => s, false, true), 15 | new ParameterType('datetime', /\d{4}-\d{2}-\d{2}/, null, (s) => s, false, true), 16 | new ParameterType('timestamp', /\d{4}-\d{2}-\d{2}/, null, (s) => s, false, true), 17 | ], 18 | ] 19 | 20 | const factory = new CombinatorialGeneratedExpressionFactory( 21 | 'I bought a {{0}} ball on {{1}}', 22 | parameterTypeCombinations 23 | ) 24 | const expressions = factory.generateExpressions().map((ge) => ge.source) 25 | assert.deepStrictEqual(expressions, [ 26 | 'I bought a {color} ball on {date}', 27 | 'I bought a {color} ball on {datetime}', 28 | 'I bought a {color} ball on {timestamp}', 29 | 'I bought a {csscolor} ball on {date}', 30 | 'I bought a {csscolor} ball on {datetime}', 31 | 'I bought a {csscolor} ball on {timestamp}', 32 | ]) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /javascript/test/CucumberExpressionParserTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import fs from 'fs' 3 | import { glob } from 'glob' 4 | import yaml from 'js-yaml' 5 | 6 | import CucumberExpressionError from '../src/CucumberExpressionError.js' 7 | import CucumberExpressionParser from '../src/CucumberExpressionParser.js' 8 | import { testDataDir } from './testDataDir.js' 9 | 10 | type Expectation = { 11 | expression: string 12 | expected_ast?: unknown 13 | exception?: string 14 | } 15 | 16 | describe('CucumberExpressionParser', () => { 17 | for (const path of glob.sync(`${testDataDir}/cucumber-expression/parser/*.yaml`)) { 18 | const expectation = yaml.load(fs.readFileSync(path, 'utf-8')) as Expectation 19 | it(`parses ${path}`, () => { 20 | const parser = new CucumberExpressionParser() 21 | if (expectation.expected_ast !== undefined) { 22 | const node = parser.parse(expectation.expression) 23 | assert.deepStrictEqual( 24 | JSON.parse(JSON.stringify(node)), // Removes type information. 25 | expectation.expected_ast 26 | ) 27 | } else if (expectation.exception !== undefined) { 28 | assert.throws(() => { 29 | parser.parse(expectation.expression) 30 | }, new CucumberExpressionError(expectation.exception)) 31 | } else { 32 | throw new Error( 33 | `Expectation must have expected or exception: ${JSON.stringify(expectation)}` 34 | ) 35 | } 36 | }) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /javascript/test/CucumberExpressionTokenizerTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import fs from 'fs' 3 | import { glob } from 'glob' 4 | import yaml from 'js-yaml' 5 | 6 | import CucumberExpressionError from '../src/CucumberExpressionError.js' 7 | import CucumberExpressionTokenizer from '../src/CucumberExpressionTokenizer.js' 8 | import { testDataDir } from './testDataDir.js' 9 | 10 | type Expectation = { 11 | expression: string 12 | expected_tokens?: unknown 13 | exception?: string 14 | } 15 | 16 | describe('CucumberExpressionTokenizer', () => { 17 | for (const path of glob.sync(`${testDataDir}/cucumber-expression/tokenizer/*.yaml`)) { 18 | const expectation = yaml.load(fs.readFileSync(path, 'utf-8')) as Expectation 19 | it(`tokenizes ${path}`, () => { 20 | const tokenizer = new CucumberExpressionTokenizer() 21 | if (expectation.expected_tokens !== undefined) { 22 | const tokens = tokenizer.tokenize(expectation.expression) 23 | assert.deepStrictEqual( 24 | JSON.parse(JSON.stringify(tokens)), // Removes type information. 25 | expectation.expected_tokens 26 | ) 27 | } else if (expectation.exception !== undefined) { 28 | assert.throws(() => { 29 | tokenizer.tokenize(expectation.expression) 30 | }, new CucumberExpressionError(expectation.exception)) 31 | } else { 32 | throw new Error( 33 | `Expectation must have expected_tokens or exception: ${JSON.stringify(expectation)}` 34 | ) 35 | } 36 | }) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /javascript/test/CucumberExpressionTransformationTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import fs from 'fs' 3 | import { glob } from 'glob' 4 | import yaml from 'js-yaml' 5 | 6 | import CucumberExpression from '../src/CucumberExpression.js' 7 | import ParameterTypeRegistry from '../src/ParameterTypeRegistry.js' 8 | import { testDataDir } from './testDataDir.js' 9 | 10 | type Expectation = { 11 | expression: string 12 | expected_regex: string 13 | } 14 | 15 | describe('CucumberExpression', () => { 16 | for (const path of glob.sync(`${testDataDir}/cucumber-expression/transformation/*.yaml`)) { 17 | const expectation = yaml.load(fs.readFileSync(path, 'utf-8')) as Expectation 18 | it(`transforms ${path}`, () => { 19 | const parameterTypeRegistry = new ParameterTypeRegistry() 20 | const expression = new CucumberExpression(expectation.expression, parameterTypeRegistry) 21 | assert.deepStrictEqual(expression.regexp.source, expectation.expected_regex) 22 | }) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /javascript/test/ExpressionFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import CucumberExpression from '../src/CucumberExpression.js' 4 | import ExpressionFactory from '../src/ExpressionFactory.js' 5 | import ParameterTypeRegistry from '../src/ParameterTypeRegistry.js' 6 | import RegularExpression from '../src/RegularExpression.js' 7 | 8 | describe('ExpressionFactory', () => { 9 | let expressionFactory: ExpressionFactory 10 | beforeEach(() => { 11 | expressionFactory = new ExpressionFactory(new ParameterTypeRegistry()) 12 | }) 13 | 14 | it('creates a RegularExpression', () => { 15 | assert.strictEqual(expressionFactory.createExpression(/x/).constructor, RegularExpression) 16 | }) 17 | 18 | it('creates a CucumberExpression', () => { 19 | assert.strictEqual(expressionFactory.createExpression('x').constructor, CucumberExpression) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /javascript/test/ParameterTypeRegistryTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | import ParameterType from '../src/ParameterType.js' 4 | import ParameterTypeRegistry from '../src/ParameterTypeRegistry.js' 5 | 6 | class Name { 7 | constructor(public readonly name: string) {} 8 | } 9 | class Person { 10 | constructor(public readonly name: string) {} 11 | } 12 | class Place { 13 | constructor(public readonly name: string) {} 14 | } 15 | 16 | const CAPITALISED_WORD = /[A-Z]+\w+/ 17 | 18 | describe('ParameterTypeRegistry', () => { 19 | let registry: ParameterTypeRegistry 20 | beforeEach(() => { 21 | registry = new ParameterTypeRegistry() 22 | }) 23 | 24 | it('does not allow more than one preferential parameter type for each regexp', () => { 25 | registry.defineParameterType( 26 | new ParameterType('name', CAPITALISED_WORD, Name, (s) => new Name(s), true, true) 27 | ) 28 | registry.defineParameterType( 29 | new ParameterType('person', CAPITALISED_WORD, Person, (s) => new Person(s), true, false) 30 | ) 31 | try { 32 | registry.defineParameterType( 33 | new ParameterType('place', CAPITALISED_WORD, Place, (s) => new Place(s), true, true) 34 | ) 35 | throw new Error('Should have failed') 36 | } catch (err) { 37 | assert.strictEqual( 38 | err.message, 39 | `There can only be one preferential parameter type per regexp. The regexp ${CAPITALISED_WORD} is used for two preferential parameter types, {name} and {place}` 40 | ) 41 | } 42 | }) 43 | 44 | it('looks up preferential parameter type by regexp', () => { 45 | const name = new ParameterType('name', /[A-Z]+\w+/, null, (s) => new Name(s), true, false) 46 | const person = new ParameterType('person', /[A-Z]+\w+/, null, (s) => new Person(s), true, true) 47 | const place = new ParameterType('place', /[A-Z]+\w+/, null, (s) => new Place(s), true, false) 48 | 49 | registry.defineParameterType(name) 50 | registry.defineParameterType(person) 51 | registry.defineParameterType(place) 52 | 53 | assert.strictEqual( 54 | registry.lookupByRegexp('[A-Z]+\\w+', /([A-Z]+\w+) and ([A-Z]+\w+)/, 'Lisa and Bob'), 55 | person 56 | ) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /javascript/test/ParameterTypeTest.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import ParameterType from '../src/ParameterType.js' 4 | import ParameterTypeRegistry from '../src/ParameterTypeRegistry.js' 5 | 6 | describe('ParameterType', () => { 7 | it('does not allow ignore flag on regexp', () => { 8 | assert.throws( 9 | () => new ParameterType('case-insensitive', /[a-z]+/i, String, (s) => s, true, true), 10 | { message: "ParameterType Regexps can't use flag 'i'" } 11 | ) 12 | }) 13 | 14 | it('has a type name for {int}', () => { 15 | const r = new ParameterTypeRegistry() 16 | const t = r.lookupByTypeName('int')! 17 | // @ts-ignore 18 | assert.strictEqual(t.type.name, 'Number') 19 | }) 20 | 21 | it('has a type name for {bigint}', () => { 22 | const r = new ParameterTypeRegistry() 23 | const t = r.lookupByTypeName('biginteger')! 24 | // @ts-ignore 25 | assert.strictEqual(t.type.name, 'BigInt') 26 | }) 27 | 28 | it('has a type name for {word}', () => { 29 | const r = new ParameterTypeRegistry() 30 | const t = r.lookupByTypeName('word')! 31 | // @ts-ignore 32 | assert.strictEqual(t.type.name, 'String') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /javascript/test/testDataDir.ts: -------------------------------------------------------------------------------- 1 | export const testDataDir = process.env.CUCUMBER_EXPRESSIONS_TEST_DATA_DIR || '../testdata' 2 | -------------------------------------------------------------------------------- /javascript/tsconfig.build-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs", 5 | "module": "CommonJS", 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /javascript/tsconfig.build-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "module": "ES6", 5 | "outDir": "dist/esm" 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /javascript/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "rootDir": ".", 9 | "noEmit": false 10 | }, 11 | "include": [ 12 | "src", 13 | "test" 14 | ], 15 | } -------------------------------------------------------------------------------- /javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "sourceMap": true, 6 | "allowJs": false, 7 | "resolveJsonModule": true, 8 | "esModuleInterop": true, 9 | "noImplicitAny": true, 10 | "downlevelIteration": true, 11 | "skipLibCheck": true, 12 | "strictNullChecks": true, 13 | "experimentalDecorators": true, 14 | "module": "ESNext", 15 | "lib": [ 16 | "ES2022" 17 | ], 18 | "target": "ES2022", 19 | "moduleResolution": "node", 20 | "allowSyntheticDefaultImports": true, 21 | "noEmit": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | venv/ 3 | # Pytest 4 | .pytest_cache 5 | -------------------------------------------------------------------------------- /python/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 22.3.0 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/PyCQA/flake8 7 | rev: 4.0.1 8 | hooks: 9 | - id: flake8 10 | args: ['--max-line-length=130','--ignore=E203,W503'] 11 | -------------------------------------------------------------------------------- /python/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Expressions for Python 2 | 3 | [The main docs are here](https://github.com/cucumber/cucumber-expressions#readme). 4 | 5 | ## Build system 6 | 7 | This project uses [Poetry](https://python-poetry.org/) as its build system. 8 | In order to develop on this project, please install Poetry as per your system's instructions on the link above. 9 | 10 | ## Tests 11 | 12 | The test suite uses `pytest` as its testing Framework. 13 | 14 | 15 | ### Preparing to run the tests 16 | 17 | In order to set up your dev environment, run the following command from this project's directory: 18 | 19 | ``` python 20 | poetry install 21 | ``` 22 | It will install all package and development requirements, and once that is done it will do a dev-install of the source code. 23 | 24 | You only need to run it once, code changes will propagate directly and do not require running the install again. 25 | 26 | 27 | ### Running the tests 28 | 29 | `pytest` automatically picks up files in the current directory or any subdirectories that have the prefix or suffix of `test_*.py`. 30 | Test function names must start with `test*`. 31 | Test class names must start with `Test*`. 32 | 33 | To run all tests: 34 | 35 | ``` python 36 | poetry run pytest 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /python/cucumber_expressions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-expressions/51272cff745a7065f794768ab8a3d9630ea770eb/python/cucumber_expressions/__init__.py -------------------------------------------------------------------------------- /python/cucumber_expressions/argument.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional, List 4 | 5 | from cucumber_expressions.group import Group 6 | from cucumber_expressions.parameter_type import ParameterType 7 | from cucumber_expressions.tree_regexp import TreeRegexp 8 | from cucumber_expressions.errors import CucumberExpressionError 9 | 10 | 11 | class Argument: 12 | def __init__(self, group, parameter_type): 13 | self._group: Group = group 14 | self.parameter_type: ParameterType = parameter_type 15 | 16 | @staticmethod 17 | def build( 18 | tree_regexp: TreeRegexp, text: str, parameter_types: List 19 | ) -> Optional[List[Argument]]: 20 | match_group = tree_regexp.match(text) 21 | if not match_group: 22 | return None 23 | 24 | arg_groups = match_group.children 25 | 26 | if len(arg_groups) != len(parameter_types): 27 | raise CucumberExpressionError( 28 | f"Group has {len(arg_groups)} capture groups, but there were {len(parameter_types)} parameter types" 29 | ) 30 | 31 | return [ 32 | Argument(arg_group, parameter_type) 33 | for parameter_type, arg_group in zip(parameter_types, arg_groups) 34 | ] 35 | 36 | @property 37 | def value(self): 38 | return self.parameter_type.transform(self.group.values if self.group else None) 39 | 40 | @property 41 | def group(self): 42 | return self._group 43 | -------------------------------------------------------------------------------- /python/cucumber_expressions/combinatorial_generated_expression_factory.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from cucumber_expressions.generated_expression import GeneratedExpression 4 | from cucumber_expressions.parameter_type import ParameterType 5 | 6 | # 256 generated expressions ought to be enough for anybody 7 | MAX_EXPRESSIONS = 256 8 | 9 | 10 | class CombinatorialGeneratedExpressionFactory: 11 | def __init__(self, expression_template, parameter_type_combinations): 12 | self.expression_template = expression_template 13 | self.parameter_type_combinations = parameter_type_combinations 14 | 15 | def generate_expressions(self) -> List[GeneratedExpression]: 16 | generated_expressions = [] 17 | self.generate_permutations(generated_expressions, 0, []) 18 | return generated_expressions 19 | 20 | def generate_permutations( 21 | self, 22 | generated_expressions: List[GeneratedExpression], 23 | depth: int, 24 | current_parameter_types: List[ParameterType], 25 | ): 26 | if len(generated_expressions) >= MAX_EXPRESSIONS: 27 | return 28 | if depth == len(self.parameter_type_combinations): 29 | generated_expressions.append( 30 | GeneratedExpression(self.expression_template, current_parameter_types) 31 | ) 32 | return 33 | for parameter_type_combination in self.parameter_type_combinations[depth]: 34 | if len(generated_expressions) >= MAX_EXPRESSIONS: 35 | return 36 | new_current_parameter_types = current_parameter_types.copy() 37 | new_current_parameter_types.append(parameter_type_combination) 38 | self.generate_permutations( 39 | generated_expressions, depth + 1, new_current_parameter_types 40 | ) 41 | -------------------------------------------------------------------------------- /python/cucumber_expressions/generated_expression.py: -------------------------------------------------------------------------------- 1 | class GeneratedExpression: 2 | def __init__(self, expression_template: str, parameter_types): 3 | self.expression_template = expression_template 4 | self.parameter_types = parameter_types 5 | self.usage_by_type_name = {} 6 | 7 | @property 8 | def source(self): 9 | return self.expression_template % tuple(p.name for p in self.parameter_types) 10 | 11 | @property 12 | def parameter_names(self): 13 | return [self.get_parameter_name(t.name) for t in self.parameter_types] 14 | 15 | def get_parameter_name(self, type_name): 16 | count = self.usage_by_type_name.get(type_name) or 0 17 | count = count + 1 18 | self.usage_by_type_name[type_name] = count 19 | return type_name if count == 1 else f"{type_name}{count}" 20 | -------------------------------------------------------------------------------- /python/cucumber_expressions/group.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | 6 | class Group: 7 | def __init__(self, value: str, start: int, end: int, children: List[Group]): 8 | self._children = children 9 | self._value = value 10 | self._start = start 11 | self._end = end 12 | 13 | @property 14 | def value(self): 15 | return self._value 16 | 17 | @property 18 | def start(self): 19 | return self._start 20 | 21 | @property 22 | def end(self): 23 | return self._end 24 | 25 | @property 26 | def children(self): 27 | return self._children 28 | 29 | @property 30 | def values(self): 31 | return [v.value for v in self.children or [self]] 32 | -------------------------------------------------------------------------------- /python/cucumber_expressions/group_builder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | from cucumber_expressions.group import Group 6 | 7 | 8 | class GroupBuilder: 9 | def __init__(self): 10 | self._group_builders: List[GroupBuilder] = [] 11 | self._capturing = True 12 | self._source: str = "" 13 | self._end_index = None 14 | self._children: List[GroupBuilder] = [] 15 | 16 | def add(self, group_builder: GroupBuilder): 17 | self._group_builders.append(group_builder) 18 | 19 | def build(self, match, group_indices) -> Group: 20 | group_index = next(group_indices) 21 | children: List[Group] = [ 22 | gb.build(match, group_indices) for gb in self._group_builders 23 | ] 24 | return Group( 25 | value=match.group(group_index), 26 | start=match.regs[group_index][0], 27 | end=match.regs[group_index][1], 28 | children=children, 29 | ) 30 | 31 | def move_children_to(self, group_builder: GroupBuilder) -> None: 32 | for child in self._group_builders: 33 | group_builder.add(child) 34 | 35 | @property 36 | def capturing(self): 37 | return self._capturing 38 | 39 | @capturing.setter 40 | def capturing(self, value: bool): 41 | self._capturing = value 42 | 43 | @property 44 | def children(self) -> list[GroupBuilder]: 45 | return self._group_builders 46 | 47 | @property 48 | def source(self) -> str: 49 | return self._source 50 | 51 | @source.setter 52 | def source(self, source: str): 53 | self._source = source 54 | -------------------------------------------------------------------------------- /python/cucumber_expressions/regular_expression.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional, List 3 | 4 | from cucumber_expressions.argument import Argument 5 | from cucumber_expressions.parameter_type import ParameterType 6 | from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry 7 | from cucumber_expressions.tree_regexp import TreeRegexp 8 | 9 | 10 | class RegularExpression: 11 | """Creates a new instance. Use this when the transform types are not known in advance, 12 | and should be determined by the regular expression's capture groups. Use this with 13 | dynamically typed languages.""" 14 | 15 | def __init__( 16 | self, expression_regexp, parameter_type_registry: ParameterTypeRegistry 17 | ): 18 | """Creates a new instance. Use this when the transform types are not known in advance, 19 | and should be determined by the regular expression's capture groups. Use this with 20 | dynamically typed languages. 21 | :param expression_regexp: the regular expression to use 22 | :type expression_regexp: Pattern 23 | :param parameter_type_registry: used to look up parameter types 24 | :type parameter_type_registry: ParameterTypeRegistry 25 | """ 26 | self.expression_regexp = re.compile(expression_regexp) 27 | self.parameter_type_registry = parameter_type_registry 28 | self.tree_regexp: TreeRegexp = TreeRegexp(self.expression_regexp.pattern) 29 | 30 | def match(self, text) -> Optional[List[Argument]]: 31 | return Argument.build( 32 | self.tree_regexp, text, list(self.generate_parameter_types(text)) 33 | ) 34 | 35 | def generate_parameter_types(self, text): 36 | for group_builder in self.tree_regexp.group_builder.children: 37 | parameter_type_regexp = group_builder.source 38 | possible_regexp = self.parameter_type_registry.lookup_by_regexp( 39 | parameter_type_regexp, self.expression_regexp, text 40 | ) 41 | yield possible_regexp or ParameterType( 42 | None, parameter_type_regexp, str, lambda *s: s[0], False, False 43 | ) 44 | 45 | @property 46 | def regexp(self): 47 | return self.expression_regexp.pattern 48 | -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "cucumber-expressions" 3 | version = "18.0.1" 4 | description = "Cucumber Expressions - a simpler alternative to Regular Expressions" 5 | authors = ["Jason Allen "] 6 | license = "MIT" 7 | 8 | readme = "README.md" 9 | 10 | packages = [ 11 | { include = "cucumber_expressions"} 12 | ] 13 | include = [ 14 | { path = "tests", format = "sdist" } 15 | ] 16 | 17 | homepage = "https://github.com/cucumber/cucumber-expressions" 18 | repository = "https://github.com/cucumber/cucumber-expressions" 19 | documentation = "https://github.com/cucumber/cucumber-expressions" 20 | 21 | keywords = ["BDD", "testing", "cucumber", "expressions"] 22 | 23 | classifiers = [ 24 | "Development Status :: 3 - Alpha", 25 | "Environment :: Console", 26 | "Intended Audience :: Developers", 27 | "Operating System :: OS Independent", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: Implementation :: CPython", 35 | "Programming Language :: Python :: Implementation :: PyPy", 36 | "Topic :: Software Development :: Testing", 37 | "Topic :: Software Development :: Libraries :: Python Modules", 38 | "License :: OSI Approved :: MIT License", 39 | ] 40 | 41 | [tool.poetry.dependencies] 42 | python = "^3.8" 43 | 44 | [tool.poetry.dev-dependencies] 45 | pre-commit = "^3.3" 46 | pytest = "^8.0.0" 47 | PyYAML = "^6.0" 48 | 49 | [build-system] 50 | requires = ["poetry-core>=1.0.0"] 51 | build-backend = "poetry.core.masonry.api" 52 | 53 | [tool.isort] 54 | py_version = 38 55 | profile = "black" 56 | force_single_line = true 57 | combine_as_imports = true 58 | lines_between_types = 1 59 | lines_after_imports = 2 60 | src_paths = ["cucumber_expressions", "tests"] 61 | 62 | 63 | [tool.black] 64 | target-version = ['py38'] 65 | -------------------------------------------------------------------------------- /python/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cucumber/cucumber-expressions/51272cff745a7065f794768ab8a3d9630ea770eb/python/tests/__init__.py -------------------------------------------------------------------------------- /python/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import yaml 3 | 4 | 5 | @pytest.fixture 6 | def load_test_yamls(request) -> dict: 7 | """Opens a given test yaml file""" 8 | with open(request.param, encoding="UTF-8") as stream: 9 | yield yaml.safe_load(stream) 10 | -------------------------------------------------------------------------------- /python/tests/definitions.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | MODULE_ROOT_DIR = Path(Path(__file__).resolve()).parent 4 | PROJECT_ROOT_DIR = Path(MODULE_ROOT_DIR).parent.parent 5 | TESTDATA_ROOT_DIR = Path(PROJECT_ROOT_DIR) / "testdata" 6 | -------------------------------------------------------------------------------- /python/tests/test_argument.py: -------------------------------------------------------------------------------- 1 | from cucumber_expressions.argument import Argument 2 | from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry 3 | from cucumber_expressions.tree_regexp import TreeRegexp 4 | 5 | 6 | class TestArgument: 7 | def test_exposes_parameter_type(self): 8 | tree_regexp = TreeRegexp(r"three (.*) mice") 9 | parameter_type_registry = ParameterTypeRegistry() 10 | arguments = Argument.build( 11 | tree_regexp, 12 | "three blind mice", 13 | [parameter_type_registry.lookup_by_type_name("string")], 14 | ) 15 | assert arguments[0].parameter_type.name == "string" 16 | -------------------------------------------------------------------------------- /python/tests/test_expression_parser.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.definitions import TESTDATA_ROOT_DIR 4 | 5 | import pytest 6 | 7 | from cucumber_expressions.expression_parser import ( 8 | CucumberExpressionParser, 9 | ) 10 | 11 | 12 | def get_expectation_yamls(): 13 | yaml_dir = Path(TESTDATA_ROOT_DIR) / "cucumber-expression" / "parser" 14 | return [ 15 | Path(yaml_dir) / file 16 | for file in Path(yaml_dir).iterdir() 17 | if file.suffix == ".yaml" 18 | ] 19 | 20 | 21 | class TestCucumberExpression: 22 | @pytest.mark.parametrize("load_test_yamls", get_expectation_yamls(), indirect=True) 23 | def test_cucumber_expression_matches(self, load_test_yamls: dict): 24 | expectation = load_test_yamls 25 | parser = CucumberExpressionParser() 26 | if "exception" in expectation: 27 | with pytest.raises(Exception) as excinfo: 28 | parser.parse(expectation["expression"]) 29 | assert excinfo.value.args[0] == expectation["exception"] 30 | else: 31 | node = parser.parse(expectation["expression"]) 32 | assert node.to_json() == expectation["expected_ast"] 33 | -------------------------------------------------------------------------------- /python/tests/test_expression_tokenizer.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.definitions import TESTDATA_ROOT_DIR 4 | 5 | import pytest 6 | 7 | from cucumber_expressions.expression_tokenizer import ( 8 | CucumberExpressionTokenizer, 9 | ) 10 | from cucumber_expressions.errors import ( 11 | CantEscape, 12 | TheEndOfLineCannotBeEscaped, 13 | ) 14 | 15 | 16 | def get_expectation_yamls(): 17 | yaml_dir = Path(TESTDATA_ROOT_DIR) / "cucumber-expression" / "tokenizer" 18 | return [ 19 | Path(yaml_dir) / file 20 | for file in Path(yaml_dir).iterdir() 21 | if file.suffix == ".yaml" 22 | ] 23 | 24 | 25 | class TestCucumberExpression: 26 | @pytest.mark.parametrize("load_test_yamls", get_expectation_yamls(), indirect=True) 27 | def test_cucumber_expression_tokenizes(self, load_test_yamls: dict): 28 | expectation = load_test_yamls 29 | tokenizer = CucumberExpressionTokenizer() 30 | if "exception" in expectation: 31 | with pytest.raises((CantEscape, TheEndOfLineCannotBeEscaped)) as excinfo: 32 | tokenizer.tokenize(expectation["expression"]) 33 | assert excinfo.value.args[0] == expectation["exception"] 34 | else: 35 | tokens = tokenizer.tokenize(expectation["expression"], to_json=True) 36 | assert tokens == expectation["expected_tokens"] 37 | -------------------------------------------------------------------------------- /python/tests/test_expression_transformation.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from tests.definitions import TESTDATA_ROOT_DIR 6 | 7 | from cucumber_expressions.expression import CucumberExpression 8 | from cucumber_expressions.parameter_type_registry import ParameterTypeRegistry 9 | 10 | 11 | def get_expectation_yamls(): 12 | yaml_dir = Path(TESTDATA_ROOT_DIR) / "cucumber-expression" / "transformation" 13 | return [ 14 | Path(yaml_dir) / file 15 | for file in Path(yaml_dir).iterdir() 16 | if file.suffix == ".yaml" 17 | ] 18 | 19 | 20 | class TestCucumberExpression: 21 | @pytest.mark.parametrize("load_test_yamls", get_expectation_yamls(), indirect=True) 22 | def test_cucumber_expression_transforms(self, load_test_yamls: dict): 23 | expectation = load_test_yamls 24 | parameter_registry = ParameterTypeRegistry() 25 | expression = CucumberExpression(expectation["expression"], parameter_registry) 26 | assert expression.regexp == expectation["expected_regex"] 27 | -------------------------------------------------------------------------------- /ruby/.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | coverage/ 3 | acceptance/ 4 | pkg/ 5 | *.gem 6 | .deps 7 | *.iml 8 | -------------------------------------------------------------------------------- /ruby/.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-performance 3 | - rubocop-rake 4 | - rubocop-rspec 5 | 6 | inherit_from: .rubocop_todo.yml 7 | 8 | inherit_mode: 9 | merge: 10 | - Exclude 11 | 12 | AllCops: 13 | TargetRubyVersion: 2.5 14 | NewCops: enable 15 | 16 | # Disabled on our repo's to enable polyglot-release 17 | Gemspec/RequireMFA: 18 | Enabled: false 19 | 20 | Layout/LineLength: 21 | Max: 200 22 | 23 | # This is documented in this spec to showcase why it's not working and people shouldn't use it 24 | # cf: 25 | Lint/MixedRegexpCaptureTypes: 26 | Exclude: 27 | - 'spec/cucumber/cucumber_expressions/tree_regexp_spec.rb' 28 | 29 | Style/Documentation: 30 | Enabled: false 31 | 32 | Style/RegexpLiteral: 33 | EnforcedStyle: slashes 34 | AllowInnerSlashes: true 35 | 36 | RSpec/MessageSpies: 37 | EnforcedStyle: receive 38 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | -------------------------------------------------------------------------------- /ruby/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Cucumber Ltd and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ruby/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $:.unshift File.expand_path('../lib', __FILE__) 4 | 5 | Dir['./rake/*.rb'].each do |f| 6 | require f 7 | end 8 | 9 | require 'rspec/core/rake_task' 10 | RSpec::Core::RakeTask.new(:spec) 11 | 12 | task default: :spec 13 | -------------------------------------------------------------------------------- /ruby/VERSION: -------------------------------------------------------------------------------- 1 | 18.0.1 2 | -------------------------------------------------------------------------------- /ruby/cucumber-cucumber-expressions.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # frozen_string_literal: true 3 | 4 | version = File.read(File.expand_path('VERSION', __dir__)).strip 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'cucumber-cucumber-expressions' 8 | s.version = version 9 | s.authors = ['Aslak Hellesøy'] 10 | s.description = 'Cucumber Expressions - a simpler alternative to Regular Expressions' 11 | s.summary = "cucumber-expressions-#{s.version}" 12 | s.email = 'cukes@googlegroups.com' 13 | s.homepage = 'https://github.com/cucumber/cucumber-expressions' 14 | s.platform = Gem::Platform::RUBY 15 | s.license = 'MIT' 16 | s.required_ruby_version = '>= 2.7' 17 | 18 | s.metadata = { 19 | 'bug_tracker_uri' => 'https://github.com/cucumber/cucumber/issues', 20 | 'changelog_uri' => 'https://github.com/cucumber/cucumber-expressions/blob/main/CHANGELOG.md', 21 | 'documentation_uri' => 'https://github.com/cucumber/cucumber-expressions#readme', 22 | 'mailing_list_uri' => 'https://community.smartbear.com/category/cucumber/discussions/cucumberos', 23 | 'source_code_uri' => 'https://github.com/cucumber/cucumber-expressions/tree/main/ruby', 24 | } 25 | 26 | s.add_runtime_dependency 'bigdecimal' 27 | 28 | s.add_development_dependency 'rake', '~> 13.1' 29 | s.add_development_dependency 'rspec', '~> 3.13' 30 | s.add_development_dependency 'rubocop', '~> 1.27.0' 31 | s.add_development_dependency 'rubocop-performance', '~> 1.7.0' 32 | s.add_development_dependency 'rubocop-rake', '~> 0.5.0' 33 | s.add_development_dependency 'rubocop-rspec', '~> 2.0.0' 34 | 35 | s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'LICENSE', 'README.md'] 36 | s.rdoc_options = ['--charset=UTF-8'] 37 | s.require_path = 'lib' 38 | end 39 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/argument.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/group' 4 | require 'cucumber/cucumber_expressions/errors' 5 | 6 | module Cucumber 7 | module CucumberExpressions 8 | class Argument 9 | attr_reader :group, :parameter_type 10 | 11 | def self.build(tree_regexp, text, parameter_types) 12 | group = tree_regexp.match(text) 13 | return nil if group.nil? 14 | 15 | arg_groups = group.children 16 | 17 | if arg_groups.length != parameter_types.length 18 | raise CucumberExpressionError.new( 19 | "Expression #{tree_regexp.regexp.inspect} has #{arg_groups.length} capture groups (#{arg_groups.map(&:value)}), " \ 20 | "but there were #{parameter_types.length} parameter types (#{parameter_types.map(&:name)})" 21 | ) 22 | end 23 | 24 | parameter_types.zip(arg_groups).map do |parameter_type, arg_group| 25 | Argument.new(arg_group, parameter_type) 26 | end 27 | end 28 | 29 | def initialize(group, parameter_type) 30 | @group, @parameter_type = group, parameter_type 31 | end 32 | 33 | def value(self_obj = :nil) 34 | raise 'No self_obj' if self_obj == :nil 35 | 36 | group_values = @group ? @group.values : nil 37 | @parameter_type.transform(self_obj, group_values) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/combinatorial_generated_expression_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require('cucumber/cucumber_expressions/generated_expression') 4 | 5 | module Cucumber 6 | module CucumberExpressions 7 | class CombinatorialGeneratedExpressionFactory 8 | def initialize(expression_template, parameter_type_combinations) 9 | @expression_template = expression_template 10 | @parameter_type_combinations = parameter_type_combinations 11 | end 12 | 13 | def generate_expressions 14 | generated_expressions = [] 15 | generate_permutations(generated_expressions, 0, []) 16 | generated_expressions 17 | end 18 | 19 | # 256 generated expressions ought to be enough for anybody 20 | MAX_EXPRESSIONS = 256 21 | 22 | def generate_permutations(generated_expressions, depth, current_parameter_types) 23 | return if generated_expressions.length >= MAX_EXPRESSIONS 24 | 25 | if depth == @parameter_type_combinations.length 26 | generated_expression = GeneratedExpression.new(@expression_template, current_parameter_types) 27 | generated_expressions.push(generated_expression) 28 | return 29 | end 30 | 31 | (0...@parameter_type_combinations[depth].length).each do |i| 32 | # Avoid recursion if no elements can be added. 33 | break if generated_expressions.length >= MAX_EXPRESSIONS 34 | 35 | new_current_parameter_types = current_parameter_types.dup # clone 36 | new_current_parameter_types.push(@parameter_type_combinations[depth][i]) 37 | generate_permutations(generated_expressions, depth + 1, new_current_parameter_types) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/expression_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/errors' 4 | require 'cucumber/cucumber_expressions/cucumber_expression' 5 | require 'cucumber/cucumber_expressions/regular_expression' 6 | 7 | module Cucumber 8 | module CucumberExpressions 9 | class ExpressionFactory 10 | def initialize(parameter_type_registry) 11 | @parameter_type_registry = parameter_type_registry 12 | end 13 | 14 | def create_expression(string_or_regexp) 15 | case string_or_regexp 16 | when String then CucumberExpression.new(string_or_regexp, @parameter_type_registry) 17 | when Regexp then RegularExpression.new(string_or_regexp, @parameter_type_registry) 18 | else 19 | raise CucumberExpressionError.new("Can't create an expression from #{string_or_regexp.inspect}") 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/generated_expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cucumber 4 | module CucumberExpressions 5 | class GeneratedExpression 6 | attr_reader :parameter_types 7 | 8 | def initialize(expression_template, parameters_types) 9 | @expression_template, @parameter_types = expression_template, parameters_types 10 | end 11 | 12 | def source 13 | sprintf(@expression_template, *@parameter_types.map(&:name)) 14 | end 15 | 16 | def parameter_names 17 | usage_by_type_name = Hash.new(0) 18 | @parameter_types.map do |t| 19 | get_parameter_name(t.name, usage_by_type_name) 20 | end 21 | end 22 | 23 | private 24 | 25 | def get_parameter_name(type_name, usage_by_type_name) 26 | count = usage_by_type_name[type_name] 27 | count += 1 28 | usage_by_type_name[type_name] = count 29 | count == 1 ? type_name : "#{type_name}#{count}" 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/group.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cucumber 4 | module CucumberExpressions 5 | class Group 6 | attr_reader :value, :start, :end, :children 7 | 8 | def initialize(value, start, _end, children) 9 | @value = value 10 | @start = start 11 | @end = _end 12 | @children = children 13 | end 14 | 15 | def values 16 | (children.empty? ? [self] : children).map(&:value) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/group_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/group' 4 | 5 | module Cucumber 6 | module CucumberExpressions 7 | class GroupBuilder 8 | attr_accessor :source 9 | 10 | def initialize 11 | @group_builders = [] 12 | @capturing = true 13 | end 14 | 15 | def add(group_builder) 16 | @group_builders.push(group_builder) 17 | end 18 | 19 | def build(match, group_indices) 20 | group_index = group_indices.next 21 | children = @group_builders.map { |gb| gb.build(match, group_indices) } 22 | Group.new(match[group_index], match.offset(group_index)[0], match.offset(group_index)[1], children) 23 | end 24 | 25 | def set_non_capturing! 26 | @capturing = false 27 | end 28 | 29 | def capturing? 30 | @capturing 31 | end 32 | 33 | def move_children_to(group_builder) 34 | @group_builders.each do |child| 35 | group_builder.add(child) 36 | end 37 | end 38 | 39 | def children 40 | @group_builders 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Cucumber 4 | module CucumberExpressions 5 | class ParameterTypeMatcher 6 | attr_reader :parameter_type 7 | 8 | def initialize(parameter_type, regexp, text, match_position = 0) 9 | @parameter_type, @regexp, @text = parameter_type, regexp, text 10 | @match = @regexp.match(@text, match_position) 11 | end 12 | 13 | def advance_to(new_match_position) 14 | (new_match_position...@text.length).each do |advanced_position| 15 | matcher = self.class.new(parameter_type, @regexp, @text, advanced_position) 16 | return matcher if matcher.find && matcher.full_word? 17 | end 18 | 19 | self.class.new(parameter_type, @regexp, @text, @text.length) 20 | end 21 | 22 | def find 23 | !@match.nil? && !group.empty? 24 | end 25 | 26 | def full_word? 27 | space_before_match_or_sentence_start? && space_after_match_or_sentence_end? 28 | end 29 | 30 | def start 31 | @match.begin(0) 32 | end 33 | 34 | def group 35 | @match.captures[0] 36 | end 37 | 38 | def <=>(other) 39 | pos_comparison = start <=> other.start 40 | return pos_comparison if pos_comparison != 0 41 | 42 | length_comparison = other.group.length <=> group.length 43 | return length_comparison if length_comparison != 0 44 | 45 | 0 46 | end 47 | 48 | private 49 | 50 | def space_before_match_or_sentence_start? 51 | match_begin = @match.begin(0) 52 | match_begin == 0 || @text[match_begin - 1].match(/\p{Z}|\p{P}|\p{S}/) 53 | end 54 | 55 | def space_after_match_or_sentence_end? 56 | match_end = @match.end(0) 57 | match_end == @text.length || @text[match_end].match(/\p{Z}|\p{P}|\p{S}/) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /ruby/lib/cucumber/cucumber_expressions/regular_expression.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/argument' 4 | require 'cucumber/cucumber_expressions/parameter_type' 5 | require 'cucumber/cucumber_expressions/tree_regexp' 6 | 7 | module Cucumber 8 | module CucumberExpressions 9 | class RegularExpression 10 | def initialize(expression_regexp, parameter_type_registry) 11 | @expression_regexp = expression_regexp 12 | @parameter_type_registry = parameter_type_registry 13 | @tree_regexp = TreeRegexp.new(@expression_regexp) 14 | end 15 | 16 | def match(text) 17 | parameter_types = @tree_regexp.group_builder.children.map do |group_builder| 18 | parameter_type_regexp = group_builder.source 19 | @parameter_type_registry.lookup_by_regexp( 20 | parameter_type_regexp, 21 | @expression_regexp, 22 | text 23 | ) || ParameterType.new( 24 | nil, 25 | parameter_type_regexp, 26 | String, 27 | ->(*s) { s[0] }, 28 | false, 29 | false 30 | ) 31 | end 32 | 33 | Argument.build(@tree_regexp, text, parameter_types) 34 | end 35 | 36 | def regexp 37 | @expression_regexp 38 | end 39 | 40 | def source 41 | @expression_regexp.source 42 | end 43 | 44 | def to_s 45 | regexp.inspect 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/argument_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/argument' 4 | require 'cucumber/cucumber_expressions/tree_regexp' 5 | require 'cucumber/cucumber_expressions/parameter_type_registry' 6 | 7 | module Cucumber 8 | module CucumberExpressions 9 | describe Argument do 10 | it 'exposes parameter_type' do 11 | tree_regexp = TreeRegexp.new(/three (.*) mice/) 12 | parameter_type_registry = ParameterTypeRegistry.new 13 | arguments = described_class.build(tree_regexp, 'three blind mice', [parameter_type_registry.lookup_by_type_name('string')]) 14 | argument = arguments[0] 15 | expect(argument.parameter_type.name).to eq('string') 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/combinatorial_generated_expression_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/parameter_type' 4 | require 'cucumber/cucumber_expressions/combinatorial_generated_expression_factory' 5 | 6 | describe Cucumber::CucumberExpressions::CombinatorialGeneratedExpressionFactory do 7 | let(:klazz) { Class.new } 8 | let(:color_parameter_type) do 9 | Cucumber::CucumberExpressions::ParameterType.new('color', /red|blue|yellow/, klazz, ->(_) { klazz.new }, true, false) 10 | end 11 | let(:css_color_parameter_type) do 12 | Cucumber::CucumberExpressions::ParameterType.new('csscolor', /red|blue|yellow/, klazz, ->(_) { klazz.new }, true, false) 13 | end 14 | let(:date_parameter_type) do 15 | Cucumber::CucumberExpressions::ParameterType.new('date', /\d{4}-\d{2}-\d{2}/, klazz, ->(_) { klazz.new }, true, false) 16 | end 17 | let(:date_time_parameter_type) do 18 | Cucumber::CucumberExpressions::ParameterType.new('datetime', /\d{4}-\d{2}-\d{2}/, klazz, ->(_) { klazz.new }, true, false) 19 | end 20 | let(:timestamp_parameter_type) do 21 | Cucumber::CucumberExpressions::ParameterType.new('timestamp', /\d{4}-\d{2}-\d{2}/, klazz, ->(_) { klazz.new }, true, false) 22 | end 23 | 24 | it 'generates multiple expressions' do 25 | parameter_type_combinations = [ 26 | [color_parameter_type, css_color_parameter_type], 27 | [date_parameter_type, date_time_parameter_type, timestamp_parameter_type] 28 | ] 29 | 30 | factory = described_class.new('I bought a {%s} ball on {%s}', parameter_type_combinations) 31 | expressions = factory.generate_expressions.map { |generated_expression| generated_expression.source } 32 | expect(expressions).to eq([ 33 | 'I bought a {color} ball on {date}', 34 | 'I bought a {color} ball on {datetime}', 35 | 'I bought a {color} ball on {timestamp}', 36 | 'I bought a {csscolor} ball on {date}', 37 | 'I bought a {csscolor} ball on {datetime}', 38 | 'I bought a {csscolor} ball on {timestamp}', 39 | ]) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/cucumber_expression_parser_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'cucumber/cucumber_expressions/cucumber_expression_parser' 5 | require 'cucumber/cucumber_expressions/errors' 6 | 7 | module Cucumber 8 | module CucumberExpressions 9 | describe CucumberExpressionParser do 10 | Dir['../testdata/cucumber-expression/parser/*.yaml'].each do |path| 11 | expectation = YAML.load_file(path) 12 | it "parses #{path}" do 13 | parser = described_class.new 14 | if expectation['exception'] 15 | expect { parser.parse(expectation['expression']) }.to raise_error(expectation['exception']) 16 | else 17 | node = parser.parse(expectation['expression']) 18 | node_hash = node.to_hash 19 | expect(node_hash).to eq(expectation['expected_ast']) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/cucumber_expression_tokenizer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'cucumber/cucumber_expressions/cucumber_expression_tokenizer' 5 | require 'cucumber/cucumber_expressions/errors' 6 | 7 | module Cucumber 8 | module CucumberExpressions 9 | describe CucumberExpressionTokenizer do 10 | Dir['../testdata/cucumber-expression/tokenizer/*.yaml'].each do |path| 11 | expectation = YAML.load_file(path) 12 | it "tokenizes #{path}" do 13 | tokenizer = described_class.new 14 | if expectation['exception'] 15 | expect { tokenizer.tokenize(expectation['expression']) }.to raise_error(expectation['exception']) 16 | else 17 | tokens = tokenizer.tokenize(expectation['expression']) 18 | token_hashes = tokens.map { |token| token.to_hash } 19 | expect(token_hashes).to eq(expectation['expected_tokens']) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/cucumber_expression_transformation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'yaml' 4 | require 'cucumber/cucumber_expressions/cucumber_expression' 5 | require 'cucumber/cucumber_expressions/parameter_type_registry' 6 | 7 | module Cucumber 8 | module CucumberExpressions 9 | describe CucumberExpression do 10 | Dir['../testdata/cucumber-expression/transformation/*.yaml'].each do |path| 11 | expectation = YAML.load_file(path) 12 | it "transforms #{path}" do 13 | parameter_registry = ParameterTypeRegistry.new 14 | if expectation['exception'] 15 | expect { 16 | cucumber_expression = described_class.new(expectation['expression'], parameter_registry) 17 | cucumber_expression.match(expectation['text']) 18 | }.to raise_error(expectation['exception']) 19 | else 20 | cucumber_expression = described_class.new(expectation['expression'], parameter_registry) 21 | matches = cucumber_expression.match(expectation['text']) 22 | values = matches.nil? ? nil : matches.map { |arg| arg.value(nil) } 23 | expect(values).to eq(expectation['expected_args']) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/expression_factory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/expression_factory' 4 | 5 | module Cucumber 6 | module CucumberExpressions 7 | describe ExpressionFactory do 8 | before do 9 | @expression_factory = described_class.new(ParameterTypeRegistry.new) 10 | end 11 | 12 | it 'creates a RegularExpression' do 13 | expect(@expression_factory.create_expression(/x/).class).to eq(RegularExpression) 14 | end 15 | 16 | it 'creates a CucumberExpression' do 17 | expect(@expression_factory.create_expression('{int}').class).to eq(CucumberExpression) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /ruby/spec/cucumber/cucumber_expressions/parameter_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'cucumber/cucumber_expressions/parameter_type' 4 | 5 | module Cucumber 6 | module CucumberExpressions 7 | describe ParameterType do 8 | it 'does not allow ignore flag on regexp' do 9 | expect do 10 | described_class.new('case-insensitive', /[a-z]+/i, String, ->(s) { s }, true, true) 11 | end.to raise_error( 12 | CucumberExpressionError, 13 | "ParameterType Regexps can't use option Regexp::IGNORECASE" 14 | ) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/allows-escaped-optional-parameter-types.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\({int})" 3 | text: "(3)" 4 | expected_args: 5 | - 3 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/allows-parameter-type-in-alternation-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: a/i{int}n/y 3 | text: i18n 4 | expected_args: 5 | - 18 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/allows-parameter-type-in-alternation-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: a/i{int}n/y 3 | text: a11y 4 | expected_args: 5 | - 11 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-allow-parameter-adjacent-to-alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int}st/nd/rd/th" 3 | text: 3rd 4 | expected_args: 5 | - 3 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-alternation-in-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three( brown/black) mice 3 | text: three brown/black mice 4 | exception: |- 5 | This Cucumber Expression has a problem at column 13: 6 | 7 | three( brown/black) mice 8 | ^ 9 | An alternation can not be used inside an optional. 10 | If you did not mean to use an alternation you can use '\/' to escape the '/'. Otherwise rephrase your expression or consider using a regular expression instead. 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int}/x" 3 | text: '3' 4 | exception: |- 5 | This Cucumber Expression has a problem at column 6: 6 | 7 | {int}/x 8 | ^ 9 | Alternative may not be empty. 10 | If you did not mean to use an alternative you can use '\/' to escape the '/' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (brown)/black mice 3 | text: three brown mice 4 | exception: |- 5 | This Cucumber Expression has a problem at column 7: 6 | 7 | three (brown)/black mice 8 | ^-----^ 9 | An alternative may not exclusively contain optionals. 10 | If you did not mean to use an optional you can use '\(' to escape the '(' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: x/{int} 3 | text: '3' 4 | exception: |- 5 | This Cucumber Expression has a problem at column 3: 6 | 7 | x/{int} 8 | ^ 9 | Alternative may not be empty. 10 | If you did not mean to use an alternative you can use '\/' to escape the '/' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-alternation-with-empty-alternative.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three brown//black mice 3 | text: three brown mice 4 | exception: |- 5 | This Cucumber Expression has a problem at column 13: 6 | 7 | three brown//black mice 8 | ^ 9 | Alternative may not be empty. 10 | If you did not mean to use an alternative you can use '\/' to escape the '/' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-empty-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three () mice 3 | text: three brown mice 4 | exception: |- 5 | This Cucumber Expression has a problem at column 7: 6 | 7 | three () mice 8 | ^^ 9 | An optional must contain some text. 10 | If you did not mean to use an optional you can use '\(' to escape the '(' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-nested-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "(a(b))" 3 | exception: |- 4 | This Cucumber Expression has a problem at column 3: 5 | 6 | (a(b)) 7 | ^-^ 8 | An optional may not contain an other optional. 9 | If you did not mean to use an optional type you can use '\(' to escape the '('. For more complicated expressions consider using a regular expression instead. 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-optional-parameter-types.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "({int})" 3 | text: '3' 4 | exception: |- 5 | This Cucumber Expression has a problem at column 2: 6 | 7 | ({int}) 8 | ^---^ 9 | An optional may not contain a parameter type. 10 | If you did not mean to use an parameter type you can use '\{' to escape the '{' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-parameter-name-with-reserved-characters.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{(string)}" 3 | text: something 4 | exception: |- 5 | This Cucumber Expression has a problem at column 2: 6 | 7 | {(string)} 8 | ^ 9 | Parameter names may not contain '{', '}', '(', ')', '\' or '/'. 10 | Did you mean to use a regular expression? 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-unfinished-parenthesis-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (exceptionally\) {string\} mice 3 | exception: |- 4 | This Cucumber Expression has a problem at column 24: 5 | 6 | three (exceptionally\) {string\} mice 7 | ^ 8 | The '{' does not have a matching '}'. 9 | If you did not intend to use a parameter you can use '\{' to escape the a parameter 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-unfinished-parenthesis-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (exceptionally\) {string} mice 3 | exception: |- 4 | This Cucumber Expression has a problem at column 7: 5 | 6 | three (exceptionally\) {string} mice 7 | ^ 8 | The '(' does not have a matching ')'. 9 | If you did not intend to use optional text you can use '\(' to escape the optional text 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-allow-unfinished-parenthesis-3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three ((exceptionally\) strong) mice 3 | exception: |- 4 | This Cucumber Expression has a problem at column 7: 5 | 6 | three ((exceptionally\) strong) mice 7 | ^ 8 | The '(' does not have a matching ')'. 9 | If you did not intend to use optional text you can use '\(' to escape the optional text 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/does-not-match-misquoted-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three "blind' mice 4 | expected_args: 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/doesnt-match-float-as-int.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int}" 3 | text: '1.22' 4 | expected_args: 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: mice/rats and rats\/mice 3 | text: rats and rats/mice 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-anonymous-parameter-type.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{}" 3 | text: '0.22' 4 | expected_args: 5 | - '0.22' 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-bigdecimal.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{bigdecimal}" 3 | text: '3.1415926535897932384626433832795028841971693993751' 4 | expected_args: 5 | - '3.1415926535897932384626433832795028841971693993751' 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-biginteger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{biginteger}" 3 | text: '31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679' 4 | expected_args: 5 | - '31415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679' 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-byte.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{byte}" 3 | text: '127' 4 | expected_args: 5 | - 127 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} and {string} mice 3 | text: three "" and "handsome" mice 4 | expected_args: 5 | - '' 6 | - handsome 7 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-double-quoted-empty-string-as-empty-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three "" mice 4 | expected_args: 5 | - '' 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-double-quoted-string-with-escaped-double-quote.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three "bl\"nd" mice 4 | expected_args: 5 | - bl"nd 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-double-quoted-string-with-single-quotes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three "'blind'" mice 4 | expected_args: 5 | - "'blind'" 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-double-quoted-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three "blind" mice 4 | expected_args: 5 | - blind 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-double.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{double}" 3 | text: '3.141592653589793' 4 | expected_args: 5 | - 3.141592653589793 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-doubly-escaped-parenthesis.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three \\(exceptionally) \\{string} mice 3 | text: three \exceptionally \"blind" mice 4 | expected_args: 5 | - blind 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-doubly-escaped-slash-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: 12\\/2020 3 | text: 12\ 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-doubly-escaped-slash-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: 12\\/2020 3 | text: '2020' 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-escaped-parenthesis-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three \(exceptionally) \{string} mice 3 | text: three (exceptionally) {string} mice 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-escaped-parenthesis-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three \((exceptionally)) \{{string}} mice 3 | text: three (exceptionally) {"blind"} mice 4 | expected_args: 5 | - blind 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-escaped-parenthesis-3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three \((exceptionally)) \{{string}} mice 3 | text: three (exceptionally) {"blind"} mice 4 | expected_args: 5 | - blind 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-escaped-slash.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: 12\/2020 3 | text: 12/2020 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-float-with-integer-part.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{float}" 3 | text: '0.22' 4 | expected_args: 5 | - 0.22 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-float-without-integer-part.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{float}" 3 | text: ".22" 4 | expected_args: 5 | - 0.22 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-float.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{float}" 3 | text: '3.141593' 4 | expected_args: 5 | - 3.141593 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-int.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int}" 3 | text: '2147483647' 4 | expected_args: 5 | - 2147483647 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-long.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{long}" 3 | text: '9223372036854775807' 4 | expected_args: 5 | - 9223372036854775807 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-multiple-double-quoted-strings.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} and {string} mice 3 | text: three "blind" and "crippled" mice 4 | expected_args: 5 | - blind 6 | - crippled 7 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-multiple-single-quoted-strings.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} and {string} mice 3 | text: three 'blind' and 'crippled' mice 4 | expected_args: 5 | - blind 6 | - crippled 7 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-before-alternation-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (brown )mice/rats 3 | text: three brown mice 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-before-alternation-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (brown )mice/rats 3 | text: three rats 4 | expected_args: [] 5 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-before-alternation-with-regex-characters-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: I wait {int} second(s)./second(s)? 3 | text: I wait 2 seconds? 4 | expected_args: 5 | - 2 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-before-alternation-with-regex-characters-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: I wait {int} second(s)./second(s)? 3 | text: I wait 1 second. 4 | expected_args: 5 | - 1 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-in-alternation-1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int} rat(s)/mouse/mice" 3 | text: 3 rats 4 | expected_args: 5 | - 3 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-in-alternation-2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int} rat(s)/mouse/mice" 3 | text: 2 mice 4 | expected_args: 5 | - 2 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-optional-in-alternation-3.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int} rat(s)/mouse/mice" 3 | text: 1 mouse 4 | expected_args: 5 | - 1 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-short.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{short}" 3 | text: '32767' 4 | expected_args: 5 | - 32767 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} and {string} mice 3 | text: three '' and 'handsome' mice 4 | expected_args: 5 | - '' 6 | - handsome 7 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-single-quoted-empty-string-as-empty-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three '' mice 4 | expected_args: 5 | - '' 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-single-quoted-string-with-double-quotes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three '"blind"' mice 4 | expected_args: 5 | - '"blind"' 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-single-quoted-string-with-escaped-single-quote.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three 'bl\'nd' mice 4 | expected_args: 5 | - bl'nd 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-single-quoted-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | text: three 'blind' mice 4 | expected_args: 5 | - blind 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/matches-word.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {word} mice 3 | text: three blind mice 4 | expected_args: 5 | - blind 6 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/matching/throws-unknown-parameter-type.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{unknown}" 3 | text: something 4 | exception: |- 5 | This Cucumber Expression has a problem at column 1: 6 | 7 | {unknown} 8 | ^-------^ 9 | Undefined parameter type 'unknown'. 10 | Please register a ParameterType for 'unknown' 11 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation-followed-by-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three blind\ rat/cat(s) 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 23 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: ALTERNATION_NODE 17 | start: 6 18 | end: 23 19 | nodes: 20 | - type: ALTERNATIVE_NODE 21 | start: 6 22 | end: 16 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 6 26 | end: 16 27 | token: blind rat 28 | - type: ALTERNATIVE_NODE 29 | start: 17 30 | end: 23 31 | nodes: 32 | - type: TEXT_NODE 33 | start: 17 34 | end: 20 35 | token: cat 36 | - type: OPTIONAL_NODE 37 | start: 20 38 | end: 23 39 | nodes: 40 | - type: TEXT_NODE 41 | start: 21 42 | end: 22 43 | token: s 44 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation-phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three hungry/blind mice 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 23 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: ALTERNATION_NODE 17 | start: 6 18 | end: 18 19 | nodes: 20 | - type: ALTERNATIVE_NODE 21 | start: 6 22 | end: 12 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 6 26 | end: 12 27 | token: hungry 28 | - type: ALTERNATIVE_NODE 29 | start: 13 30 | end: 18 31 | nodes: 32 | - type: TEXT_NODE 33 | start: 13 34 | end: 18 35 | token: blind 36 | - type: TEXT_NODE 37 | start: 18 38 | end: 19 39 | token: " " 40 | - type: TEXT_NODE 41 | start: 19 42 | end: 23 43 | token: mice 44 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation-with-parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: I select the {int}st/nd/rd/th 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 29 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 1 11 | token: I 12 | - type: TEXT_NODE 13 | start: 1 14 | end: 2 15 | token: " " 16 | - type: TEXT_NODE 17 | start: 2 18 | end: 8 19 | token: select 20 | - type: TEXT_NODE 21 | start: 8 22 | end: 9 23 | token: " " 24 | - type: TEXT_NODE 25 | start: 9 26 | end: 12 27 | token: the 28 | - type: TEXT_NODE 29 | start: 12 30 | end: 13 31 | token: " " 32 | - type: PARAMETER_NODE 33 | start: 13 34 | end: 18 35 | nodes: 36 | - type: TEXT_NODE 37 | start: 14 38 | end: 17 39 | token: int 40 | - type: ALTERNATION_NODE 41 | start: 18 42 | end: 29 43 | nodes: 44 | - type: ALTERNATIVE_NODE 45 | start: 18 46 | end: 20 47 | nodes: 48 | - type: TEXT_NODE 49 | start: 18 50 | end: 20 51 | token: st 52 | - type: ALTERNATIVE_NODE 53 | start: 21 54 | end: 23 55 | nodes: 56 | - type: TEXT_NODE 57 | start: 21 58 | end: 23 59 | token: nd 60 | - type: ALTERNATIVE_NODE 61 | start: 24 62 | end: 26 63 | nodes: 64 | - type: TEXT_NODE 65 | start: 24 66 | end: 26 67 | token: rd 68 | - type: ALTERNATIVE_NODE 69 | start: 27 70 | end: 29 71 | nodes: 72 | - type: TEXT_NODE 73 | start: 27 74 | end: 29 75 | token: th 76 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation-with-unused-end-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three )blind\ mice/rats 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 23 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: ALTERNATION_NODE 17 | start: 6 18 | end: 23 19 | nodes: 20 | - type: ALTERNATIVE_NODE 21 | start: 6 22 | end: 18 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 6 26 | end: 7 27 | token: ")" 28 | - type: TEXT_NODE 29 | start: 7 30 | end: 18 31 | token: blind mice 32 | - type: ALTERNATIVE_NODE 33 | start: 19 34 | end: 23 35 | nodes: 36 | - type: TEXT_NODE 37 | start: 19 38 | end: 23 39 | token: rats 40 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation-with-unused-start-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three blind\ mice/rats( 3 | exception: |- 4 | This Cucumber Expression has a problem at column 23: 5 | 6 | three blind\ mice/rats( 7 | ^ 8 | The '(' does not have a matching ')'. 9 | If you did not intend to use optional text you can use '\(' to escape the optional text 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation-with-white-space.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\ three\\ hungry/blind\\ mice\\ " 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 29 7 | nodes: 8 | - type: ALTERNATION_NODE 9 | start: 0 10 | end: 29 11 | nodes: 12 | - type: ALTERNATIVE_NODE 13 | start: 0 14 | end: 15 15 | nodes: 16 | - type: TEXT_NODE 17 | start: 0 18 | end: 15 19 | token: " three hungry" 20 | - type: ALTERNATIVE_NODE 21 | start: 16 22 | end: 29 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 16 26 | end: 29 27 | token: 'blind mice ' 28 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: mice/rats 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 9 7 | nodes: 8 | - type: ALTERNATION_NODE 9 | start: 0 10 | end: 9 11 | nodes: 12 | - type: ALTERNATIVE_NODE 13 | start: 0 14 | end: 4 15 | nodes: 16 | - type: TEXT_NODE 17 | start: 0 18 | end: 4 19 | token: mice 20 | - type: ALTERNATIVE_NODE 21 | start: 5 22 | end: 9 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 5 26 | end: 9 27 | token: rats 28 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/anonymous-parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{}" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 2 7 | nodes: 8 | - type: PARAMETER_NODE 9 | start: 0 10 | end: 2 11 | nodes: [] 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/closing-brace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "}" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 1 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 1 11 | token: "}" 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/closing-parenthesis.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: ")" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 1 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 1 11 | token: ")" 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/empty-alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "/" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 1 7 | nodes: 8 | - type: ALTERNATION_NODE 9 | start: 0 10 | end: 1 11 | nodes: 12 | - type: ALTERNATIVE_NODE 13 | start: 0 14 | end: 0 15 | nodes: [] 16 | - type: ALTERNATIVE_NODE 17 | start: 1 18 | end: 1 19 | nodes: [] 20 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/empty-alternations.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "//" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 2 7 | nodes: 8 | - type: ALTERNATION_NODE 9 | start: 0 10 | end: 2 11 | nodes: 12 | - type: ALTERNATIVE_NODE 13 | start: 0 14 | end: 0 15 | nodes: [] 16 | - type: ALTERNATIVE_NODE 17 | start: 1 18 | end: 1 19 | nodes: [] 20 | - type: ALTERNATIVE_NODE 21 | start: 2 22 | end: 2 23 | nodes: [] 24 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/empty-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: '' 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 0 7 | nodes: [] 8 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/escaped-alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: mice\/rats 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 10 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 10 11 | token: mice/rats 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/escaped-backslash.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\\\" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 2 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 2 11 | token: "\\" 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/escaped-opening-parenthesis.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\(" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 2 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 2 11 | token: "(" 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/escaped-optional-followed-by-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three \((very) blind) mice 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 26 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: TEXT_NODE 17 | start: 6 18 | end: 8 19 | token: "(" 20 | - type: OPTIONAL_NODE 21 | start: 8 22 | end: 14 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 9 26 | end: 13 27 | token: very 28 | - type: TEXT_NODE 29 | start: 14 30 | end: 15 31 | token: " " 32 | - type: TEXT_NODE 33 | start: 15 34 | end: 20 35 | token: blind 36 | - type: TEXT_NODE 37 | start: 20 38 | end: 21 39 | token: ")" 40 | - type: TEXT_NODE 41 | start: 21 42 | end: 22 43 | token: " " 44 | - type: TEXT_NODE 45 | start: 22 46 | end: 26 47 | token: mice 48 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/escaped-optional-phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three \(blind) mice 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 19 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: TEXT_NODE 17 | start: 6 18 | end: 13 19 | token: "(blind" 20 | - type: TEXT_NODE 21 | start: 13 22 | end: 14 23 | token: ")" 24 | - type: TEXT_NODE 25 | start: 14 26 | end: 15 27 | token: " " 28 | - type: TEXT_NODE 29 | start: 15 30 | end: 19 31 | token: mice 32 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/escaped-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\(blind)" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 8 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 7 11 | token: "(blind" 12 | - type: TEXT_NODE 13 | start: 7 14 | end: 8 15 | token: ")" 16 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/opening-brace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{" 3 | exception: |- 4 | This Cucumber Expression has a problem at column 1: 5 | 6 | { 7 | ^ 8 | The '{' does not have a matching '}'. 9 | If you did not intend to use a parameter you can use '\{' to escape the a parameter 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/opening-parenthesis.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "(" 3 | exception: |- 4 | This Cucumber Expression has a problem at column 1: 5 | 6 | ( 7 | ^ 8 | The '(' does not have a matching ')'. 9 | If you did not intend to use optional text you can use '\(' to escape the optional text 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/optional-containing-nested-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three ((very) blind) mice 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 25 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: OPTIONAL_NODE 17 | start: 6 18 | end: 20 19 | nodes: 20 | - type: OPTIONAL_NODE 21 | start: 7 22 | end: 13 23 | nodes: 24 | - type: TEXT_NODE 25 | start: 8 26 | end: 12 27 | token: very 28 | - type: TEXT_NODE 29 | start: 13 30 | end: 14 31 | token: " " 32 | - type: TEXT_NODE 33 | start: 14 34 | end: 19 35 | token: blind 36 | - type: TEXT_NODE 37 | start: 20 38 | end: 21 39 | token: " " 40 | - type: TEXT_NODE 41 | start: 21 42 | end: 25 43 | token: mice 44 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/optional-phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (blind) mice 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 18 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: OPTIONAL_NODE 17 | start: 6 18 | end: 13 19 | nodes: 20 | - type: TEXT_NODE 21 | start: 7 22 | end: 12 23 | token: blind 24 | - type: TEXT_NODE 25 | start: 13 26 | end: 14 27 | token: " " 28 | - type: TEXT_NODE 29 | start: 14 30 | end: 18 31 | token: mice 32 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "(blind)" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 7 7 | nodes: 8 | - type: OPTIONAL_NODE 9 | start: 0 10 | end: 7 11 | nodes: 12 | - type: TEXT_NODE 13 | start: 1 14 | end: 6 15 | token: blind 16 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{string}" 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 8 7 | nodes: 8 | - type: PARAMETER_NODE 9 | start: 0 10 | end: 8 11 | nodes: 12 | - type: TEXT_NODE 13 | start: 1 14 | end: 7 15 | token: string 16 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three blind mice 3 | expected_ast: 4 | type: EXPRESSION_NODE 5 | start: 0 6 | end: 16 7 | nodes: 8 | - type: TEXT_NODE 9 | start: 0 10 | end: 5 11 | token: three 12 | - type: TEXT_NODE 13 | start: 5 14 | end: 6 15 | token: " " 16 | - type: TEXT_NODE 17 | start: 6 18 | end: 11 19 | token: blind 20 | - type: TEXT_NODE 21 | start: 11 22 | end: 12 23 | token: " " 24 | - type: TEXT_NODE 25 | start: 12 26 | end: 16 27 | token: mice 28 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/parser/unfinished-parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{string" 3 | exception: |- 4 | This Cucumber Expression has a problem at column 1: 5 | 6 | {string 7 | ^ 8 | The '{' does not have a matching '}'. 9 | If you did not intend to use a parameter you can use '\{' to escape the a parameter 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/alternation-phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three blind/cripple mice 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 5 11 | text: three 12 | - type: WHITE_SPACE 13 | start: 5 14 | end: 6 15 | text: " " 16 | - type: TEXT 17 | start: 6 18 | end: 11 19 | text: blind 20 | - type: ALTERNATION 21 | start: 11 22 | end: 12 23 | text: "/" 24 | - type: TEXT 25 | start: 12 26 | end: 19 27 | text: cripple 28 | - type: WHITE_SPACE 29 | start: 19 30 | end: 20 31 | text: " " 32 | - type: TEXT 33 | start: 20 34 | end: 24 35 | text: mice 36 | - type: END_OF_LINE 37 | start: 24 38 | end: 24 39 | text: '' 40 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: blind/cripple 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 5 11 | text: blind 12 | - type: ALTERNATION 13 | start: 5 14 | end: 6 15 | text: "/" 16 | - type: TEXT 17 | start: 6 18 | end: 13 19 | text: cripple 20 | - type: END_OF_LINE 21 | start: 13 22 | end: 13 23 | text: '' 24 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/empty-string.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: '' 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: END_OF_LINE 9 | start: 0 10 | end: 0 11 | text: '' 12 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escape-non-reserved-character.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\[" 3 | exception: |- 4 | This Cucumber Expression has a problem at column 2: 5 | 6 | \[ 7 | ^ 8 | Only the characters '{', '}', '(', ')', '\', '/' and whitespace can be escaped. 9 | If you did mean to use an '\' you can use '\\' to escape it 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escaped-alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: blind\ and\ famished\/cripple mice 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 29 11 | text: blind and famished/cripple 12 | - type: WHITE_SPACE 13 | start: 29 14 | end: 30 15 | text: " " 16 | - type: TEXT 17 | start: 30 18 | end: 34 19 | text: mice 20 | - type: END_OF_LINE 21 | start: 34 22 | end: 34 23 | text: '' 24 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escaped-char-has-start-index-of-text-token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: " \\/ " 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: WHITE_SPACE 9 | start: 0 10 | end: 1 11 | text: " " 12 | - type: TEXT 13 | start: 1 14 | end: 3 15 | text: "/" 16 | - type: WHITE_SPACE 17 | start: 3 18 | end: 4 19 | text: " " 20 | - type: END_OF_LINE 21 | start: 4 22 | end: 4 23 | text: '' 24 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escaped-end-of-line.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\" 3 | exception: |- 4 | This Cucumber Expression has a problem at column 1: 5 | 6 | \ 7 | ^ 8 | The end of line can not be escaped. 9 | You can use '\\' to escape the '\' 10 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escaped-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\(blind\\)" 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 9 11 | text: "(blind)" 12 | - type: END_OF_LINE 13 | start: 9 14 | end: 9 15 | text: '' 16 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escaped-parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\{string\\}" 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 10 11 | text: "{string}" 12 | - type: END_OF_LINE 13 | start: 10 14 | end: 10 15 | text: '' 16 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/escaped-space.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "\\ " 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 2 11 | text: " " 12 | - type: END_OF_LINE 13 | start: 2 14 | end: 2 15 | text: '' 16 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/optional-phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three (blind) mice 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 5 11 | text: three 12 | - type: WHITE_SPACE 13 | start: 5 14 | end: 6 15 | text: " " 16 | - type: BEGIN_OPTIONAL 17 | start: 6 18 | end: 7 19 | text: "(" 20 | - type: TEXT 21 | start: 7 22 | end: 12 23 | text: blind 24 | - type: END_OPTIONAL 25 | start: 12 26 | end: 13 27 | text: ")" 28 | - type: WHITE_SPACE 29 | start: 13 30 | end: 14 31 | text: " " 32 | - type: TEXT 33 | start: 14 34 | end: 18 35 | text: mice 36 | - type: END_OF_LINE 37 | start: 18 38 | end: 18 39 | text: '' 40 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "(blind)" 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: BEGIN_OPTIONAL 9 | start: 0 10 | end: 1 11 | text: "(" 12 | - type: TEXT 13 | start: 1 14 | end: 6 15 | text: blind 16 | - type: END_OPTIONAL 17 | start: 6 18 | end: 7 19 | text: ")" 20 | - type: END_OF_LINE 21 | start: 7 22 | end: 7 23 | text: '' 24 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/parameter-phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three {string} mice 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 5 11 | text: three 12 | - type: WHITE_SPACE 13 | start: 5 14 | end: 6 15 | text: " " 16 | - type: BEGIN_PARAMETER 17 | start: 6 18 | end: 7 19 | text: "{" 20 | - type: TEXT 21 | start: 7 22 | end: 13 23 | text: string 24 | - type: END_PARAMETER 25 | start: 13 26 | end: 14 27 | text: "}" 28 | - type: WHITE_SPACE 29 | start: 14 30 | end: 15 31 | text: " " 32 | - type: TEXT 33 | start: 15 34 | end: 19 35 | text: mice 36 | - type: END_OF_LINE 37 | start: 19 38 | end: 19 39 | text: '' 40 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{string}" 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: BEGIN_PARAMETER 9 | start: 0 10 | end: 1 11 | text: "{" 12 | - type: TEXT 13 | start: 1 14 | end: 7 15 | text: string 16 | - type: END_PARAMETER 17 | start: 7 18 | end: 8 19 | text: "}" 20 | - type: END_OF_LINE 21 | start: 8 22 | end: 8 23 | text: '' 24 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/tokenizer/phrase.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: three blind mice 3 | expected_tokens: 4 | - type: START_OF_LINE 5 | start: 0 6 | end: 0 7 | text: '' 8 | - type: TEXT 9 | start: 0 10 | end: 5 11 | text: three 12 | - type: WHITE_SPACE 13 | start: 5 14 | end: 6 15 | text: " " 16 | - type: TEXT 17 | start: 6 18 | end: 11 19 | text: blind 20 | - type: WHITE_SPACE 21 | start: 11 22 | end: 12 23 | text: " " 24 | - type: TEXT 25 | start: 12 26 | end: 16 27 | text: mice 28 | - type: END_OF_LINE 29 | start: 16 30 | end: 16 31 | text: '' 32 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/alternation-with-optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: a/b(c) 3 | expected_regex: "^(?:a|b(?:c)?)$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/alternation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: a/b c/d/e 3 | expected_regex: "^(?:a|b) (?:c|d|e)$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/empty.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: '' 3 | expected_regex: "^$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/escape-regex-characters.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "^$[]\\(\\){}\\\\.|?*+" 3 | expected_regex: "^\\^\\$\\[\\]\\(\\)(.*)\\\\\\.\\|\\?\\*\\+$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/optional.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "(a)" 3 | expected_regex: "^(?:a)?$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/parameter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "{int}" 3 | expected_regex: "^((?:-?\\d+)|(?:\\d+))$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/text.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: a 3 | expected_regex: "^a$" 4 | -------------------------------------------------------------------------------- /testdata/cucumber-expression/transformation/unicode.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: Привет, Мир(ы)! 3 | expected_regex: "^Привет, Мир(?:ы)?!$" 4 | -------------------------------------------------------------------------------- /testdata/regular-expression/matching/optional-capture-groups-all.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "^a (b )?c (d )?e (f )?g$" 3 | text: a b c d e f g 4 | expected_args: 5 | - 'b ' 6 | - 'd ' 7 | - 'f ' 8 | -------------------------------------------------------------------------------- /testdata/regular-expression/matching/optional-capture-groups-issue.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "^I should( not)? be on the map$" 3 | text: I should be on the map 4 | expected_args: [null] 5 | -------------------------------------------------------------------------------- /testdata/regular-expression/matching/optional-capture-groups-some.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | expression: "^a (b )?c (d )?e (f )?g$" 3 | text: a b c e f g 4 | expected_args: 5 | - 'b ' 6 | - 7 | - 'f ' 8 | --------------------------------------------------------------------------------