# Gherkin Refine > Gherkin and Cucumber linter for Node.js. Supports all 31 gherkin-lint 4.2.4 rules, installs the same `gherkin-lint` command, reads `.gherkin-lintrc`, and adds safe autofixes, plugins, and JSON, NDJSON, and SARIF output. npm package: `gherkin-refine` (version 1.4.6). Requires Node.js 22.18 or later. ESM only. MIT licensed. Install with `npm install --save-dev gherkin-refine` and lint with `npx gherkin-refine .`. For automated consumers, use `--format json`. Not every gherkin-lint behavior is identical: `xunit` output is unavailable and custom `--rulesdir` rules need porting to a plugin. # Getting started Gherkin Refine needs Node.js 22.18 or later. ## Install and lint ```sh npm install --save-dev gherkin-refine npx gherkin-refine . ``` The CLI finds `*.feature` files and skips `.git`, `node_modules`, `dist`, and `coverage`. Without a configuration file it runs the recommended rules: duplicate tags, duplicate Feature and Scenario names, and unused or undeclared Scenario Outline variables. Exit code 0 means no errors, 1 means lint errors or too many warnings, and 2 means a configuration or usage error. ## Coming from gherkin-lint Replace the `gherkin-lint` dependency with `gherkin-refine`. The package installs a `gherkin-lint` command and reads `.gherkin-lintrc` and `.gherkin-lintignore`, so scripts and configuration keep working. See [Migrate from gherkin-lint](migration.md). ## Configure Create `gherkin-refine.config.json`, or a `.js`, `.mjs`, or `.ts` variant: ```json { "extends": ["recommended"], "rules": { "scenario-size": ["warn", { "maxSteps": 12 }], "no-trailing-whitespace": "error" }, "ignores": ["generated/**/*.feature"] } ``` Every rule and its options are listed in [Rules](rules.md). [Configuration](configuration.md) covers overrides, presets, and inline suppression comments. ## Fix safely Formatting rules such as `no-trailing-whitespace`, `no-extra-blank-lines`, `indentation`, and `use-and` have safe autofixes: ```sh npx gherkin-refine features/ --fix-dry-run npx gherkin-refine features/ --fix ``` A fix is applied only when the result still parses as valid Gherkin. ## Run in GitHub Actions ```yaml name: Gherkin on: push: pull_request: permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 22.18.0 cache: npm - run: npm ci - run: npx gherkin-refine . ``` For code scanning, `--format sarif` writes SARIF output. [AI agent integration](agent-integration.md) covers JSON output for tools and agents. # Migration from gherkin-lint Replace the `gherkin-lint` dependency with `gherkin-refine`. The package installs a `gherkin-lint` command, reads `.gherkin-lintrc` and `.gherkin-lintignore` from the working directory, and accepts the gherkin-lint flags below, so existing scripts keep working. The [comparison](comparison.md) summarizes what changes beyond the rules. A `.gherkin-lintrc` runs only the rules it lists. When it lists rules that gherkin-refine cannot fully check, each run prints one warning line on stderr. To review those differences, or to move to a native configuration, convert the file. The historical `.gherkin-lintrc` file is JSON and may contain comments. Run: ```sh gherkin-refine migrate .gherkin-lintrc --dry-run gherkin-refine migrate .gherkin-lintrc ``` The migration writes `gherkin-refine.config.json` and does not replace an existing file unless `--force` is provided. Review every reported behavior difference before enabling the generated configuration. The generated configuration sets `extends: []`. gherkin-lint runs only the rules a file lists, so the recommended preset stays off until you remove that line. Rules set to `off` in the legacy file are not reported as unsupported. Keep `.gherkin-lintignore`. gherkin-refine reads it from the working directory on every run, as gherkin-lint does, and adds its non-empty lines to `ignores`. | Legacy rule | New rule | Status | | --- | --- | --- | | `no-duplicate-tags` | `no-duplicate-tags` | Same intent | | `no-dupe-feature-names` | `no-duplicate-feature-names` | Scope is the set of linted files | | `no-dupe-scenario-names` | `no-duplicate-scenario-names` | The default `anywhere` maps to `{ scope: "anywhere" }`; `in-feature` maps to one Feature or Rule. Names compare case-insensitively | | `no-unused-variables` | `no-unused-outline-variables`, `no-undefined-outline-variables` | Unused Examples columns and undeclared placeholders map separately. Each undeclared placeholder is reported where it appears | | `scenario-size` | `scenario-size`, `background-size` | Scenario and Background limits map separately. Without options, both use the gherkin-lint default of 15 | | `name-length` | `name-length` | Same per-node limits for Feature, Rule, Scenario, and Step | | `no-trailing-spaces` | `no-trailing-whitespace` | Same intent, with safe autofix | | `no-multiple-empty-lines` | `no-extra-blank-lines` | Same intent, with safe autofix | | `keywords-in-logical-order` | `logical-keyword-order` | Uses parser semantic step types, including localized dialects | | `allowed-tags` | `allowed-tags` | Same `tags` and `patterns` options. Tags inside Rule blocks are also checked | | `no-restricted-tags` | `no-restricted-tags` | Same `tags` and `patterns` options. Tags inside Rule blocks are also checked | | `indentation`, `new-line-at-eof`, `file-name`, `use-and`, `one-space-between-tags`, `required-tags`, `no-restricted-patterns` | Same names | Same options and findings. `indentation`, `new-line-at-eof`, `use-and`, and `one-space-between-tags` have a safe autofix | | `no-unnamed-features`, `no-unnamed-scenarios`, `no-empty-file`, `no-files-without-scenarios`, `no-empty-background`, `no-background-only-scenario`, `no-scenario-outlines-without-examples`, `no-examples-in-scenarios`, `no-partially-commented-tag-lines`, `no-superfluous-tags` | Same names | Same findings. Except for `indentation`, rules that walk Scenarios also check the ones inside Rule blocks | | `no-homogenous-tags` | `no-homogeneous-tags` | Spelling corrected; see the fixes below | | `max-scenarios-per-file` | `feature-size` | Same count and options: `countOutlineExamples` defaults to `true` when migrated, `maxScenarios` to 10 | | `only-one-when` | `only-one-when` | `And` after `When` no longer counts as a second `When` ([gherkin-lint#345](https://github.com/gherkin-lint/gherkin-lint/issues/345)) | | Custom rules from `--rulesdir` | none | Reported as unsupported | Three rules fix gherkin-lint bugs and can only report less than before: `no-homogeneous-tags` ignores a single Scenario or Examples block and reports one finding per tag ([#231](https://github.com/gherkin-lint/gherkin-lint/issues/231), [#257](https://github.com/gherkin-lint/gherkin-lint/issues/257), [#170](https://github.com/gherkin-lint/gherkin-lint/issues/170)), `no-background-only-scenario` ignores a Background without Scenarios ([#159](https://github.com/gherkin-lint/gherkin-lint/issues/159)), and `only-one-when` as above. `migrate --strict` fails without writing a file when any rule cannot be fully migrated. `test/gherkin-lint-parity.test.ts` runs gherkin-lint 4.2.4 and gherkin-refine on the same fixtures in CI for every rule. Most cases compare each finding by file and line; the others compare line text, counts, affected files, or a subset. See the [comparison](comparison.md#behavior-that-changes) for the split. This is a migration aid, not a compatibility mode. Review output because the former and current tools have different parser and scope behavior. ## CLI flags | gherkin-lint | gherkin-refine | | --- | --- | | `-c, --config` | `-c, --config`, including a path to a `.gherkin-lintrc` | | `-f, --format` | `-f, --format`. `xunit` has no equivalent; `json`, `ndjson`, and `sarif` are available | | `-i, --ignore` | `-i, --ignore`. The comma-separated patterns replace `.gherkin-lintignore` | | `-r, --rulesdir` | `-r, --rulesdir`. A directory without `.js` files is accepted; custom rules must be ported to a [plugin](plugins.md) | # Gherkin Refine and gherkin-lint This page compares [gherkin-lint](https://github.com/gherkin-lint/gherkin-lint) 4.2.4 with Gherkin Refine 1.4.1. The [migration guide](migration.md) maps each gherkin-lint rule and CLI flag. ## Compatibility summary Gherkin Refine replaces the gherkin-lint command. It does not reproduce every gherkin-lint finding. - Stays compatible: the `gherkin-lint` command, `.gherkin-lintrc`, `.gherkin-lintignore`, the `-c`, `-f` and `-i` flags, and all 31 built-in gherkin-lint rules. - Changes: some findings, because of the newer parser, Scenarios inside `Rule` blocks, and three fixed gherkin-lint bugs. See [behavior that changes](#behavior-that-changes). - Not supported: `xunit` output and custom rules. `-r` or `--rulesdir` exits with code 2 when the directory holds `.js` files, so port those rules to a [plugin](plugins.md) first. ## Should you switch Consider switching if the project runs Node.js 22.18 or later, none of the reasons to stay below apply, and you want any of: a maintained Cucumber parser, `warn` severity, inline suppression, safe autofix, or SARIF output. Stay on gherkin-lint for now if the project runs an older Node.js, CI reads `xunit` reports, custom `--rulesdir` rules are not worth porting yet, or something downstream depends on the exact gherkin-lint findings. gherkin-lint has years of production use. Gherkin Refine is newer and independently maintained, and its parity tests reduce migration risk but do not replace that track record. ## Feature comparison | | gherkin-lint 4.2.4 | Gherkin Refine 1.4.1 | | --- | --- | --- | | Published | December 20, 2023 | October 12, 2026 | | Node.js | 10 or later | 22.18 or later | | Module format | CommonJS | ESM only API with TypeScript types. The CLI runs from CommonJS and ESM projects on Node.js 22.18 or later | | License | ISC | MIT | | Gherkin parser | `gherkin` 9.0.0, deprecated in favor of `@cucumber/gherkin` | `@cucumber/gherkin` 42 | | Built-in rules | 31 | 35: the 31 gherkin-lint rules as 33, because `no-unused-variables` and `scenario-size` each split in two, plus `tag-pattern` and `unused-disable-directive` | | Scenarios inside `Rule` blocks | In a test of 13 rules, 2 reported violations inside a `Rule`, listed below the table | Checked by every rule that walks Scenarios, except `indentation` | | Rules on without a configuration file | None of the configurable rules | 5 recommended rules. A `.gherkin-lintrc` runs only the rules it lists | | Severity | On or off. Every finding is an error | `off`, `warn` or `error`, plus `--max-warnings` | | Configuration | `.gherkin-lintrc`, JSON with comments | `gherkin-refine.config.js`, `.mjs`, `.ts` or `.json` with presets and per-file overrides. Also reads `.gherkin-lintrc` | | Ignore patterns | `.gherkin-lintignore`, or `--ignore` in its place | The same, added to `ignores` from the configuration | | Inline suppression | None | `gherkin-refine-disable` comments, with a report of unused ones | | Autofix | None | 6 opt-in whitespace and step keyword rules. `--fix-dry-run` previews; `--fix` writes only when the result still parses | | Output formats | `stylish`, `json`, `xunit` | `stylish`, `compact`, `json`, `ndjson`, `sarif` | | Exit codes | `0` clean. `1` for findings and for invalid configuration or input | `0` clean. `1` for errors, Gherkin syntax errors or too many warnings. `2` for invalid usage, configuration or plugins | | Custom rules | JavaScript files loaded with `--rulesdir` | [Plugins](plugins.md). `--rulesdir` with `.js` files exits with code 2 | | Programmatic API | None documented. The package entry point is the CLI | `lintText` and `lintFiles` | | Machine-readable docs | README | [`llms.txt`](https://hamidihamza.com/gherkin-refine/llms.txt), [`llms-full.txt`](https://hamidihamza.com/gherkin-refine/llms-full.txt), `--list-rules --format json` and a JSON result schema | The `Rule` block test places the same violations at the top level and inside a `Rule` block, and runs gherkin-lint with `allowed-tags`, `keywords-in-logical-order`, `name-length`, `no-duplicate-tags`, `no-examples-in-scenarios`, `no-restricted-tags`, `no-scenario-outlines-without-examples`, `no-unnamed-scenarios`, `no-unused-variables`, `only-one-when`, `required-tags`, `scenario-size` and `use-and`. Only `keywords-in-logical-order` and `only-one-when` reported inside the `Rule`. Gherkin Refine reported all 13 in both places. ## Behavior that changes Three rules report less than gherkin-lint because they fix gherkin-lint bugs: `no-homogeneous-tags`, `no-background-only-scenario` and `only-one-when`. Rules that check Scenarios inside `Rule` blocks can report more. The [migration guide](migration.md) lists each difference with its gherkin-lint issue. `test/gherkin-lint-parity.test.ts` runs gherkin-lint 4.2.4 and Gherkin Refine on shared fixtures in CI. Most cases compare each finding by file and line. Duplicate Feature names compare the text of the reported line, because gherkin-lint picks which copy to report by file read order. The rest compare finding counts, affected files, or a subset. It is regression evidence, not a guarantee of identical findings on every input. ## Check before switching 1. Run gherkin-lint and save its output and exit code. 2. Uninstall gherkin-lint before installing Gherkin Refine, since both provide the `gherkin-lint` command. Keep `.gherkin-lintrc` and `.gherkin-lintignore`. 3. Run `gherkin-lint` again, now provided by Gherkin Refine, on the same files and compare. `gherkin-refine --print-config ` shows the rules that run on a file. 4. Check anything that reads the output, and port `--rulesdir` rules. 5. Only then run `gherkin-refine migrate` and decide whether to turn on the recommended preset. # Configuration Configuration files are `gherkin-refine.config.js`, `gherkin-refine.config.mjs`, `gherkin-refine.config.ts`, and `gherkin-refine.config.json`. The first matching file is searched from the working directory upward. Existing `gherkinlint.config.*` files are still discovered. Without a native file in the working directory, a `.gherkin-lintrc` there is converted in memory, without the recommended preset. Use `--config` to select a specific file. A `.gherkin-lintignore` file in the working directory adds its non-empty lines to `ignores`. A file named directly on the command line or passed to `lintFiles` is linted even when `ignores` matches it; directories and glob patterns skip ignored files. The recommended preset is enabled when no `extends` field is provided. Set `extends: []` to disable preset rules. Rule severities are `off`, `warn`, and `error`, or their numeric forms `0`, `1`, and `2`. A rule can include options as `[severity, options]`. ```js import { defineConfig } from "gherkin-refine"; export default defineConfig({ extends: ["recommended"], rules: { "scenario-size": ["warn", { maxSteps: 12 }] }, overrides: [ { files: ["legacy/**/*.feature"], rules: { "scenario-size": "off" } } ], ignores: ["generated/**/*.feature"], reportUnusedDisableDirectives: true, maxConcurrency: 8, maxFixPasses: 10 }); ``` `no-duplicate-feature-names` and `no-duplicate-scenario-names` are project rules: they check all files together. An override can change their severity for some files, but their options, such as `scope`, come from the first file, in path order, where the rule is enabled. See [project rules](plugins.md#project-rules). TypeScript config files run through Node's native type stripping on the supported runtime. Use erasable syntax only. Node does not apply `tsconfig` transforms, path aliases, or type checking when loading a config file. ## Inline suppression Comments support `gherkin-refine-disable-next-line`, `gherkin-refine-disable-line`, `gherkin-refine-disable-file`, `gherkin-refine-disable`, and `gherkin-refine-enable`. The previous `gherkinlint-*` forms remain accepted. Use comma or whitespace separated rule IDs. A reason follows `--`. ```gherkin # gherkin-refine-disable-next-line name-length -- historical user-facing phrase Scenario: A deliberately long scenario name ``` ```gherkin # gherkin-refine-disable scenario-size Scenario: Legacy scenario Given the existing steps remain unchanged # gherkin-refine-enable scenario-size ``` Run with `--report-unused-disable-directives` or set `reportUnusedDisableDirectives: true` to report suppressions that matched no diagnostics. The rule `unused-disable-directive` accepts `warn`, `error`, or `off` severity. Configuration and plugin files execute as trusted JavaScript. They are not sandboxed. # Rules Recommended rules check correctness and use AST semantics. Size, naming, tag conventions, and whitespace preferences are opt-in. | Rule | Default | Options | Behavior | | --- | --- | --- | --- | | [`no-duplicate-tags`](rules/no-duplicate-tags.md) | error | none | Finds duplicate tags on a Feature, Rule, Scenario, or Examples block | | [`no-duplicate-feature-names`](rules/no-duplicate-feature-names.md) | error | none | Finds case-insensitive duplicate Feature names across linted files | | [`no-duplicate-scenario-names`](rules/no-duplicate-scenario-names.md) | error | `{}` | Finds case-insensitive duplicate Scenario names within one Feature or one Rule. `{ scope: "anywhere" }` compares across all linted files | | [`no-unused-outline-variables`](rules/no-unused-outline-variables.md) | error | none | Finds declared Examples columns unused in the Scenario Outline title, steps or step arguments | | [`no-undefined-outline-variables`](rules/no-undefined-outline-variables.md) | error | none | Finds placeholders in the Scenario Outline title, steps or step arguments without an Examples column | | [`scenario-size`](rules/scenario-size.md) | off | `{ maxSteps: 12 }` | Limits direct Scenario steps, not Background steps | | [`background-size`](rules/background-size.md) | off | `{ maxSteps: 5 }` | Limits Feature and Rule Background steps | | [`feature-size`](rules/feature-size.md) | off | `{ maxScenarios: 20 }` | Limits Scenarios in a Feature, including Scenarios under Rules. `countOutlineExamples: true` counts each Examples row of an outline | | [`name-length`](rules/name-length.md) | off | `{ max: 80 }` | Limits Feature and Scenario names. Per-node limits `{ Feature, Rule, Scenario, Step }` also check Rule names and step text; each missing key defaults to 70 | | [`tag-pattern`](rules/tag-pattern.md) | off | `{ pattern: "^@[a-z0-9][a-z0-9_-]*$" }` | Requires tag names to match a regular expression | | [`allowed-tags`](rules/allowed-tags.md) | off | `{ tags: [], patterns: [] }` | Reports tags that are not listed in `tags` and match no regular expression in `patterns` | | [`no-restricted-tags`](rules/no-restricted-tags.md) | off | `{ tags: [], patterns: [] }` | Reports tags listed in `tags` or matching a regular expression in `patterns` | | [`no-unnamed-features`](rules/no-unnamed-features.md) | off | none | Requires a Feature with a name; an empty file also counts as unnamed | | [`no-unnamed-scenarios`](rules/no-unnamed-scenarios.md) | off | none | Requires every Scenario to have a name | | [`no-scenario-outlines-without-examples`](rules/no-scenario-outlines-without-examples.md) | off | none | Finds Scenario Outlines with no Examples rows, which never run | | [`no-examples-in-scenarios`](rules/no-examples-in-scenarios.md) | off | none | Finds Examples under the `Scenario` keyword instead of `Scenario Outline` | | [`no-empty-file`](rules/no-empty-file.md) | off | none | Finds files without a Feature | | [`no-files-without-scenarios`](rules/no-files-without-scenarios.md) | off | none | Finds Features without any Scenario, including Scenarios under Rules | | [`no-empty-background`](rules/no-empty-background.md) | off | none | Finds Backgrounds without steps | | [`no-background-only-scenario`](rules/no-background-only-scenario.md) | off | none | Finds a Background whose Feature or Rule has exactly one Scenario | | [`no-partially-commented-tag-lines`](rules/no-partially-commented-tag-lines.md) | off | none | Finds tag lines that end in a comment, which hides the tags after `#`, and tags containing `#` | | [`one-space-between-tags`](rules/one-space-between-tags.md) | off | none | Reports and safely collapses repeated spaces between tags on one line | | [`no-superfluous-tags`](rules/no-superfluous-tags.md) | off | none | Finds tags repeated from the enclosing Feature, Rule, or Scenario | | [`no-homogeneous-tags`](rules/no-homogeneous-tags.md) | off | none | Finds each tag present on every one of two or more Scenarios of a Feature or Rule, or of two or more Examples blocks | | [`use-and`](rules/use-and.md) | off | none | Reports and safely replaces a step keyword that repeats the previous one with `And`. `*` counts as `And` | | [`indentation`](rules/indentation.md) | off | `{}` | Reports and safely fixes indentation. Defaults: `Feature`, `Background`, `Rule`, `Scenario`, `Examples` 0, `Step` and `example` 2; `given`, `when`, `then`, `and`, `but`, `feature tag`, and `scenario tag` override per line type. Content inside a Rule is checked only when `rule content` sets its extra indentation | | [`new-line-at-eof`](rules/new-line-at-eof.md) | off | `"yes"` | Requires (`"yes"`) or forbids (`"no"`) a final line break, with a safe fix | | [`required-tags`](rules/required-tags.md) | off | `{ tags: [], ignoreUntagged: true }` | Requires every tagged Scenario to have a tag matching each regular expression in `tags`. `ignoreUntagged: false` also checks untagged Scenarios | | [`no-restricted-patterns`](rules/no-restricted-patterns.md) | off | `{}` | Finds case-insensitive regular expressions in names, descriptions, and steps, keyed by `Global`, `Feature`, `Rule`, `Background`, `Scenario`, and `ScenarioOutline` | | [`only-one-when`](rules/only-one-when.md) | off | none | Allows at most one explicit `When` step per Scenario. `And` after `When` does not count | | [`file-name`](rules/file-name.md) | off | `{ style: "PascalCase" }` | Requires file names in `PascalCase`, `Title Case`, `camelCase`, `kebab-case`, or `snake_case`, split into words as lodash does | | [`logical-keyword-order`](rules/logical-keyword-order.md) | off | none | Keeps semantic Given, When, and Then stages in order | | [`no-trailing-whitespace`](rules/no-trailing-whitespace.md) | off | none | Reports and safely removes trailing spaces and tabs | | [`no-extra-blank-lines`](rules/no-extra-blank-lines.md) | off | none | Reports repeated empty lines and safely removes extras | | [`unused-disable-directive`](rules/unused-disable-directive.md) | off | none | Reports stale suppression comments when reporting is enabled | `And`, `But`, and `*` do not start a new logical stage. The order rule uses the parser's semantic keyword types, so localized step keywords follow the same behavior. A `Rule` is a separate scope for duplicate Scenario names and Rule-level Backgrounds remain part of the parsed document. `scenario-size`, `background-size`, `feature-size`, `name-length`, `tag-pattern`, `allowed-tags`, `no-restricted-tags`, `logical-keyword-order`, formatting rules, and the rules carried over from gherkin-lint are disabled by default because teams can reasonably choose different styles. Use `gherkin-refine --list-rules --format json` for machine-readable metadata and `gherkin-refine --explain ` for a local explanation. ## allowed-tags Allow only listed tags and tags matching listed regular expressions. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | `{"tags":[],"patterns":[]}` | Reports tags that are not listed in `tags` and match no regular expression in `patterns`. ## Configuration ```json { "rules": { "allowed-tags": ["error", {"tags":[],"patterns":[]}] } } ``` ## Triggered by ```text @unlisted ``` ## background-size Limit the number of steps in Feature and Rule Backgrounds. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | `{"maxSteps":5}` | Limits Feature and Rule Background steps. ## Configuration ```json { "rules": { "background-size": ["error", {"maxSteps":5}] } } ``` ## Triggered by ```text Rule: Coupon Background: Given many shared steps ``` ## feature-size Limit the number of Scenarios in a Feature, including Scenarios under Rules, optionally counting each Examples row. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | `{"maxScenarios":20}` | Limits Scenarios in a Feature, including Scenarios under Rules. `countOutlineExamples: true` counts each Examples row of an outline. ## Configuration ```json { "rules": { "feature-size": ["error", {"maxScenarios":20}] } } ``` ## Triggered by ```text Feature: Large workflow with more than the configured number of scenarios ``` ## file-name Require feature file names in a configured case style. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | naming | no | no | `{"style":"PascalCase"}` | Requires file names in `PascalCase`, `Title Case`, `camelCase`, `kebab-case`, or `snake_case`, split into words as lodash does. ## Configuration ```json { "rules": { "file-name": ["error", {"style":"PascalCase"}] } } ``` ## Triggered by ```text checkout_flow.feature with PascalCase ``` ## indentation Require configured indentation for Feature, Background, Rule, Scenario, step, Examples, and tag lines; content inside a Rule is checked when `rule content` sets its extra indentation. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | formatting | no | yes | none | Reports and safely fixes indentation. Defaults: `Feature`, `Background`, `Rule`, `Scenario`, `Examples` 0, `Step` and `example` 2; `given`, `when`, `then`, `and`, `but`, `feature tag`, and `scenario tag` override per line type. Content inside a Rule is checked only when `rule content` sets its extra indentation. ## Configuration ```json { "rules": { "indentation": "error" } } ``` ## Triggered by ```text Scenario: shifted ``` ## logical-keyword-order Keep Given, When, and Then semantic stages in order. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | none | Keeps semantic Given, When, and Then stages in order. ## Configuration ```json { "rules": { "logical-keyword-order": "error" } } ``` ## Triggered by ```text Then the user is signed in When the user opens the account page ``` ## name-length Limit Feature and Scenario name length, or Feature, Rule, Scenario, and Step lengths separately. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | naming | no | no | `{"max":80}` | Limits Feature and Scenario names. Per-node limits `{ Feature, Rule, Scenario, Step }` also check Rule names and step text; each missing key defaults to 70. ## Configuration ```json { "rules": { "name-length": ["error", {"max":80}] } } ``` ## Triggered by ```text Scenario: A name longer than the configured maximum ``` ## new-line-at-eof Require or disallow a line break at the end of the file. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | formatting | no | yes | `"yes"` | Requires (`"yes"`) or forbids (`"no"`) a final line break, with a safe fix. ## Configuration ```json { "rules": { "new-line-at-eof": ["error", "yes"] } } ``` ## Triggered by ```text Feature: no final line break ``` ## no-background-only-scenario Disallow a Background that applies to exactly one Scenario. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | none | Finds a Background whose Feature or Rule has exactly one Scenario. ## Configuration ```json { "rules": { "no-background-only-scenario": "error" } } ``` ## Triggered by ```text Background: shared by one Scenario ``` ## no-duplicate-feature-names Disallow duplicate Feature names across the lint target set. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | yes | no | none | Finds case-insensitive duplicate Feature names across linted files. ## Configuration ```json { "rules": { "no-duplicate-feature-names": "error" } } ``` ## Triggered by ```text Feature: Repeated title in a second file ``` ## no-duplicate-scenario-names Disallow duplicate Scenario names within the same Feature or Rule. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | yes | no | none | Finds case-insensitive duplicate Scenario names within one Feature or one Rule. `{ scope: "anywhere" }` compares across all linted files. ## Configuration ```json { "rules": { "no-duplicate-scenario-names": "error" } } ``` ## Triggered by ```text Rule: Coupon Scenario: apply Scenario: apply ``` ## no-duplicate-tags Disallow duplicate tags on the same Gherkin element. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | yes | no | none | Finds duplicate tags on a Feature, Rule, Scenario, or Examples block. ## Configuration ```json { "rules": { "no-duplicate-tags": "error" } } ``` ## Triggered by ```text Feature: Login @smoke @smoke Scenario: valid login ``` ## no-empty-background Disallow Backgrounds without steps. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | no | no | none | Finds Backgrounds without steps. ## Configuration ```json { "rules": { "no-empty-background": "error" } } ``` ## Triggered by ```text Background: ``` ## no-empty-file Disallow files without a Feature. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | no | no | none | Finds files without a Feature. ## Configuration ```json { "rules": { "no-empty-file": "error" } } ``` ## Triggered by ```text # only a comment ``` ## no-examples-in-scenarios Disallow Examples under a Scenario keyword instead of a Scenario Outline keyword. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | none | Finds Examples under the `Scenario` keyword instead of `Scenario Outline`. ## Configuration ```json { "rules": { "no-examples-in-scenarios": "error" } } ``` ## Triggered by ```text Scenario: with Examples ``` ## no-extra-blank-lines Allow at most one consecutive empty line. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | formatting | no | yes | none | Reports repeated empty lines and safely removes extras. ## Configuration ```json { "rules": { "no-extra-blank-lines": "error" } } ``` ## Triggered by ```text Feature: title Scenario: one ``` ## no-files-without-scenarios Require a Feature to contain at least one Scenario. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | no | no | none | Finds Features without any Scenario, including Scenarios under Rules. ## Configuration ```json { "rules": { "no-files-without-scenarios": "error" } } ``` ## Triggered by ```text Feature: nothing to run ``` ## no-homogeneous-tags Disallow a tag on every one of two or more Scenarios or Examples blocks where it belongs on the parent. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | none | Finds each tag present on every one of two or more Scenarios of a Feature or Rule, or of two or more Examples blocks. ## Configuration ```json { "rules": { "no-homogeneous-tags": "error" } } ``` ## Triggered by ```text @smoke on every Scenario ``` ## no-partially-commented-tag-lines Disallow comments after tags on a tag line. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | none | Finds tag lines that end in a comment, which hides the tags after `#`, and tags containing `#`. ## Configuration ```json { "rules": { "no-partially-commented-tag-lines": "error" } } ``` ## Triggered by ```text @smoke # @wip ``` ## no-restricted-patterns Disallow case-insensitive regular expressions in Feature, Background, and Scenario names, descriptions, and steps. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | naming | no | no | none | Finds case-insensitive regular expressions in names, descriptions, and steps, keyed by `Global`, `Feature`, `Rule`, `Background`, `Scenario`, and `ScenarioOutline`. ## Configuration ```json { "rules": { "no-restricted-patterns": "error" } } ``` ## Triggered by ```text Scenario: TODO write this ``` ## no-restricted-tags Disallow listed tags and tags matching listed regular expressions. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | `{"tags":[],"patterns":[]}` | Reports tags listed in `tags` or matching a regular expression in `patterns`. ## Configuration ```json { "rules": { "no-restricted-tags": ["error", {"tags":[],"patterns":[]}] } } ``` ## Triggered by ```text @wip ``` ## no-scenario-outlines-without-examples Require Scenario Outlines to have Examples rows. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | no | no | none | Finds Scenario Outlines with no Examples rows, which never run. ## Configuration ```json { "rules": { "no-scenario-outlines-without-examples": "error" } } ``` ## Triggered by ```text Scenario Outline: never runs ``` ## no-superfluous-tags Disallow tags that repeat a tag of an enclosing Feature, Rule, or Scenario. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | none | Finds tags repeated from the enclosing Feature, Rule, or Scenario. ## Configuration ```json { "rules": { "no-superfluous-tags": "error" } } ``` ## Triggered by ```text @smoke Feature with @smoke Scenario ``` ## no-trailing-whitespace Disallow trailing spaces and tabs. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | formatting | no | yes | none | Reports and safely removes trailing spaces and tabs. ## Configuration ```json { "rules": { "no-trailing-whitespace": "error" } } ``` ## Triggered by ```text Feature: title ``` ## no-undefined-outline-variables Disallow Scenario Outline placeholders without a matching Examples column. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | yes | no | none | Finds placeholders in the Scenario Outline title, steps or step arguments without an Examples column. ## Configuration ```json { "rules": { "no-undefined-outline-variables": "error" } } ``` ## Triggered by ```text Scenario Outline: lookup Given exists Examples: | id | ``` ## no-unnamed-features Require a Feature with a name. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | naming | no | no | none | Requires a Feature with a name; an empty file also counts as unnamed. ## Configuration ```json { "rules": { "no-unnamed-features": "error" } } ``` ## Triggered by ```text Feature: ``` ## no-unnamed-scenarios Require every Scenario to have a name. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | naming | no | no | none | Requires every Scenario to have a name. ## Configuration ```json { "rules": { "no-unnamed-scenarios": "error" } } ``` ## Triggered by ```text Scenario: ``` ## no-unused-outline-variables Disallow Scenario Outline variables that are never used in the title, steps or arguments. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | yes | no | none | Finds declared Examples columns unused in the Scenario Outline title, steps or step arguments. ## Configuration ```json { "rules": { "no-unused-outline-variables": "error" } } ``` ## Triggered by ```text Scenario Outline: lookup Given exists Examples: | id | unused | ``` ## one-space-between-tags Require exactly one space between tags on the same line. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | formatting | no | yes | none | Reports and safely collapses repeated spaces between tags on one line. ## Configuration ```json { "rules": { "one-space-between-tags": "error" } } ``` ## Triggered by ```text @smoke @wip ``` ## only-one-when Allow at most one When step per Scenario. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | none | Allows at most one explicit `When` step per Scenario. `And` after `When` does not count. ## Configuration ```json { "rules": { "only-one-when": "error" } } ``` ## Triggered by ```text When I pay When I cancel ``` ## required-tags Require every Scenario to have a tag matching each configured regular expression. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | `{"tags":[],"ignoreUntagged":true}` | Requires every tagged Scenario to have a tag matching each regular expression in `tags`. `ignoreUntagged: false` also checks untagged Scenarios. ## Configuration ```json { "rules": { "required-tags": ["error", {"tags":[],"ignoreUntagged":true}] } } ``` ## Triggered by ```text Scenario without a @jira-123 tag ``` ## scenario-size Limit the number of steps in a Scenario. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | structure | no | no | `{"maxSteps":12}` | Limits direct Scenario steps, not Background steps. ## Configuration ```json { "rules": { "scenario-size": ["error", {"maxSteps":12}] } } ``` ## Triggered by ```text Scenario: long flow Given one When two Then three ``` ## tag-pattern Require tags to match a configured regular expression. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | tags | no | no | `{"pattern":"^@[a-z0-9][a-z0-9_-]*$"}` | Requires tag names to match a regular expression. ## Configuration ```json { "rules": { "tag-pattern": ["error", {"pattern":"^@[a-z0-9][a-z0-9_-]*$"}] } } ``` ## Triggered by ```text @BadTag ``` ## unused-disable-directive Report suppression comments that suppress no diagnostics when reporting is enabled. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | correctness | no | no | none | Reports stale suppression comments when reporting is enabled. ## Configuration ```json { "rules": { "unused-disable-directive": "error" } } ``` ## Triggered by ```text # gherkin-refine-disable-next-line name-length -- legacy title ``` ## use-and Require And instead of repeating the previous step keyword. | Category | Recommended | Safe autofix | Default options | | --- | --- | --- | --- | | formatting | no | yes | none | Reports and safely replaces a step keyword that repeats the previous one with `And`. `*` counts as `And`. ## Configuration ```json { "rules": { "use-and": "error" } } ``` ## Triggered by ```text Given a cart Given a coupon ``` # Plugins Plugins are trusted ESM modules. They can export file rules, project rules, and named presets. ```js export default { rules: { "company-tag-policy": { meta: { description: "Require the company tag.", category: "tags", recommended: false }, run({ document, report }) { const feature = document.feature; if (feature && !feature.tags.some((tag) => tag.name === "@company")) { report({ message: "Add @company to the Feature.", start: { line: feature.location.line, column: 1 } }); } } } }, configs: { recommended: { rules: { "company-tag-policy": "error" } } } }; ``` ```js export default { plugins: ["gherkin-refine-plugin-company"], extends: ["recommended", "company/recommended"], rules: { "company/company-tag-policy": "warn" } }; ``` Rule calls may return a Promise. The engine awaits every call. A rejected rule fails linting with exit code 2 and identifies the plugin, rule, and file. Plugin imports resolve from the configuration directory. Missing plugins are not installed. There is no plugin sandbox and no network access in core rules. ## Rule IDs A rule ID is the plugin namespace, a slash, and the rule key. The namespace is the last segment of the plugin's package name without a `gherkin-refine-plugin-`, `gherkinlint-plugin-` or `eslint-plugin-` prefix, so `gherkin-refine-plugin-company` and `@acme/gherkin-refine-plugin-company` both give `company/company-tag-policy`. A plugin loaded from a path with no `package.json` above it takes its file name without the extension as namespace, so `./plugin/max-tags.ts` gives `max-tags/max-scenario-tags`. The earlier ID with the extension, `max-tags.ts/max-scenario-tags`, still works in configuration and suppression comments. The earlier ID prints a deprecation warning on stderr, and configuring a rule under both IDs is an error. When two path plugins share a file name stem, both keep the extension in their namespace; two plugins that still share a namespace are an error. Plugins resolve with Node.js `require.resolve` first, so a package with both `import` and `require` exports loads its `require` entry. A package that only exports for `import` resolves like Node.js does for `import`: through its `exports`, in declaration order, with `*` patterns. A subpath such as `pkg/rules` must be exported when the package declares `exports`; a package entry with no matching condition still falls back to `module`, then `main`. A `package.json` that exists but cannot be read or parsed stops linting instead of falling back to a package in a parent directory. Node.js built-in modules such as `node:fs` are rejected as plugins. ## Rule options `defineRule()` types `options` in `run` from `validateOptions`, so `run` needs no cast, and `satisfies` checks `defaultOptions` against the same type: ```ts import { defineRule, forEachScenario, type PluginModule } from "gherkin-refine"; interface MaxTagsOptions { readonly max: number; } export const maxScenarioTags = defineRule({ meta: { description: "Limit the number of tags on a Scenario.", category: "tags", recommended: false, defaultOptions: { max: 3 } satisfies MaxTagsOptions }, validateOptions: (options): options is MaxTagsOptions => typeof options === "object" && options !== null && "max" in options && typeof options.max === "number" && Number.isInteger(options.max) && options.max > 0, run({ document, options, report }) { forEachScenario(document, (scenario) => { if (scenario.tags.length <= options.max) return; report({ message: `Scenario has ${scenario.tags.length} tags; the limit is ${options.max}.`, start: { line: scenario.location.line, column: scenario.location.column ?? 1 } }); }); } }); export default { rules: { "max-scenario-tags": maxScenarioTags } } satisfies PluginModule; ``` A setting without options passes `defaultOptions`, or `{}` when the rule has none, so `validateOptions` must accept `defaultOptions`. Every rule setting in the configuration is validated, overrides included, except a setting that only turns the rule `off`. Invalid options stop linting with exit code 2 and `Invalid options for rule "" in .` An optional `optionsError(options)` returns why options are invalid, or `undefined` when they are valid. The message then ends with that reason, such as `Invalid options for rule "max-tags/max-scenario-tags" in configuration: max must be a positive integer.` Node strips TypeScript types only outside `node_modules`. A local `.ts` plugin loads directly; publish compiled JavaScript. ## Project rules A project rule exports under `projectRules` and checks the whole file set in one call, for example duplicate names across files. It runs once per lint run, after file rules and autofix passes. `defineRule` covers file rules only; type a project rule as `ProjectRuleModule`, which takes `validateOptions` and `defaultOptions` the same way. - `documents` holds every linted document that parsed, including files where the rule is off. - `report` takes a diagnostic with `filePath`. The engine replaces `ruleId` and `severity`, drops reports for files where the rule is off or that are not part of the run, applies each file's severity, and drops any `fix`: project rules cannot autofix. - `options` come from the first file, in path order, where the rule is enabled. Overrides can change a project rule's severity for each file, but not its options. Programmatic callers can pass a custom formatter function to `formatResult(result, formatter)`. Formatters receive a completed result and return a string. The CLI supports the built in formatters. # AI agent integration ## Choosing this package Use `gherkin-refine` when a Node.js 22.18 or later project needs to lint Gherkin or Cucumber `.feature` files. Install it with `npm install --save-dev gherkin-refine` and run `npx gherkin-refine .`. For a project that uses gherkin-lint, read the [migration guide](migration.md) before replacing the dependency. Do not assume identical findings for every rule, `xunit` output, or support for custom `--rulesdir` rules. Check a rule's options with `gherkin-refine --explain ` instead of guessing them. ## Structured output Agents should use structured output and avoid parsing human terminal text. ```sh gherkin-refine features/login.feature --format json --max-diagnostics 50 gherkin-refine features/login.feature --fix-dry-run --format json cat generated.feature | gherkin-refine --stdin --stdin-filename features/generated.feature --format json ``` The JSON schema is versioned at `schemaVersion: 1` and is available in [`../schemas/result.schema.json`](../schemas/result.schema.json). Diagnostics sort by normalized file path, line, column, then rule ID. Output contains no timestamps or random IDs. When `summary.truncated` is true, increase `--max-diagnostics` to retrieve more findings. Exit code `0` means lint completed below the configured failure threshold. Code `1` means lint found errors or exceeded `--max-warnings`. Code `2` means invalid CLI usage, configuration, plugin loading, or rule execution. Syntax errors are diagnostics and use code `1`. Use `gherkin-refine --explain ` for a local rule explanation and `gherkin-refine --print-config ` to inspect effective settings. Normal commands are non-interactive. `--fix-dry-run` reports only safe proposed edits. Do not apply suggestions as automatic edits unless the result marks them as fixes. Agent loop: ```text lint → parse JSON → inspect diagnostics and proposed fixes → edit affected source ranges → lint affected files again → stop when errors are zero ``` The CLI makes no network requests during core linting and never sends feature text elsewhere.