Custom Rules

July 2, 2026 · View on GitHub

How to write and register custom rules for behave-lint.

Rule base class

Custom rules inherit from behave_lint.rules.base.Rule and implement a check method that receives a behave_model feature node and a Config object, returning a list of Diagnostic objects.

from behave_lint.models.config import Config
from behave_lint.models.diagnostic import Diagnostic
from behave_lint.models.enums import Category, Severity
from behave_lint.models.rule_metadata import RuleMetadata, RuleExample
from behave_lint.rules.base import Rule


class NoGivenInThenRule(Rule):
    """MY001: Detect 'Given' steps appearing after 'Then' steps."""

    metadata = RuleMetadata(
        rule_id="MY001",
        name="no-given-in-then",
        title="Given steps should not appear after Then steps",
        description=(
            "Detects Given steps that appear after a Then step, "
            "which breaks the Given-When-Then convention."
        ),
        category=Category.STYLE,
        default_severity=Severity.WARNING,
        motivation="Given steps after Then steps are confusing.",
        since="1.0.0",
        examples=[
            RuleExample(
                before=(
                    "  Scenario: Test\n"
                    "    Given a user\n"
                    "    Then I see results\n"
                    "    Given another user\n"
                ),
                after=(
                    "  Scenario: Test\n"
                    "    Given a user\n"
                    "    And another user\n"
                    "    Then I see results\n"
                ),
                description="Move Given steps before Then.",
            ),
        ],
        tags=["steps", "ordering", "custom"],
    )

    def check(self, feature, config: Config) -> list[Diagnostic]:
        diagnostics: list[Diagnostic] = []
        for scenario in feature.all_scenarios():
            seen_then = False
            for step in getattr(scenario, "steps", []):
                keyword = getattr(step, "keyword", "").strip().lower()
                if keyword == "then":
                    seen_then = True
                if seen_then and keyword == "given":
                    diagnostics.append(
                        self.diagnostic(
                            message=(
                                f"Given step '{step.name}' appears "
                                "after a Then step"
                            ),
                            node=step,
                            suggestion="Move Given steps before Then.",
                        )
                    )
        return diagnostics

Registration

Register rules via the behave_lint.rules entry point in your pyproject.toml:

[project.entry-points."behave_lint.rules"]
no-given-in-then = "my_package.rules:NoGivenInThenRule"

Auto-fix support

To add auto-fix support, implement the get_fixes method:

from behave_lint.autofix.models import FixEdit
from behave_lint.models.enums import AutoFixCapability


def get_fixes(self, feature, config, diagnostics):
    # Return list[FixEdit] for safe or unsafe fixes
    ...

See the Auto-Fix guide for details.

Rule base class API

The Rule base class provides:

Attribute/MethodTypeDescription
metadataRuleMetadataRule identity and docs (required).
scopeRuleScopeSINGLE_FILE (default) or CROSS_FILE.
default_paramsdict[str, Any]Default configurable parameters.
check(feature, config)list[Diagnostic]Analyze and return diagnostics (required).
get_fixes(feature, config, diagnostics)list[FixEdit]Return auto-fix edits (optional).
diagnostic(message, node, ...)DiagnosticCreate a diagnostic with metadata pre-filled.
rule_idstrProperty — rule ID from metadata.
categoryCategoryProperty — category from metadata.
default_severitySeverityProperty — default severity from metadata.

diagnostic() parameters

ParameterTypeDescription
messagestrWhat is wrong (factual statement).
nodeHasLocation | NoneA behave-model element with file_path and line.
lineint | NoneExplicit line number (overrides node).
columnint | NoneExplicit column number.
file_pathstr | NoneExplicit file path (overrides node).
end_lineint | NoneEnd line for multi-line diagnostics.
end_columnint | NoneEnd column for multi-line diagnostics.
suggestionstr | NoneHow to fix it (actionable guidance).
doc_urlstr | NoneURL to rule documentation.
severitySeverity | NoneOverride severity (defaults to rule's default).

RuleMetadata fields

FieldTypeRequiredDescription
rule_idstrYesStable, unique identifier (e.g., "BC001").
namestrYesShort, human-readable, kebab-case name.
titlestrYesOne-line summary for CLI and docs.
descriptionstrYesOne-paragraph description of what the rule checks.
categoryCategoryYesRule category enum.
default_severitySeverityYesDefault severity when enabled.
motivationstrYesWhy the rule exists — the problem it solves.
sincestrYesVersion when the rule was introduced.
exampleslist[RuleExample]NoBefore/after examples for documentation.
auto_fixAutoFixCapabilityNoAuto-fix capability. Default: NONE.
tagslist[str]NoFree-form tags for filtering and grouping.
referenceslist[str]NoExternal references (URLs, standards).
configurableboolNoWhether the rule accepts parameters. Default: False.
experimentalboolNoWhether the rule is experimental. Default: False.
deprecatedboolNoWhether the rule is deprecated. Default: False.
deprecated_versionstr | NoneNoVersion in which deprecated.
replaced_bystr | NoneNoRule ID that replaces this one.
aliaseslist[str]NoAlternative names for backward compatibility.
dependencieslist[str]NoRule IDs that must execute before this rule.
conflictslist[str]NoRule IDs that conflict with this rule.
doc_urlstr | NoneNoURL to rule documentation.
authorstr | NoneNoAuthor or maintainer.
min_versionstr | NoneNoMinimum behave-lint version required.
estimated_fix_costFixCostNoEstimated effort to fix. Default: LOW.
performance_impactPerformanceImpactNoExecution cost. Default: NEGLIGIBLE.
educational_valueEducationalValueNoPedagogical value. Default: NONE.

RuleScope

ValueDescription
SINGLE_FILERule analyzes one feature file at a time. Parallelized across (rule, file) pairs.
CROSS_FILERule analyzes the entire project at once. Executed sequentially after all single-file rules.

FixEdit fields

FieldTypeDescription
file_pathstrPath to the file to modify.
start_lineint1-based start line of the region to replace.
end_lineint1-based end line (inclusive).
old_textstrOriginal text being replaced (for validation).
new_textstrReplacement text.
safetyAutoFixCapabilitySAFE or UNSAFE.
rule_idstrThe rule that produced this fix.
diagnostic_lineintThe diagnostic line number that triggered this fix.

Testing

Write unit tests that:

  1. Create a .feature file with the violation.
  2. Load it with load_features.
  3. Run rule.check(feature, config).
  4. Assert the expected diagnostics.

See the existing rule tests in tests/unit/behave_lint/rules/ for examples.