Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gherkin-lint-plus

A Gherkin linter for .feature files, implemented in TypeScript and built on @cucumber/gherkin. It aims to be a modern, maintainable take on the same idea as the original gherkin-lint CLI.

Requirements: Node.js >=20.

Install

npm install gherkin-lint-plus

From a git checkout (development):

cd gherkin-lint-plus
npm install
npm run build

npm run build runs the unit tests first, then compiles to dist/.

Usage

You need a config file in the current working directory unless you pass -c (see below).

npx gherkin-lint-plus [options] [files...]
  • No arguments: lints all **/*.feature under ., respecting ignore rules.
  • Paths: files, directories, or globs; directories are expanded to **/*.feature under them.

CLI options

Option Description
-c, --config [path] Config file (default: .gherkin-lintrc in cwd)
-f, --format [format] Output format; only stylish is supported today
-i, --ignore <items> Comma-separated ignore globs (overrides .gherkin-lintignore if present)
-r, --rulesdir <dir> Extra directory of custom rules (repeatable); see Custom rules

Exit code 1 if any file has lint errors.

Configuration file

The default name is .gherkin-lintrc in your working directory. Contents are JSON with optional comments (JSONC). Use -c / --config if the file lives elsewhere.

Each key is a rule name. Allowed values:

Form Meaning
"on" Rule enabled with default options (or no extra options).
"off" Rule disabled (not allowed for mandatory rules).
["on", <options>] Rule enabled; <options> is an object for most rules, or a string where documented (e.g. new-line-at-eof, no-dupe-scenario-names).
["off", <options>] Rule disabled (same restriction for mandatory rules).

Optional rules are off if omitted from the config.

Mandatory rules always run. You may omit them or set "on" / ["on", …]; you must not set them to "off" or ["off", …] (the CLI reports a configuration error).

Example .gherkin-lintrc

{
  "indentation": "on",
  "no-trailing-spaces": "on",
  "file-name": ["on", { "style": "kebab-case" }],
  "new-line-at-eof": ["on", "yes"]
}

Ignoring feature files

  1. Add .gherkin-lintignore in the working directory: one glob pattern per line.
  2. Or use -i / --ignore with comma-separated globs (this overrides the ignore file when provided).

