gurkencheck
A linter for Gherkin feature files. It reads your .feature files and tells you
where they drift from the conventions your team has agreed on.
Install
npm install --save-dev gurkencheck
Get started
Run it. With no configuration file, gurkencheck uses its recommended rules: the ones that catch a mistake rather than express a preference — an empty file, a scenario with no name, a variable that will never be substituted. Nothing in that set depends on how you lay a file out, so it should be quiet on a codebase that has never been linted.
npx gurkencheck
When you want something different, create a file called
.gurkencheckrc and list the rules you want. A configuration file
replaces the recommended set rather than adding to it, so every rule is off until you switch
it on.
A rule is set to "on", "warn" or "off".
"warn" reports exactly the same findings but does not fail the run, which is
what you want for a rule the team is working towards rather than enforcing.
{
"no-unnamed-features": "on",
"no-unnamed-scenarios": "on",
"no-trailing-spaces": "on",
"indentation": ["on", {"Feature": 0, "Scenario": 2, "Step": 4}]
}
With no paths given, it searches the current directory for .feature files.
It exits with 0 when there is nothing worse than a warning, 1 when
a rule set to "on" was broken, and 2 when it could not run at all.
Feature files in another language
Gherkin is translated into dozens of languages. A file says which one it is written in with a header on its first line:
# language: fr
Fonctionnalité: Se déconnecter
Scénario: Se déconnecter
Quand Ulrick se déconnecte
If every file in your project is written in the same language, set it once instead, with
--language fr or a language key in your configuration file. A header
in a file always wins over that setting, so a project can be mostly one language with
exceptions.
Sharing a configuration
Build on top of another configuration with extends. What your file says wins
over what it extends, and later entries in a list win over earlier ones.
{
"extends": "gurkencheck:recommended",
"indentation": ["on", {"Step": 4}],
"no-trailing-spaces": "off"
}
An entry is one of three things:
| Entry | What it means |
|---|---|
gurkencheck:recommended | The built-in recommended rules. |
./team/.gurkencheckrc | Another file, resolved from the file doing the extending. |
@acme/gurkencheck-config | An installed package exporting a configuration, as JSON or as a module. |
Command line options
| Option | What it does |
|---|---|
-f, --format | Output format: stylish (the default), json, junit, sarif or tap, or the path to a formatter of your own. |
-c, --config | Path to a configuration file, if it is not .gurkencheckrc in the current directory. |
-i, --ignore | Comma separated globs to skip. Overrides .gurkencheckignore. |
-r, --rulesdir | A directory holding your own rules. May be given more than once. |
-l, --language | The dialect to read files in when they carry no # language: header. |
-h, --help | Show the options. |
-v, --version | Show the version number. |
Reading the output
Findings go to stdout, so gurkencheck > report.json and
gurkencheck | less work. Anything that stops the linter running — a bad
option, an invalid configuration — goes to stderr, so it never lands in a redirected
report.
Each finding is printed as line:column message rule.
Both numbers start at 1, so an editor can underline exactly the right text. A finding about a
whole file or a whole line — a missing new line at the end of the file, say — shows
the line on its own. The json format carries the same line and
column fields, in the same shape eslint's JSON formatter uses, so tools built
around that already understand it.
Skipping files
Put one glob per line in a .gurkencheckignore file, or pass
--ignore on the command line. Without either, node_modules is skipped
and everything else is checked.
A pattern that matches a directory skips everything below it, the same way
.gitignore and .eslintignore work, so build is enough and
you do not have to write build/**. Blank lines and lines starting with
# are ignored.
Reporting to GitHub code scanning
--format sarif writes a SARIF 2.1.0 log, which GitHub reads directly. Upload
it and each finding is shown inline on the pull request that introduced it.
- run: npx gurkencheck --format sarif > gurkencheck.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gurkencheck.sarif
Paths in the log are relative to the directory gurkencheck ran in, which is what code scanning needs in order to match a finding to a file in the repository.
Switching a rule off for one place
Ignoring a whole file is often too blunt: one long step name should not cost you every other check in that file. Write a comment in the feature file instead.
# gurkencheck-disable-next-line name-length
Scenario: A name that is long for a good reason and stays that way
# gurkencheck-disable use-and, name-length
... everything below here skips those two rules ...
# gurkencheck-enable use-and
# gurkencheck-disable-file no-trailing-spaces
| Directive | What it covers |
|---|---|
gurkencheck-disable-next-line | The line directly below the comment. |
gurkencheck-disable | From the comment to the end of the file, or to the next gurkencheck-enable. |
gurkencheck-enable | Resumes the rules a gurkencheck-disable switched off. |
gurkencheck-disable-file | The whole file, wherever the comment appears. |
Name the rules you mean, separated by commas or spaces. A directive naming no rules covers all of them. Comments inside a doc string are text and are left alone.
The four always-on rules cannot be switched off this way. A file that breaks one of them cannot be read at all, so hiding the message would leave nothing in its place.
Rules you switch on
These are all off by default. Each page shows an example that passes and one that fails.
-
allowed-tagsOnly lets you use tags from a list you decide on.
-
file-nameKeeps every feature file named in the same style.
-
indentationChecks how far each line is indented.
-
keywords-in-logical-orderKeeps Given, When and Then in that order.
-
max-scenarios-per-fileLimits how many scenarios one file may hold.
-
name-lengthStops names and steps from growing too long.
-
new-line-at-eofRequires, or forbids, an empty line at the end of the file.
-
no-background-only-scenarioDrops a Background that only one scenario uses.
-
no-dupe-feature-namesMakes sure no two features share a name.
-
no-dupe-file-namesNo two feature files may share a name.
-
no-dupe-scenario-namesMakes sure no two scenarios share a name.
-
no-duplicate-tagsStops the same tag being written twice in one place.
-
no-empty-backgroundRemoves a Background with no steps in it.
-
no-empty-fileFlags feature files with nothing in them.
-
no-examples-in-scenariosReminds you that Examples belong to a Scenario Outline.
-
no-files-without-scenariosFlags a feature file that never tests anything.
-
no-homogenous-tagsMoves a tag up when every scenario carries it.
-
no-multiple-empty-linesLimits how many blank lines may sit next to each other.
-
no-partially-commented-tag-linesCatches a "#" part way along a line of tags.
-
no-restricted-patternsBans wording you do not want in your feature files.
-
no-restricted-tagsBans tags you do not want committed.
-
no-scenario-outlines-without-examplesFlags a Scenario Outline with no rows to run.
-
no-superfluous-tagsRemoves a tag repeated from the level above.
-
no-trailing-spacesRemoves spaces and tabs left at the end of a line.
-
no-unnamed-featuresEvery Feature needs a name.
-
no-unnamed-scenariosEvery Scenario needs a name.
-
no-unused-variablesEvery column of an Examples table should be used by a step.
-
no-undeclared-variablesEvery <placeholder> in a step needs a column to fill it in.
-
one-space-between-tagsOne space between tags on the same line.
-
only-one-whenOne action per scenario.
-
required-tagsRequires each scenario to carry certain tags.
-
scenario-sizeLimits how many steps a scenario or background may have.
-
use-andUse And instead of repeating a keyword.
Always on
These four are not really settings. They describe things Gherkin itself refuses to read, so a file that breaks one of them cannot be checked at all.
-
no-tags-on-backgroundsA Background cannot have tags.
-
one-feature-per-fileOne Feature per file.
-
up-to-one-background-per-fileAt most one Background per file.
-
background-before-scenariosA Background has to come before the Scenarios it applies to.
-
no-multiline-stepsA step has to fit on one line.
Writing your own formatter
Pass a path or a package name to --format. The module exports a function
taking the results; it may print the output itself, or return it as a string and let
gurkencheck print it.
// count.mjs
export default function count(results) {
const findings = results.reduce((total, file) => total + file.errors.length, 0);
return `${findings} findings in ${results.length} files`;
}
npx gurkencheck --format ./count.mjs
Each result is {filePath, errors}, and each error is
{message, rule, line, column, severity}. A default export, a
printResults export, or a module that is itself the function all work.
Writing your own rule
Point --rulesdir at a directory of your own modules. Each one exports an object
with a name and a run function, and gets called once per file.
// rules/no-lorem.js
const name = 'no-lorem';
export default {
name,
run(feature, file) {
if (feature === undefined) return [];
return feature.children
.filter((child) => child.scenario?.name.includes('lorem'))
.map((child) => ({
message: 'Placeholder text left in a scenario name',
rule: name,
line: child.scenario.location.line,
}));
},
};
run may also be async and return a promise, for a rule that has
to wait for something — reading a file, or asking an issue tracker whether a tag
refers to a real ticket. Files are checked one after another, so rules see a predictable
order.
Then switch it on by name, the same as any built-in rule:
npx gurkencheck --rulesdir ./rules