Modernize gherkin: spec-conformant parser, pickle compiler, cucumber-messages (3.0.0 proposal)#23
Open
tomasz-tomczyk wants to merge 34 commits into
Open
Modernize gherkin: spec-conformant parser, pickle compiler, cucumber-messages (3.0.0 proposal)#23tomasz-tomczyk wants to merge 34 commits into
tomasz-tomczyk wants to merge 34 commits into
Conversation
Vendored verbatim from cucumber/gherkin @ 374bc7a (see test/conformance/UPSTREAM.md): 46 good features (+ ast/pickles/tokens/source goldens), 11 bad features (+ errors goldens), and the 80-language gherkin-languages.json dialect data. These are the objective parity gate for the parser rewrite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cts) Loads the vendored 80-language dialect data, caches it in :persistent_term, and exposes keyword lookups per language/group (feature, scenario, step keywords, etc.). Custom UnknownLanguageError matches the bad/invalid_language golden message. Fully implemented and tested: asserts the 80-dialect count and en/fr/de keyword lookups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Type-only spine the parser fan-out builds against, modeled on the cucumber-messages schema: Gherkin.Location on every node; Gherkin.AST.* nodes (GherkinDocument, Feature, Rule, Background, Scenario, Examples, Step, DataTable, TableRow, TableCell, DocString, Tag, Comment); Gherkin.Pickle + Gherkin.PickleStep. Gherkin.Message serializes envelopes to NDJSON (source done; ast/pickle return :not_implemented). Gherkin.Pipeline behaviour + NotImplemented default give a config-swappable parser/compiler seam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gherkin.Conformance is the single public entry point the harness drives; it routes through the configurable Gherkin.Pipeline backend so a real parser raises the score with no harness edits. conformance_test.exs grades every good/ and bad/ corpus file (byte-exact after key-sort + uri normalization) and prints a baseline scoreboard (AST 0/46, Pickles 0/46, Errors 0/11). Tagged :conformance + :pending so the default mix test stays green; run via mix conformance / mix test --only conformance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ARCHITECTURE.md maps the module layout and the exact plug-in seams where the scanner, AST parser, pickles compiler and NDJSON serializers land (Gherkin.Pipeline backend + the two :not_implemented projections in Gherkin.Message), each mapped to the score column it moves. ROADMAP.md / PARITY.md capture the full plan and the objective conformance gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the scanner -> parser -> AST-serializer pipeline behind the
Gherkin.Pipeline behaviour, wired via config:
* Gherkin.AstParser.Scanner — one classified token per source line,
dialect-driven keyword matching, # language: header, CRLF handling,
table-cell + tag tokenization with escape/whitespace rules.
* Gherkin.AstParser — recursive-descent grammar producing the
Gherkin.AST.* tree (feature/background/rule/scenario/outline/examples/
steps/doc-strings/data-tables/tags/comments/descriptions).
* Gherkin.AstParser.IdAssigner — post-order id numbering matching the
reference cucumber AstBuilder (children, then tags, then node).
* Gherkin.Message.AST — projects the AST struct tree into the
cucumber-messages GherkinDocument map shape (gherkin_document_envelope/1).
All 46 testdata/good AST goldens pass byte-for-byte. Pickle compilation is
left stubbed (next wave). Existing 41 tests stay green; mix format clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the AST parser with structured error reporting matching the
reference parser's bad-path output:
* Error recovery in the feature/children walk: unrecognized lines are
recorded and skipped so multiple errors surface in one pass
(multiple_parser_errors).
* Unterminated doc-string detection -> unexpected-EOF error.
* Whitespace-in-tags detection.
* Tag-then-EOF expected-token set aligned with the reference
(#TagLine, #RuleLine, #Comment, #Empty).
* Errors sorted into source order; combined with semantic validation
(inconsistent table cell count).
All 11 testdata/bad error goldens now pass. Adds scanner + parser unit
tests (30 new). AST stays 46/46; existing tests green; format clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement Gherkin.AstParser.PickleCompiler and wire it as the pipeline's
compile_pickles/1 backend. The compiler:
* emits one pickle per Scenario and one per Examples body row of an outline,
expanding <placeholder>s in step text, data-table cells, doc-string content
and media type, and the scenario name;
* prepends background steps (feature background, then rule background) to each
pickle in scope, but skips them for a step-less scenario (which still emits an
empty pickle, matching the reference);
* inherits and unions tags Feature -> Rule -> Scenario/Outline -> Examples;
* resolves And/But/* (Conjunction) step types to the preceding non-conjunction
step's type.
Pickle ids use their own counter that continues from max(AST id) + 1; for each
pickle every step is numbered before the pickle itself. astNodeIds link a pickle
to [scenario] (or [scenario, examples-row] for outlines) and a step to [step]
(or [step, examples-row]). Add a `location` field to %Gherkin.Pickle{} (scenario
location, or the examples body-row location for outlines) as the goldens require.
Covered by 18 unit tests exercising outline expansion, background prepend/skip,
tag union, conjunction resolution, and table/doc-string substitution.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Gherkin.Message.Pickle (a mechanical snake_case -> camelCase projection
mirroring Gherkin.Message.AST) and implement Gherkin.Message.pickle_envelope/1
to use it. Pickle steps project astNodeIds/id/text/type plus an optional
argument: a dataTable of cell values (no row ids, no cell locations) or a
docString of content + optional mediaType (no location, no delimiter). Tags
project to {astNodeId, name}. to_ndjson's key-sort + nil-drop yields byte-exact
golden output.
Drives the conformance Pickles column to 46/46 (AST 46/46, Errors 11/11).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the runtime jason dependency in favour of the stdlib JSON module (Elixir 1.18+). Serializer now builds sorted objects explicitly, delegating only scalar leaves to JSON.encode!, preserving byte-exact conformance output. Bumps elixir floor to ~> 1.18.
Gherkin.parse/2, parse!/2 and pickles/2 are the new public surface, backed directly by Gherkin.AstParser.Pipeline (NOT via Application config, since Mix does not load a dependency's config/config.exs — a downstream git/hex dep would otherwise fall back to the stub). The optional config :gherkin, :pipeline override is still honoured when present. The original line-based parser surface (parse/1, parse_file/1, flatten/1, scenarios_for/1, producing Gherkin.Elements.* structs) moves to Gherkin.Legacy, kept for backwards compatibility. Add Gherkin.ParseError for the bang variants. gherkin_test.exs retargeted at Gherkin.Legacy; new public_api_test.exs added. 102 tests (was 89); conformance unchanged (AST 46/46, Pickles 46/46, Errors 11/11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tation
cabbage now consumes Gherkin.pickles/2, so the legacy line-based parser has no
remaining consumers. Delete it entirely:
- Gherkin.Legacy (the moved-aside surface from the previous commit)
- Gherkin.Parser and Gherkin.Parsers.* (FeatureParser, ScenarioParser,
BackgroundParser, RuleParser, StepParser, DescriptionParser, TableParser,
DocStringParser, TagParser)
- Gherkin.Elements.* (Feature, Scenario, ScenarioOutline, Step, Line, Rule)
- test/gherkin/parser_test.exs and test/gherkin/tag_parser_test.exs
Tag/outline/table behaviour these tests covered is exercised by the conformance
suite and the dedicated ast_parser/pickle_compiler tests. gherkin_test.exs is
rewritten against the public Gherkin.parse/2 + pickles/2 API. Stale doc comments
referencing the legacy modules are updated.
Gherkin.{parse/2, parse!/2, pickles/2} remain the canonical public API, backed
directly by Gherkin.AstParser.Pipeline. 78 tests; conformance unchanged
(AST 46/46, Pickles 46/46, Errors 11/11).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the official Gherkin grammar (Description := (#Other | #Comment)+), descriptions consume any line that is not one of the current context's terminator tokens. In Feature/Rule-header position that includes step keywords and `*` bullets (e.g. the CCK minimal.feature), which are description text, not steps. Make take_description context-aware via per-scope terminator sets that mirror the reference parser's state machine: - Feature/Rule header: background/tag/scenario/rule lines terminate - Background: step/tag/scenario/rule lines terminate - Scenario/Outline: + examples line terminates - Examples: table_row/tag/examples/scenario/rule terminate Description text now matches the reference AstBuilder exactly: leading empty lines dropped, interior blank lines preserved verbatim (raw whitespace), trailing blanks trimmed, each line kept as full raw text (no trailing trim, mirroring getLineText(0)). Conformance unchanged: AST 46/46, Pickles 46/46, Errors 11/11. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement the Markdown-with-Gherkin (MDG) dialect from cucumber/gherkin's
MARKDOWN_WITH_GHERKIN.md (referenced upstream commit 374bc7a8). A new
Gherkin.AstParser.MarkdownScanner emits the SAME token stream the existing
recursive-descent parser consumes, so MDG and the equivalent classic
.feature produce an identical AST/pickles. The scanner is a port of the
reference GherkinInMarkdownTokenMatcher:
- `#{1,6} <keyword>:` headers -> Feature/Background/Rule/Scenario/Outline/
Examples lines (column = indent + header-prefix length + 1)
- first significant non-block line -> Feature line fallback (whole-line name,
no keyword)
- `[*+-] <step keyword>` list items -> steps (the `* ` star keyword is the
bullet marker here, not a step keyword)
- GFM table rows indented 2-5 spaces -> table rows; GFM separator rows
(`| --- |`) and all other non-Gherkin prose are neutered to :empty
(hence MDG descriptions are always empty, matching the reference)
- fenced code blocks -> doc strings (longer fences treat shorter inner
fences as literal body)
- backtick-wrapped `@tag` -> tag lines (whitespace between tags is allowed)
API: Gherkin.parse/pickles take `markdown: true`; a `.md` :uri auto-detects
the dialect. Threaded through an optional Gherkin.Pipeline.parse/3 callback
and AstParser.parse/3; the conformance harness routes `.feature.md` uris to
the Markdown scanner.
Supporting fixes: the AST serializer omits a nil feature keyword (header-less
MDG feature); unescape_doc handles arbitrary-length backtick fences; the
whitespace-in-tags validation is skipped for Markdown tag lines.
Conformance: plain unchanged (AST 46/46, Pickles 46/46, Errors 11/11); the 5
vendored Markdown twins pass fully (AST 5/5, Pickles 5/5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Markdown-with-Gherkin scanner neutered every non-keyword line to
:empty, so MDG feature/rule/scenario descriptions were always empty. The
official MDG dialect does populate descriptions in one case: a GFM table
separator row (| --- |) is matched as a #Comment by the reference
GherkinInMarkdownTokenMatcher, and a #Comment opens the Description rule.
Once opened, the in-description parser state has no #Empty short-circuit,
so subsequent lines (including unindented table rows and blanks) are
collected as #Other description text, with trailing blanks trimmed.
This is the sole reason the CCK `markdown` sample's leading
"| boz | boo |" table row becomes the feature.description.
Changes:
* markdown_scanner: classify a GFM separator row (any indent) as a
:comment carrying `gfm_separator: true`, instead of neutering to
:empty. Non-separator unindented pipe lines stay :empty.
* ast_parser: a :comment now OPENS a description (started? = true),
mirroring the reference grammar's start_rule("Description"); and
collect_comments rejects gfm_separator comments so they never appear
in gherkinDocument.comments (the reference gives them matchedType
Empty).
Verified the full CCK markdown AST + pickles match the reference JS
implementation byte-for-byte (ids/uri aside). Conformance unchanged: AST
46/46, Pickles 46/46, Errors 11/11, Markdown AST 5/5, Pickles 5/5; the
5 markdown twins still emit empty descriptions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.tool-versions wrongly pinned Elixir 1.14.5-otp-26 / Erlang 26.0, which no longer matches the dev baseline or the ~> 1.18 mix.exs requirement. Point it at the actual baseline so mise resolves the right toolchain in this dir. Remove the legacy CircleCI config; CI moves to GitHub Actions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…retarget fork metadata - Remove excoveralls from deps and the test_coverage: [tool: ExCoveralls] config; switch to built-in coverage via 'mix test --cover'. excoveralls was the last puller of jason, so jason is now fully gone from the dep tree. - Set test_coverage summary threshold to 0 so --cover stays informational and never exits non-zero on coverage alone; the test + conformance suites are the CI gates. - Migrate the conformance harness from Jason.decode! to the built-in JSON (Elixir 1.18+), the project's only remaining Jason use. - Harden the conformance test: it now asserts 100% pass / 0 fail / 0 pending / 0 error in every area (AST, Pickles, Errors + Markdown twins). 'mix conformance' now exits non-zero on any regression (verified: a corrupted golden yields exit 2). - Retarget mix.exs package metadata at the fork (tomasz-tomczyk/gherkin) for source_url/homepage_url/links and maintainers, keeping MIT and a note that the hex package name / app: stays :gherkin (rename deferred). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gherkin.parse/2, parse!/2, pickles/2 and the Gherkin.Dialect public surface were already fully specced. Cover the last gaps on Gherkin.Message: media_type_plain/0, media_type_markdown/0, and encode_sorted/1. Verified clean under the Elixir 1.20 set-theoretic type checker (mix compile --warnings-as-errors) with all 19 gherkin modules compiling with zero type warnings; mix test and mix conformance green on both 1.18 and 1.20. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the removed CircleCI config. Uses erlef/setup-beam@v1 across the three supported Elixir/Erlang pairs from the official compatibility table: 1.18.1 / OTP 27, 1.19.5 / OTP 28, 1.20.0-rc.6 / OTP 27. 1.20 is still an RC, pinned to the exact version the .tool-versions baseline uses. Each leg caches deps + _build keyed on elixir/otp/mix.lock, then runs mix deps.get, format --check-formatted, compile --warnings-as-errors, test, and the conformance gate (mix conformance), which exits non-zero on any corpus regression. Triggers on push and pull_request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old README documented a long-gone API (Gherkin.Elements.Feature, stream parsing). Rewrite it to describe the current parser: the parse -> AST -> pickles pipeline, the conformance status (AST/Pickles/Errors 46/46/11 + Markdown 5/5 against the official cucumber/gherkin corpus), Elixir 1.18+ / built-in JSON / zero runtime deps, a GitHub Actions status badge, and accurate Gherkin.parse/2 and Gherkin.pickles/2 usage (the passing module doctests). Add a clear attribution section: a fork of cabbage-ex/gherkin (orig. authors Matt Widmann / Steve B / Max Marcon), MIT. Drop the dead Hex/coveralls/CircleCI badges. Add the MIT LICENSE file the package metadata and README reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The conformance alias is mix test --only conformance, which must run in the test env. Without def cli/preferred_envs, running mix conformance from a default shell aborted with "mix test is running in the dev environment", breaking the CI conformance gate. Add preferred_envs: [conformance: :test]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pipeline is fully shipped. Gherkin.AstParser.Pipeline is the only backend and both entry points (public Gherkin, Gherkin.Conformance) now default to it directly, so the stub backend and its sentinel value were unreachable. - Delete Gherkin.Pipeline.NotImplemented and drop :not_implemented from the Gherkin.Pipeline callback return types. - Default Gherkin.Conformance to Gherkin.AstParser.Pipeline; drop every :not_implemented branch from ast/pickles/errors_ndjson. - Drop the not_implemented type, the _other fallback clauses, and the to_ndjson(:not_implemented) skip from Gherkin.Message. - Trim config/config.exs: the :pipeline config line is no longer needed (both entry points default directly) and its comment was false (pickles shipped). - Refresh stale moduledocs (Message, Conformance, dialect_test). The optional :pipeline override (public API opt + config) is retained as a test seam. Conformance + full test suite stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
keywordType derives purely from the keyword's dialect group(s); the previous step accumulator was never read. Make it a 2-arity function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The old doc described a pre-implementation type-only spine and referenced deleted legacy files (lib/gherkin/elements/*, parser.ex, parsers/*, Gherkin.parse/1) and a stub backend returning :not_implemented. Replace it with the actual scanner/markdown-scanner -> parser -> id-assigner -> pickle compiler -> message serializer design, the Dialect i18n foundation, the single AstParser.Pipeline backend, and the conformance harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feature_tag_errors/1 always returned [] — there is no feature-tag semantic validation in the reference parser (tags are validated at scan time). Drop the dead clause and its call from validate/1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
split_lines/1, drop_trailing_empty/1 and leading_space_count/1 were duplicated byte-for-byte between Scanner and MarkdownScanner. Extract them into a shared pure module Gherkin.AstParser.LineUtil so the two scanners cannot drift. Also document the block-group try-order invariant in both scanners: only scenario_outline-before-scenario is load-bearing; the differing :rule position between the two lists is intentional and harmless (groups are disjoint). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…kage Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rpus Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The recursive-descent parser already turns a Markdown table that precedes the first heading into the Feature's description (the GFM separator row is a match_Comment that opens the Description rule). But Gherkin.Conformance routed .feature.md sources to the Markdown scanner with a bare function_exported?/3 guard, which returns false for a backend module that has not yet been loaded. In a fresh VM the first .feature.md parse therefore fell through to the plain .feature scanner, which rejects Markdown prose and emitted spurious parse errors instead of the AST -- dropping feature.description. Pair the guard with Code.ensure_loaded?/1 so routing is robust to module load order. Vendor the CCK markdown sample into the conformance corpus and add a regression test that purges the backend module to pin the routing fix; the sample's feature.description is exactly "| boz | boo |", matching the reference byte-for-byte.
Address review feedback comparing next vs upstream/master ahead of the
cabbage-ex/gherkin proposal:
- ci.yml: bump actions (checkout@v6, cache@v5); drop redundant comments
- mix.exs: remove `mix new` boilerplate and the package-name NOTE comment
- LICENSE: drop the "(fork)" qualifier
- README: lead with Highlights → Usage; drop Attribution/License prose
- message/{ast,pickle}.ex: make moduledocs self-sufficient (drop
ARCHITECTURE.md references)
- Move ROADMAP.md and PARITY.md out of the repo (dev-process artifacts,
now under ../); PARITY.md was also stale
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-audit cleanup (low-risk, conformance-gated): - Promote the duplicated `location_map/3` and `put_optional/3` helpers to `Gherkin.Message` (@doc false) and import them in the AST and Pickle serializers, removing three copies of the location projection and two of put_optional. Behaviour unchanged (location_map now also accepts nil, matching the pickle path). - Add an "Architecture" module map to the `Gherkin` moduledoc: the four stages (lex/parse/compile/serialize) mapped to their modules, plus a note that `Gherkin.Conformance` is test/CI-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1.20.0/1.20.1 are now released; drop the 1.20.0-rc.6 pin so the matrix tracks stable, matching cabbage's CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point @source_url (and the homepage that follows it) at the canonical cabbage-ex/gherkin repo ahead of the proposal PR. Drop the now self-referential "Upstream (cabbage-ex/gherkin)" package link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hello! I'm Tomasz, I've contacted the maintainer of this GH org with the interest of updating both
gherkinandcabbagepackages to the latest & greatest standards. I've driven Claude through this, as it is indeed a lot of work, but thankfully the CCK from Cucumber makes this a great candidate for LLM tools IMHO!What this is
A proposal to revive and modernize
gherkinas the canonical Elixir Gherkin package. This is a near-total rewrite — please review it as a proposal with evidence, not a line-by-line diff. It targetsnext(notmaster) so nothing takes overmasteruntil we agree to integrate.Highlights
# language:headers.mix compile --warnings-as-errorsclean; CI matrix Elixir 1.18 / 1.19 / 1.20.Evidence
mix compile --warnings-as-errorsclean ·mix test --include conformance→ 7 doctests, 99 tests, 0 failures ·mix format --check-formattedclean.Standing issues / PRs this addresses
This rewrite subsumes the following open items:
# language:Notes
3.0.0(never published to Hex, so no unreleased split).cabbagerunner has a companion proposal PR that depends on this.