If there is no ignore file and you did not pass -i, node_modules/** is ignored by default.

Built-in rules summary

Rule name Category In .gherkin-lintrc? Enable example
no-tags-on-backgrounds Parse No — fix the .feature file
one-feature-per-file Parse No
up-to-one-background-per-file Parse No
no-multiline-steps Parse No
unexpected-error Parse No (other parse failures)
no-empty-file Mandatory Yes (cannot turn off) Omit, or "no-empty-file": "on"
no-files-without-scenarios Mandatory Yes (cannot turn off) Omit, or "on"
no-unnamed-features Mandatory Yes (cannot turn off) Omit, or "on"
no-unnamed-scenarios Mandatory Yes (cannot turn off) Omit, or "on"
allowed-tags Optional Yes "allowed-tags": "on" + options (see below)
file-name Optional Yes "file-name": ["on", { "style": "PascalCase" }]
indentation Optional Yes "indentation": "on" or ["on", { … }]
keywords-in-logical-order Optional Yes "keywords-in-logical-order": "on"
max-scenarios-per-file Optional Yes "max-scenarios-per-file": ["on", { … }]
name-length Optional Yes "name-length": ["on", { … }]
new-line-at-eof Optional Yes "new-line-at-eof": ["on", "yes"]
no-background-only-scenario Optional Yes "no-background-only-scenario": "on"
no-dupe-feature-names Optional Yes "no-dupe-feature-names": "on"
no-dupe-scenario-names Optional Yes "on" or ["on", "in-feature"]
no-duplicate-tags Optional Yes "no-duplicate-tags": "on"
no-empty-background Optional Yes "no-empty-background": "on"
no-examples-in-scenarios Optional Yes "no-examples-in-scenarios": "on"
no-homogenous-tags Optional Yes "no-homogenous-tags": "on"
no-multiple-empty-lines Optional Yes "no-multiple-empty-lines": "on"
no-partially-commented-tag-lines Optional Yes "no-partially-commented-tag-lines": "on"
no-restricted-patterns Optional Yes ["on", { … }]
no-restricted-tags Optional Yes ["on", { "tags": [], "patterns": [] }]
no-scenario-outlines-without-examples Optional Yes "no-scenario-outlines-without-examples": "on"
no-superfluous-tags Optional Yes "no-superfluous-tags": "on"
no-trailing-spaces Optional Yes "no-trailing-spaces": "on"
no-unused-variables Optional Yes "no-unused-variables": "on"
one-space-between-tags Optional Yes "one-space-between-tags": "on"
only-one-when Optional Yes "only-one-when": "on"
required-tags Optional Yes ["on", { … }]
scenario-size Optional Yes ["on", { "steps-length": { … } }]
use-and Optional Yes "use-and": "on"

Parse rules are emitted when the Gherkin parser rejects the file. They are not toggled in config; correct the source. Mandatory rules always run and cannot be disabled.

Backgrounds: you may have one optional Background under Feature, and one optional Background inside each Rule:. up-to-one-background-per-file fires when a second Background appears in the same scope (duplicate at feature level or duplicate inside one rule).


Rule reference

Parse-time rules (not configurable)

These appear as rule IDs on parse failures. Fix the .feature text; do not add them to .gherkin-lintrc.

Rule What it checks
no-tags-on-backgrounds Tags are not allowed on Background.
one-feature-per-file Only one Feature: per file.
up-to-one-background-per-file At most one Background in each feature or rule block.
no-multiline-steps Step text must not continue as a random continuation line (use And, Doc Strings, or tables).
unexpected-error Other parser errors, surfaced with the raw message.

Mandatory rules

Always evaluated. Use "on" or omit the key; never "off".

no-empty-file

What: Whitespace-only or missing feature content is not allowed.

Example:

{
  "no-empty-file": "on"
}

no-files-without-scenarios

What: The file must contain at least one scenario (including under Rule:).

Example:

{
  "no-files-without-scenarios": "on"
}

no-unnamed-features

What: Feature: must have a name.

Example:

{
  "no-unnamed-features": "on"
}

no-unnamed-scenarios

What: Scenario: / Scenario Outline: must have a name.

Example:

{
  "no-unnamed-scenarios": "on"
}

Optional rules

Turn on with "rule-name": "on" or "off" to disable. Use ["on", …] when the rule needs options (below).

allowed-tags

What: Only listed tags and patterns are allowed on tag-bearing nodes.

Enable / off:

{
  "allowed-tags": [
    "on",
    { "tags": ["@watch", "@wip"], "patterns": ["^@todo$"] }
  ]
}
{ "allowed-tags": "off" }

file-name

What: Basename of *.feature must match a naming style (default PascalCase).

Styles: PascalCase, Title Case, camelCase, kebab-case, snake_case

{
  "file-name": ["on", { "style": "kebab-case" }]
}

indentation

What: Checks column positions for keywords, tags, steps, and example tables. Defaults: feature-level keywords at 0; steps and table rows at 2 spaces. Override per keyword type; localized Gherkin keywords are supported.

{
  "indentation": [
    "on",
    {
      "Feature": 0,
      "Background": 0,
      "Rule": 0,
      "Scenario": 0,
      "Step": 2,
      "Examples": 0,
      "example": 2,
      "given": 2,
      "when": 2,
      "then": 2,
      "and": 2,
      "but": 2,
      "feature tag": 0,
      "scenario tag": 0
    }
  ]
}

keywords-in-logical-order

What: Given / When / Then order should follow a sensible flow.

{ "keywords-in-logical-order": "on" }

max-scenarios-per-file

What: Caps how many “scenarios” count per file (plain scenarios count as 1; outlines can count each example row).

Options: maxScenarios (default 10), countOutlineExamples (default true)

{
  "max-scenarios-per-file": [
    "on",
    { "maxScenarios": 10, "countOutlineExamples": true }
  ]
}

name-length

What: Max length for Feature, Rule, Scenario, and step text (default 70 each).

{
  "name-length": [
    "on",
    { "Feature": 70, "Rule": 70, "Scenario": 70, "Step": 70 }
  ]
}

new-line-at-eof

What: Require or forbid a newline at end of file. Must use a string option: "yes" or "no".

{ "new-line-at-eof": ["on", "yes"] }
{ "new-line-at-eof": ["on", "no"] }

no-background-only-scenario

What: If there is only one scenario, a Background is not allowed (feature level or inside a Rule).

{ "no-background-only-scenario": "on" }

no-dupe-feature-names

What: The same feature name must not appear in more than one linted file (cross-file state).

{ "no-dupe-feature-names": "on" }

no-dupe-scenario-names

What: Duplicate scenario names. String option: "anywhere" (default, cross-file) or "in-feature" (per feature only).

{ "no-dupe-scenario-names": "on" }
{ "no-dupe-scenario-names": ["on", "in-feature"] }

no-duplicate-tags

What: No duplicate tags on the same feature, rule, or scenario.

{ "no-duplicate-tags": "on" }

no-empty-background

What: Background must contain at least one step.

{ "no-empty-background": "on" }

no-examples-in-scenarios

What: Examples belongs on scenario outlines, not plain scenarios.

{ "no-examples-in-scenarios": "on" }

no-homogenous-tags

What: If every scenario shares the same tag(s), prefer moving those tags to the feature (or avoid redundant tagging).

{ "no-homogenous-tags": "on" }

no-multiple-empty-lines

What: No runs of multiple blank lines.

{ "no-multiple-empty-lines": "on" }

no-partially-commented-tag-lines

What: Tag lines must not be partially commented in a misleading way.

{ "no-partially-commented-tag-lines": "on" }

no-restricted-patterns

What: Ban regex patterns (case-insensitive) in names, descriptions, and step text. Keys: Global, Feature, Rule, Background, Scenario, ScenarioOutline. Match step text only (not the Given/When keyword).

{
  "no-restricted-patterns": [
    "on",
    {
      "Global": ["^globally restricted"],
      "Feature": ["poor description"],
      "Background": [],
      "Scenario": [],
      "Rule": [],
      "ScenarioOutline": []
    }
  ]
}

no-restricted-tags

What: Disallow specific @tags and patterns.

{
  "no-restricted-tags": [
    "on",
    { "tags": ["@watch"], "patterns": ["^@todo$"] }
  ]
}

no-scenario-outlines-without-examples

What: Every scenario outline must have Examples.

{ "no-scenario-outlines-without-examples": "on" }

no-superfluous-tags

What: Tags repeated on both feature and scenarios inside that feature are flagged.

{ "no-superfluous-tags": "on" }

no-trailing-spaces

What: No trailing spaces on lines.

{ "no-trailing-spaces": "on" }

no-unused-variables

What: Scenario outline placeholders must match Examples headers (no unused columns, no missing columns).

{ "no-unused-variables": "on" }

one-space-between-tags

What: Multiple tags on one line must be separated by a single space.

{ "one-space-between-tags": "on" }

only-one-when

What: At most one When step per scenario.

{ "only-one-when": "on" }

required-tags

What: Scenarios must match tag regexes. Options: tags (regex strings), ignoreUntagged (default true — untagged scenarios skipped).

{
  "required-tags": [
    "on",
    { "tags": ["^@issue:[1-9]\\d*$"], "ignoreUntagged": false }
  ]
}

scenario-size

What: Max steps per Background (feature level), Scenario (feature-level scenarios/outlines), and everything under a Rule (uses the Rule limit).

{
  "scenario-size": [
    "on",
    {
      "steps-length": {
        "Rule": 15,
        "Background": 15,
        "Scenario": 15
      }
    }
  ]
}

use-and

What: Repeated same step keyword (Given, When, …) should use And / But instead.

{ "use-and": "on" }

Custom rules

Use -r / --rulesdir one or more times. Each path must be a directory (absolute or relative to cwd). The linter loads every *.ts file in that directory (not subfolders). Files are compiled at runtime with jiti; JavaScript rule files are not supported.

Each module must default-export a rule object:

import type { Feature } from '@cucumber/messages';

const name = 'my-rule';

export default {
  name,
  run(
    feature: Feature | undefined,
    file: { relativePath: string; lines: string[] },
    config: unknown
  ) {
    return [{ message: '…', rule: name, line: 1 }];
  },
  availableConfigs: [] as const,
};
  • name — must match the key in .gherkin-lintrc.
  • run — returns { message, rule, line }[], or undefined / [] if OK. config is the second part of ["on", …] when present, else {}.
  • availableConfigs[] if only "on"/"off"; otherwise a string array (allowed string options) or an object whose keys are allowed option names.

If two files define the same name, the one loaded last wins.

Add @cucumber/messages (and optionally typescript) to your project for typing. See built-in rules under src/rules/ and the rulesdir fixture.

Development

Script Purpose
npm test Run Vitest once
npm run test:watch Vitest watch mode
npm run test:coverage Tests + coverage (HTML & LCOV under coverage/)
npm run build Tests, then tsc
npm start Run node dist/main.js (after build)

Limitations (current)

  • Output: only the stylish formatter; json / xunit are not implemented.

License

ISC

About

Gherkin lint

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages