# Cucumber-Ruby
Ruby support is experimental and opt-in. It requires explicit definition and feature roots for one Cucumber suite. Include support files that register steps.
This release does not claim complete Ruby semantics or feature parity with the JavaScript/TypeScript adapters. Reflective local rebinding through called methods, returned Binding values or other files, and indirect DSL mutation can escape static tracking and affect finding accuracy. Ruby sources are not excluded as binary: NUL inside comments, strings and regexps is valid Ruby, and a top-level NUL ends the executed source, so registrations after it are not extracted. An oversized loaded provider is an incomplete load, not a run failure; `--fail-on-incomplete` rejects it. Review Ruby findings against source; a clean run, including one with `--fail-on-incomplete`, cannot certify behavior outside the supported subset.
Start with an exploratory run. It reports findings from the supported subset and lists what it could not resolve:
```sh
cuke-dedup path/to/project --definitions 'features/**/*.rb' --features 'features/**/*.feature'
```
For a CI gate, also require both corpora and fail on incompleteness. Unresolved requires, autoloads, dynamic `step` calls and DSL changes are common in Ruby suites, so this command fails until the suite stays inside the supported subset or the gaps are configured away, for example with `rubyLoadPaths`:
```sh
cuke-dedup path/to/project --definitions 'features/**/*.rb' --features 'features/**/*.feature' --require-definitions --require-features --fail-on-incomplete
```
The adapter never executes Ruby, Gemfiles, hooks or dependencies. Explicit selection establishes DSL context but cannot resolve every runtime override.
Code that may redefine the DSL or dispatch dynamically (`send` with a computed method name, `eval`, `define_method`, `include`, method-table changes) withdraws trust from every Ruby registration in the suite, because it could run while files load. Calls inside step handler blocks and scenario hook blocks (top-level, receiverless `Before`, `After`, `Around`, `AfterStep`, `BeforeStep`) run only during scenarios and do not count, while those methods are the runner's own: redefining a step keyword or scenario hook withdraws trust. Definitions, aliases and `self` exposure inside those blocks still count, and so do `BeforeAll`, `AfterAll`, `AfterConfiguration` and `InstallPlugin` blocks. A call is also exempt when it sits directly in an instance method (`def name`) of a `class` or `module` body and the analyzed suite's load-time code is closed: every top-level and class-body statement is a comment, a registration (step keywords, `ParameterType`, `Before`/`After`/`Around`/`AfterStep`/`BeforeStep`) with literal arguments, a literal `require`/`require_relative`, or a `class`/`module` declaration with no superclass, whose name is not a constant already defined by Ruby, its standard libraries, Bundler, or the Cucumber runner and its dependencies, and which contains only comments, instance methods and further such declarations; and every top-level load resolves and every selected file is analyzed. Then no load-time code can call that method, so the call can only run during scenarios. Any other load-time statement in an analyzed file (an assignment, `World`, `include`, a top-level `def`, an unresolved top-level `require`) restores the suite-wide withdrawal, and the warning names the call. The check covers the files the analyzer reads: a file Cucumber loads that is not selected (such as an unselected `features/support/env.rb`) is not seen, as with every other registration decision, so select support files when the guarantee matters. Loads inside step handlers and methods run only when those do and do not open the suite.
Default discovery remains JS/TS-only. A definition pattern selects `.rb` files only when it depends on the suffix, as `features/**/*.rb` or `features/support/env.rb` do. Suffix-agnostic patterns such as `features/**` keep their JS/TS selection and report skipped Ruby files in a warning, except files the source graph below follows a load to, which are analyzed. Analyze independent suites separately: a selection that contains both Ruby and JS/TS sources is rejected before comparison.
## Registrations and providers
Direct top-level step keywords and the underlying `register_rb_step_definition` method accept static string/regex matchers and inline `do/end` or brace blocks, with parenthesized or command-call syntax. Cucumber-Ruby defines the step keywords of every Gherkin dialect, whatever the feature language, so `Angenommen`, `Dado` and `假如` register like `Given`. Keywords containing spaces or punctuation, apart from a trailing `!` or `?` as in `Avast!`, can only be called through `send` and remain incomplete. Strings are Cucumber Expressions; slash and `%r` literals (any ASCII punctuation delimiter; `() [] {} <>` nest by depth; a backslash escapes the next character) are Ruby regexes. `%r=…=` and whitespace delimiters are valid Ruby but tree-sitter-ruby cannot parse them, so they are reported as syntax errors and the run is incomplete. A backslash before a non-bracket delimiter, `/` included, is dropped unless that character is regex syntax (`$ * + . ? ^ | ) ] } >`), as Ruby does; `%r\…\` is unrecognized and incomplete. Double-quoted `\uXXXX` and `\u{...}` scalar escapes decode before comparison; single-quoted escapes remain literal. Invalid Unicode scalars are unsupported. A step keyword or core registrar called through any receiver (`me.Given('a step') { ... }`, `self.register_rb_step_definition('a step') { ... }`, `target.When(...)`, `@browser.a(...)`, a local instance or namespace alias) is incomplete at the call when it can register, because any receiver may evaluate to main: a method can return `self`, and a local can be rebound through `binding` or `binding_of_caller`. Cucumber-Ruby registrations take `(pattern, symbol = nil, options = {}, &proc)`, need a `String` or `Regexp` pattern, and use `symbol || proc`, which must be a `Symbol` or a `Proc`. Each argument is classified by the value Ruby gives it: a String or Regexp (string, character and regex literals, a non-backtick heredoc, `__FILE__`), a Symbol or Proc (symbols, lambdas, a `def`, which returns its name), `nil` or `false` (also an `in` pattern test), another known type (numbers, including a sign written directly before them such as `-1`, while `- 1` is a method call; `true`, arrays, hashes, ranges, `__LINE__`, `__ENCODING__`), or a value only runtime decides; an assignment takes its right-hand side and parentheses their last statement. Keyword pairs and a literal non-empty `**hash` are one trailing Hash argument, while another `**hash` may be empty (parentheses and nested `**` literals count, and `**nil` is empty); each splat or `...` supplies any number of arguments, while a literal array splat supplies its elements, its keyword pairs forming one Hash element and an unknown `**hash` entry adding nothing or one Hash. A call can therefore register when one of its possible argument lists has at most three arguments, a first argument that may be a String or Regexp, and either a second argument that may be a Symbol or a Proc, or a second argument that is absent, `nil` or `false` together with a block, a block argument whose operand is not `nil`, or `...`, which forwards a block. Ruby converts a block argument with `to_proc`, which code can define on any class (`&false` raises a TypeError until `FalseClass#to_proc` exists, then passes a Proc); only a `nil` operand (`&nil`, `&()`, `&(1; nil)`) passes no block without conversion, and a `nil` splat (`*()`) passes nothing. Calls that cannot register, for which Cucumber-Ruby's registrar raises, are ignored, such as Watir's `browser.a(text: 'Go')` and `browser.a(:text, link)`, Builder's `xml.a(href: '/x') { ... }`, and `x.Given('a step')` without a block. `self.Given` follows the same rule.
Immutable registration captures support subsequent top-level `.call` uses and closed local alias chains. Top-level keyword aliases such as `alias setup Given` are unsupported: Cucumber installs the registrar on main’s singleton class. Reassignment, shadowing, escape, unknown dispatch and lookup/invocation overrides remain incomplete.
Preceding resolved loads can establish immutable top-level captures, constant re-exports, closed namespace aliases and transparent module forwarders: `ROOT_GIVEN = method(:Given); module Provider; GIVEN = ::ROOT_GIVEN; end`, then `register = Provider::GIVEN; register.call('a step') { work() }`. A dependency-only consumer may inherit earlier sibling loads only when every resolved caller initializes the provider first and no unresolved loader can introduce another context; independently selected files cannot use that proof. Unresolved external loads also disable sibling inheritance. Unloaded files, namespace/constant replacement, escapes, conflicting definitions and uncertain lookup cannot establish provenance. Receiverless captures inside modules/classes, arbitrary namespace factories, cross-file locals and generated providers are unsupported. Library consumers call `SourceAdapter::prepare_session` with selected files and resolved dependency edges before extraction. Loading edges default to unknown entry provenance; mark verified dependency-only targets with `SourceDependency::with_dependency_only_target(true)`.
Local forwarding helpers require a top-level method whose entire body forwards one matcher and one block parameter unchanged: `def given_step(text, &handler); Given(text, &handler); end`. A later same-file top-level call may supply a static matcher and inline block. Uncalled declarations register nothing. Extra effects, transformed arguments, nested or cross-file calls are unsupported. Replacement and unresolved references invalidate wrapper trust; unrelated same-spelled methods may conservatively prevent discovery.
## Handlers and assertion evidence
Registrations also accept source-bound `&lambda`, `&proc` and unique preceding named handlers. Root `method(:name)` captures preserve method semantics; private root methods do not establish `public_method` captures. Class-bound methods require a unique loaded class and stable constructor origin; their registrations remain available, but receiver-dependent comparison is incomplete. A closed local lambda/proc invoked once at top level can register inline steps; its captured scope remains non-comparable. Custom allocation, block conversion, lookup overrides and escaped/replaced handlers cannot establish provenance. Closed provider ownership never grants assertion trust.
Handlers that only mark their step pending are excluded from handler rules, as in the JavaScript frontend: an empty body, or one statement that is a `pending`, `todo` or `not_implemented` call, a `return`/`next` of one, or `raise`/`fail` whose arguments carry a `pending`, `todo` or `not implemented` label (`raise Cucumber::Pending`, `fail 'todo'`); an unlabelled `raise NotImplementedError` still counts, like `throw new NotImplementedError()`. Rules cover duplicate matchers, exact supported handler reuse, and usage/ambiguity/overlap for supported matchers. Fingerprints preserve tree structure, literals, receivers, writes and operation order. Equivalent parameters/locals may be renamed; methods, properties, captures and shadowing remain significant. Bare helper calls differ from local reads. Nested blocks, lambdas, interpolation and rescue/ensure retain exact structure. A handler-local lambda assigned once in the handler scope executes in place at every later zero-argument `.call` or `.()` invocation, in any expression position, one level deep; inside a nested block the expansion is deferred and keeps its values. A declared lambda that is never invoked or reassigned, or declared inside a nested block or lambda, is syntax, not behaviour; a parameterized declared lambda invoked with arguments leaves its values unproven and withdraws comparison. Passing the lambda, indexing, operator assignment and mixed writes keep its deferred value-bearing identity. Captured top-level locals require the same file context, a single simple literal, lambda or proc initializer and no binding replacement or mutable-value escape. Immutable scalar reads preserve comparison; unresolved mutable captures remain non-comparable. Block parameters/locals preserve shadowing: a write inside a block to a name the block rebinds never reaches the capture. `for` and pattern variables, `rescue => e` and a named-group regex literal on the left of `=~` bind locals and keep handlers uncertain; a bare `rescue`, a regex on the right, an interpolated regex or a regex held in a variable bind nothing. Top-level constants assigned once with an integer, float, symbol, boolean, nil or frozen string literal resolve to their value, before or after the registration; reassigned, qualified, nested or operator writes, unfrozen strings, unknown constants and files calling `const_set`, `remove_const` or `const_missing` retain the name. Registrations inside executed lambdas resolve enclosing parameters and single-literal block-locals before file locals; their handlers remain non-comparable. Source-location values remain context-sensitive without absolute paths. Dynamic lexical access, method/class/module definitions, heredocs and unresolved bindings are non-comparable and incomplete.
Near-wording findings require compatible capture contexts and supported handler evidence. An ordinary handler whose every construct is modeled publishes an action stream like the JavaScript frontend's: one event per call naming the receiver-chain root and the method with arguments dropped (`page.locator('#save').click()` records `page#locator` and `page#click`; a receiverless root call is the base of its chain, so `helper.run` and `helper().run` compare equal), one event per bare helper call, callbacks attached to or passed to calls, and control flow. Near wording then needs ordered event overlap at or above the configured similarity, and at least one differing event: handlers whose streams are identical but whose syntax differs (literals, symbols, assignments) are parameterization material, never near duplicates. Blocks or lambdas with parameters, element references, operators, `super`, `yield`, splats, interpolation, `rescue`, and untrusted receiverless `expect` chains (with or without arguments or parentheses) keep the exact-handler wording policy for the whole handler, also beside configured assertions, so receiverless RSpec `expect(...).to`/`.not_to` pairs never become near findings; an ordinary helper named `expect` loses near recall. Other untrusted assertion idioms (`is_expected`, `should`, Minitest `assert_*`/`refute_*`) compare as ordinary actions, so a polarity pair beside enough shared actions can still reach the near threshold, as in the JavaScript frontend with an unrecognized assertion library. A lambda or proc literal is a callback only when handed to a call, directly or inside an argument container; elsewhere it is syntax. Straight-line ordinary-call parameterization preserves bindings, methods, order, arity, literal types and safe navigation. With configured assertion evidence, other handlers abstract literal values for parameterization while their behaviour events keep every value, and operations retain complete syntax, including receivers, arguments, attached callbacks, writes and control flow. Different assertion values, subjects or polarity cannot reach equivalence; like the JavaScript frontend, they count as one differing event, so a pair that also shares enough ordinary behaviour can still be a near finding (`expect(x).to_be(1); settle` beside `to_be(2); settle` is near at a 0.5 floor and not at the default 0.70). A declared callable's declaration and a plain local write that only stores its invocation's result are not events of their own; writes to other destinations and operator writes stay effects. Deferred assertion sequences must still match; a parameterized callback withdraws handler comparison only when it contains an assertion, where the JavaScript frontend withdraws for any parameterized callback. An assertion nested in control flow or in an inline-invoked literal also differentiates that construct, so Ruby reports fewer near findings there than JavaScript does.
Explicitly configured assertion factories add value- and polarity-sensitive evidence under the [closed provider contract](../configuration.md#trusted-assertion-modules). Bounded source proofs support immutable namespace, Method, hash-export and factory aliases, static member dispatch, and preceding fixture re-exports; resolved aliases compare across files by spelling and owner while unresolved captures stay file-local. Unknown calls, escapes and shared mutations withdraw comparison authority. Shallow copies retain shared nested fields. RSpec, Minitest and Capybara receive no inferred assertion trust.
Named handlers reuse evidence from their exact source. Supported constructor-injected assertion fields retain allocation and file context; unrelated instance state stays uncertain. An unresolved configured assertion disables optional handler comparison while preserving resolved registrations. Parsing, binding, loading and proof-budget uncertainty still report incompleteness.
## Matchers
Cucumber Expression comparison shares Unicode, whitespace and placeholder normalization across languages without changing executable text. Supported Ruby regex translation normalizes named captures and escaped spaces while preserving capture arity, anchors, quantifiers and all flags. Flag order/repetition is irrelevant; encoding flags remain distinct. Unsupported patterns retain source identity.
| Regex form | Matching policy |
| --- | --- |
| Line anchors, noncapturing groups, ASCII digit/word/space classes and complements | Supported conservative Ruby-to-Rust subset. |
| ASCII punctuation identity escapes and escaped spaces | Literal inside and outside classes. |
| Unique ASCII `(?<name>...)` / `(?'name'...)` captures | Capture order retained; ordinary groups become noncapturing when named groups exist, matching Ruby. |
| Literal braces and supported repetition, including `{,n}` | Ruby semantics retained. |
| `m` | Dot-all, independent of line anchors. |
| `i`, optionally with `m` | ASCII literal patterns with optional line anchors only; excludes `ss`, `ff`, `fi`, `fl`, `st`, `ffi`, `ffl`, whose Ruby Unicode folding cannot be reproduced. Non-ASCII, escapes, classes, groups and quantifiers remain indeterminate. |
| `u` / `n` | UTF-8 patterns / ASCII-source patterns against UTF-8 feature text. |
| Other encodings, conflicting flags, non-ASCII binary patterns, duplicate capture names, lookarounds, backreferences, possessive quantifiers, unsupported escapes/class extensions | Indeterminate. |
Indeterminate execution cannot justify unused or overlap findings; identical literal matchers can still establish duplicates.
## Ownership, dependencies and usage
Dynamic/nested registrations and local DSL replacement are untrusted; self-qualified and other receiver calls that can register report incomplete discovery (see Registrations and providers). Isolated module method definitions and ordinary aliases preserve top-level trust. Qualified declaration prefixes need preceding local namespaces; missing/shadowed prefixes cannot establish ownership by spelling. Namespace aliases, owner escapes, dynamic installation and unresolved reflective receivers remain conservative. Registrar-affecting mutations invalidate registrations across the selected suite. Any reopening of `Cucumber::Glue`, its singleton methods or calls on it count as registrar-affecting whatever the method names, because that namespace implements registration. Ordinary helpers, including top-level methods and classes without a superclass, keep trust.
`include`, `extend`, `prepend` and subclassing run hooks (`included`, `extended`, `prepended`, `inherited`) that can change any owner, and installed gems are never read. Outside step bodies these forms therefore keep the selected suite untrusted. Support modules that include gem modules such as `Capybara::DSL`, and page objects with a superclass, cause this today. The diagnostic names the call or superclass that caused it.
Ruby definition globs define the analysis entry points. Caller inheritance uses only resolved static source loads; it does not infer runner configuration. Unknown loaders retain incompleteness; a loader call without operands (`load()`, `require()`) loads nothing and is an ordinary call. Literal top-level `require_relative` and `require` against explicit `rubyLoadPaths` form a bounded source graph. As in Ruby, a request loads `<request>.rb` unless it already ends in `.rb`, so dotted names such as `pages.v2` load `pages.v2.rb`; a request ending in a native-library suffix (`.so`, `.o`, `.dll`, `.bundle`) loads `<request>.rb` only when no searched directory holds a native library for it under any of those suffixes, and otherwise stays unresolved, because Ruby prefers a native library and which suffix it accepts depends on the platform. Configured `assertionModules` resolve the same way; a load past its file or byte limit is not followed and marks analysis incomplete at that load, and provider proofs over a selection above their limit are unavailable (see [Safety and limitations](../safety-and-limitations.md#input-limits)). Missing/unsupported loads remain incomplete; external providers such as `aruba/cucumber` need source roots and supported registration semantics. Eager autoload withholds suite trust; deferred autoload preserves initial registrations but marks analysis incomplete. Findings from supported local definitions remain possible; zero findings cannot certify the suite.
Literal `step` calls, including support-file and deferred calls, prevent matching unused findings without proving reachability. Dynamic calls, unresolved receivers, `steps`, delegation replacement, unresolved dependencies and unreadable selected sources disable Ruby unused findings and report incompleteness. Resolving a load removes only that loading uncertainty. Indirect calls neither increase feature counts nor establish ambiguity. Library consumers merge `Extraction.indirect_usage` and use `analysis::analyze_with_step_usage`; older entry points report incomplete usage without it.
Closed, unchanged, unescaped local instances may dispatch literal methods declared on a preceding loaded class without inheritance or custom class-level `new`/`allocate`. This proves receiver isolation only. Reflection uses resolved targets: unrelated literal method updates preserve registrations; uncertain eager dispatch and registration/dispatch API replacement invalidate the suite.
## Parameter types
Direct top-level `ParameterType` supports literal names, string/regex/array patterns and deferred lambda/proc transformers. Direct inline transformer bodies are deferred; dynamic property access there preserves adjacent registrations but establishes neither transformer equivalence nor assertion trust. Unsupported forms and dynamic indirect usage remain incomplete.
Source patterns supersede configured fallbacks. Duplicate names and unresolved declarations remove matching authority and mark incompleteness; unknown names and built-in redefinitions invalidate registration trust. Regex flags, nested arrays, immediate transformer invocation and preferential-regex registration are unsupported. Library consumers receive `Extraction.parameter_types` separately from handlers; the CLI merges the selected registry before expression matching.
Back to [Discovery and frameworks](../discovery-and-frameworks.md).