Skip to content

API Reference

Auto-generated API documentation from source code docstrings.

behave_lint

behave-lint — A fast, opinionated, extensible linter for Gherkin .feature files.

This package provides static analysis for Behave BDD projects, consuming behave-model as its single source of truth.

Public API is defined in API.md. Only names explicitly exported here are considered stable and publicly supported.

AutoFixCapability

Bases: Enum

Declares a rule's auto-fix support.

Members

NONE: Not fixable. SAFE: Fix does not change semantics. UNSAFE: Fix may change semantics.

Source code in behave_lint/models/enums.py
class AutoFixCapability(Enum):
    """Declares a rule's auto-fix support.

    Members:
        NONE: Not fixable.
        SAFE: Fix does not change semantics.
        UNSAFE: Fix may change semantics.
    """

    NONE = "none"
    SAFE = "safe"
    UNSAFE = "unsafe"

Category

Bases: Enum

Groups rules by concern (what kind of problem).

Members

CORRECTNESS: Definitively wrong structures. STYLE: Stylistic conventions. COMPLEXITY: Overly complex specifications. CONSISTENCY: Cross-file consistency. PEDANTIC: Strict best practices (opt-in). STEP_DEFINITIONS: Cross-reference with step defs.

Source code in behave_lint/models/enums.py
class Category(Enum):
    """Groups rules by concern (what kind of problem).

    Members:
        CORRECTNESS: Definitively wrong structures.
        STYLE: Stylistic conventions.
        COMPLEXITY: Overly complex specifications.
        CONSISTENCY: Cross-file consistency.
        PEDANTIC: Strict best practices (opt-in).
        STEP_DEFINITIONS: Cross-reference with step defs.
    """

    CORRECTNESS = "correctness"
    STYLE = "style"
    COMPLEXITY = "complexity"
    CONSISTENCY = "consistency"
    PEDANTIC = "pedantic"
    STEP_DEFINITIONS = "step_definitions"

    @property
    def code(self) -> str:
        """Single-letter category code used in rule IDs.

        Returns:
            The category code: C, S, X, K, P, or D.
        """
        return _CATEGORY_CODES[self]

    @property
    def default_severity(self) -> Severity:
        """Default severity for rules in this category.

        Returns:
            The default Severity for the category.
        """
        return _CATEGORY_DEFAULT_SEVERITY[self]

    @classmethod
    def from_string(cls, value: str) -> Category:
        """Parse a string into a Category member.

        Args:
            value: Case-insensitive category string.

        Returns:
            The matching Category member.

        Raises:
            ValueError: If the string does not match any member.
        """
        normalized = value.strip().lower()
        for member in cls:
            if member.value == normalized:
                return member
        valid = ", ".join(m.value for m in cls)
        raise ValueError(f"Invalid category '{value}'. Expected one of: {valid}.")

    @classmethod
    def from_code(cls, code: str) -> Category:
        """Parse a single-letter code into a Category member.

        Args:
            code: Single uppercase letter (C, S, X, K, P, D).

        Returns:
            The matching Category member.

        Raises:
            ValueError: If the code does not match any member.
        """
        normalized = code.strip().upper()
        for member in cls:
            if member.code == normalized:
                return member
        valid = ", ".join(m.code for m in cls)
        raise ValueError(f"Invalid category code '{code}'. Expected one of: {valid}.")

code property

Single-letter category code used in rule IDs.

Returns:

Type Description
str

The category code: C, S, X, K, P, or D.

default_severity property

Default severity for rules in this category.

Returns:

Type Description
Severity

The default Severity for the category.

from_code(code) classmethod

Parse a single-letter code into a Category member.

Parameters:

Name Type Description Default
code str

Single uppercase letter (C, S, X, K, P, D).

required

Returns:

Type Description
Category

The matching Category member.

Raises:

Type Description
ValueError

If the code does not match any member.

Source code in behave_lint/models/enums.py
@classmethod
def from_code(cls, code: str) -> Category:
    """Parse a single-letter code into a Category member.

    Args:
        code: Single uppercase letter (C, S, X, K, P, D).

    Returns:
        The matching Category member.

    Raises:
        ValueError: If the code does not match any member.
    """
    normalized = code.strip().upper()
    for member in cls:
        if member.code == normalized:
            return member
    valid = ", ".join(m.code for m in cls)
    raise ValueError(f"Invalid category code '{code}'. Expected one of: {valid}.")

from_string(value) classmethod

Parse a string into a Category member.

Parameters:

Name Type Description Default
value str

Case-insensitive category string.

required

Returns:

Type Description
Category

The matching Category member.

Raises:

Type Description
ValueError

If the string does not match any member.

Source code in behave_lint/models/enums.py
@classmethod
def from_string(cls, value: str) -> Category:
    """Parse a string into a Category member.

    Args:
        value: Case-insensitive category string.

    Returns:
        The matching Category member.

    Raises:
        ValueError: If the string does not match any member.
    """
    normalized = value.strip().lower()
    for member in cls:
        if member.value == normalized:
            return member
    valid = ", ".join(m.value for m in cls)
    raise ValueError(f"Invalid category '{value}'. Expected one of: {valid}.")

Config dataclass

Resolved configuration for a lint run.

Attributes:

Name Type Description
select list[str]

Rule IDs to enable (empty = all defaults).

ignore list[str]

Rule IDs to disable.

profile str

Profile name applied to this config (e.g. "recommended").

group list[str]

Group names applied to this config (e.g. ["correctness", "style"]).

severity_overrides dict[str, Severity]

Per-rule severity overrides.

output str

Output format(s), comma-separated.

output_file str | None

Output file path (None = stdout).

paths list[str]

Default paths to lint.

exclude list[str]

Paths to exclude from linting.

step_definitions str | None

Step definitions directory, or None.

cache bool

Whether to enable caching.

cache_dir str

Cache directory path.

plugins dict[str, bool]

Plugin enable/disable map.

rule_params dict[str, dict[str, object]]

Per-rule parameters.

fail_on Severity

Minimum severity that causes non-zero exit.

max_warnings int

Max warnings before exit code is non-zero (-1 = no limit).

Source code in behave_lint/models/config.py
@dataclass(frozen=True, slots=True)
class Config:
    """Resolved configuration for a lint run.

    Attributes:
        select: Rule IDs to enable (empty = all defaults).
        ignore: Rule IDs to disable.
        profile: Profile name applied to this config (e.g. "recommended").
        group: Group names applied to this config (e.g. ["correctness", "style"]).
        severity_overrides: Per-rule severity overrides.
        output: Output format(s), comma-separated.
        output_file: Output file path (None = stdout).
        paths: Default paths to lint.
        exclude: Paths to exclude from linting.
        step_definitions: Step definitions directory, or None.
        cache: Whether to enable caching.
        cache_dir: Cache directory path.
        plugins: Plugin enable/disable map.
        rule_params: Per-rule parameters.
        fail_on: Minimum severity that causes non-zero exit.
        max_warnings: Max warnings before exit code is non-zero (-1 = no limit).
    """

    select: list[str] = field(default_factory=list)
    ignore: list[str] = field(default_factory=list)
    profile: str = "none"
    group: list[str] = field(default_factory=list)
    severity_overrides: dict[str, Severity] = field(default_factory=dict)
    output: str = "console"
    output_file: str | None = None
    paths: list[str] = field(default_factory=lambda: ["features/"])
    exclude: list[str] = field(default_factory=list)
    step_definitions: str | None = None
    cache: bool = True
    cache_dir: str = ".behave-lint-cache"
    plugins: dict[str, bool] = field(default_factory=dict)
    rule_params: dict[str, dict[str, object]] = field(default_factory=dict)
    fail_on: Severity = Severity.WARNING
    max_warnings: int = -1

    def is_rule_enabled(self, rule_id: str) -> bool:
        """Check if a rule is enabled given select/ignore lists.

        Args:
            rule_id: The rule ID to check.

        Returns:
            True if the rule is enabled, False if explicitly ignored.
            If select is non-empty, only selected rules are enabled.
        """
        if rule_id in self.ignore:
            return False
        if self.select:
            return rule_id in self.select
        return True

    def get_severity(self, rule_id: str, default: Severity) -> Severity:
        """Get the effective severity for a rule.

        Args:
            rule_id: The rule ID.
            default: Default severity if no override exists.

        Returns:
            The severity from overrides, or the default.
        """
        return self.severity_overrides.get(rule_id, default)

get_severity(rule_id, default)

Get the effective severity for a rule.

Parameters:

Name Type Description Default
rule_id str

The rule ID.

required
default Severity

Default severity if no override exists.

required

Returns:

Type Description
Severity

The severity from overrides, or the default.

Source code in behave_lint/models/config.py
def get_severity(self, rule_id: str, default: Severity) -> Severity:
    """Get the effective severity for a rule.

    Args:
        rule_id: The rule ID.
        default: Default severity if no override exists.

    Returns:
        The severity from overrides, or the default.
    """
    return self.severity_overrides.get(rule_id, default)

is_rule_enabled(rule_id)

Check if a rule is enabled given select/ignore lists.

Parameters:

Name Type Description Default
rule_id str

The rule ID to check.

required

Returns:

Type Description
bool

True if the rule is enabled, False if explicitly ignored.

bool

If select is non-empty, only selected rules are enabled.

Source code in behave_lint/models/config.py
def is_rule_enabled(self, rule_id: str) -> bool:
    """Check if a rule is enabled given select/ignore lists.

    Args:
        rule_id: The rule ID to check.

    Returns:
        True if the rule is enabled, False if explicitly ignored.
        If select is non-empty, only selected rules are enabled.
    """
    if rule_id in self.ignore:
        return False
    if self.select:
        return rule_id in self.select
    return True

Diagnostic dataclass

A single issue found by a rule.

Attributes:

Name Type Description
rule_id str

Stable rule identifier (e.g., "BC001").

severity Severity

Severity level (effective, after user overrides).

message str

Human-readable description of the issue.

file_path str

Path to the .feature file.

line int

1-based line number where the issue starts.

column int | None

1-based column number, or None.

end_line int | None

End line for multi-line issues, or None.

end_column int | None

End column for multi-line issues, or None.

suggestion str | None

Human-readable fix suggestion, or None.

doc_url str | None

URL to rule documentation, or None.

category Category

Rule category.

Source code in behave_lint/models/diagnostic.py
@dataclass(frozen=True, slots=True)
class Diagnostic:
    """A single issue found by a rule.

    Attributes:
        rule_id: Stable rule identifier (e.g., "BC001").
        severity: Severity level (effective, after user overrides).
        message: Human-readable description of the issue.
        file_path: Path to the .feature file.
        line: 1-based line number where the issue starts.
        column: 1-based column number, or None.
        end_line: End line for multi-line issues, or None.
        end_column: End column for multi-line issues, or None.
        suggestion: Human-readable fix suggestion, or None.
        doc_url: URL to rule documentation, or None.
        category: Rule category.
    """

    rule_id: str
    severity: Severity
    message: str
    file_path: str
    line: int
    category: Category
    column: int | None = None
    end_line: int | None = None
    end_column: int | None = None
    suggestion: str | None = None
    doc_url: str | None = None

    def __post_init__(self) -> None:
        """Validate field constraints after initialization."""
        if self.line < 1:
            raise ValueError(f"line must be >= 1, got {self.line}")
        if self.column is not None and self.column < 1:
            raise ValueError(f"column must be >= 1, got {self.column}")
        if self.end_line is not None and self.end_line < self.line:
            raise ValueError(
                f"end_line ({self.end_line}) must be >= line ({self.line})"
            )
        if self.end_column is not None and self.column is None:
            raise ValueError("end_column requires column to be set")

    @property
    def location(self) -> str:
        """Formatted location string for display.

        Returns:
            A string like "features/login.feature:15:3" or
            "features/login.feature:15".
        """
        if self.column is not None:
            return f"{self.file_path}:{self.line}:{self.column}"
        return f"{self.file_path}:{self.line}"

    @property
    def is_error(self) -> bool:
        """Whether this diagnostic has ERROR severity.

        Returns:
            True if severity is ERROR, False otherwise.
        """
        return self.severity is Severity.ERROR

    @property
    def is_warning(self) -> bool:
        """Whether this diagnostic has WARNING severity.

        Returns:
            True if severity is WARNING, False otherwise.
        """
        return self.severity is Severity.WARNING

    @property
    def is_info(self) -> bool:
        """Whether this diagnostic has INFO severity.

        Returns:
            True if severity is INFO, False otherwise.
        """
        return self.severity is Severity.INFO

is_error property

Whether this diagnostic has ERROR severity.

Returns:

Type Description
bool

True if severity is ERROR, False otherwise.

is_info property

Whether this diagnostic has INFO severity.

Returns:

Type Description
bool

True if severity is INFO, False otherwise.

is_warning property

Whether this diagnostic has WARNING severity.

Returns:

Type Description
bool

True if severity is WARNING, False otherwise.

location property

Formatted location string for display.

Returns:

Type Description
str

A string like "features/login.feature:15:3" or

str

"features/login.feature:15".

__post_init__()

Validate field constraints after initialization.

Source code in behave_lint/models/diagnostic.py
def __post_init__(self) -> None:
    """Validate field constraints after initialization."""
    if self.line < 1:
        raise ValueError(f"line must be >= 1, got {self.line}")
    if self.column is not None and self.column < 1:
        raise ValueError(f"column must be >= 1, got {self.column}")
    if self.end_line is not None and self.end_line < self.line:
        raise ValueError(
            f"end_line ({self.end_line}) must be >= line ({self.line})"
        )
    if self.end_column is not None and self.column is None:
        raise ValueError("end_column requires column to be set")

FixCost

Bases: Enum

Estimated effort to fix a diagnostic.

Members

TRIVIAL: Minimal effort (e.g., whitespace). LOW: Quick fix (e.g., rename). MEDIUM: Requires some thought (e.g., restructure scenario). HIGH: Significant effort (e.g., rewrite feature file).

Source code in behave_lint/models/enums.py
class FixCost(Enum):
    """Estimated effort to fix a diagnostic.

    Members:
        TRIVIAL: Minimal effort (e.g., whitespace).
        LOW: Quick fix (e.g., rename).
        MEDIUM: Requires some thought (e.g., restructure scenario).
        HIGH: Significant effort (e.g., rewrite feature file).
    """

    TRIVIAL = "trivial"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

LintResult dataclass

Result of a lint run.

Attributes:

Name Type Description
diagnostics list[Diagnostic]

All diagnostics, sorted by file/line/rule.

summary LintSummary

Execution summary.

exit_code int

Exit code (0, 1, or 2).

fixes list[FixEdit]

Auto-fix edits collected during the run (if any).

Source code in behave_lint/models/lint_result.py
@dataclass(frozen=True, slots=True)
class LintResult:
    """Result of a lint run.

    Attributes:
        diagnostics: All diagnostics, sorted by file/line/rule.
        summary: Execution summary.
        exit_code: Exit code (0, 1, or 2).
        fixes: Auto-fix edits collected during the run (if any).
    """

    diagnostics: list[Diagnostic] = field(default_factory=list)
    summary: LintSummary = field(default_factory=LintSummary)
    exit_code: int = 0
    fixes: list[FixEdit] = field(default_factory=list)

    @property
    def has_errors(self) -> bool:
        """Whether any diagnostic has ERROR severity.

        Returns:
            True if at least one error diagnostic exists.
        """
        return self.summary.error_count > 0

    @property
    def has_diagnostics(self) -> bool:
        """Whether any diagnostics were produced.

        Returns:
            True if the diagnostics list is non-empty.
        """
        return self.summary.total_diagnostics > 0

    @property
    def passed(self) -> bool:
        """Whether the lint run passed (exit code 0).

        Returns:
            True if exit_code is 0.
        """
        return self.exit_code == 0

has_diagnostics property

Whether any diagnostics were produced.

Returns:

Type Description
bool

True if the diagnostics list is non-empty.

has_errors property

Whether any diagnostic has ERROR severity.

Returns:

Type Description
bool

True if at least one error diagnostic exists.

passed property

Whether the lint run passed (exit code 0).

Returns:

Type Description
bool

True if exit_code is 0.

LintSummary dataclass

Execution summary for a lint run.

Attributes:

Name Type Description
total_files int

Number of files analyzed.

files_with_issues int

Number of files with at least one diagnostic.

total_diagnostics int

Total diagnostics produced.

error_count int

Diagnostics with severity ERROR.

warning_count int

Diagnostics with severity WARNING.

info_count int

Diagnostics with severity INFO.

rules_executed int

Number of rules that ran.

duration_ms float

Execution time in milliseconds.

cache_hits int

Number of cache hits.

cache_misses int

Number of cache misses.

Source code in behave_lint/models/lint_result.py
@dataclass(frozen=True, slots=True)
class LintSummary:
    """Execution summary for a lint run.

    Attributes:
        total_files: Number of files analyzed.
        files_with_issues: Number of files with at least one diagnostic.
        total_diagnostics: Total diagnostics produced.
        error_count: Diagnostics with severity ERROR.
        warning_count: Diagnostics with severity WARNING.
        info_count: Diagnostics with severity INFO.
        rules_executed: Number of rules that ran.
        duration_ms: Execution time in milliseconds.
        cache_hits: Number of cache hits.
        cache_misses: Number of cache misses.
    """

    total_files: int = 0
    files_with_issues: int = 0
    total_diagnostics: int = 0
    error_count: int = 0
    warning_count: int = 0
    info_count: int = 0
    rules_executed: int = 0
    duration_ms: float = 0.0
    cache_hits: int = 0
    cache_misses: int = 0

    @classmethod
    def from_diagnostics(
        cls,
        diagnostics: list[Diagnostic],
        *,
        total_files: int = 0,
        files_with_issues: int = 0,
        rules_executed: int = 0,
        duration_ms: float = 0.0,
        cache_hits: int = 0,
        cache_misses: int = 0,
    ) -> LintSummary:
        """Build a summary from a list of diagnostics.

        Args:
            diagnostics: The diagnostics produced by the run.
            total_files: Number of files analyzed.
            files_with_issues: Number of files with issues.
            rules_executed: Number of rules that ran.
            duration_ms: Execution time in milliseconds.
            cache_hits: Number of cache hits.
            cache_misses: Number of cache misses.

        Returns:
            A LintSummary with counts derived from the diagnostics.
        """
        error_count = sum(1 for d in diagnostics if d.is_error)
        warning_count = sum(1 for d in diagnostics if d.is_warning)
        info_count = sum(1 for d in diagnostics if d.is_info)
        return cls(
            total_files=total_files,
            files_with_issues=files_with_issues,
            total_diagnostics=len(diagnostics),
            error_count=error_count,
            warning_count=warning_count,
            info_count=info_count,
            rules_executed=rules_executed,
            duration_ms=duration_ms,
            cache_hits=cache_hits,
            cache_misses=cache_misses,
        )

from_diagnostics(diagnostics, *, total_files=0, files_with_issues=0, rules_executed=0, duration_ms=0.0, cache_hits=0, cache_misses=0) classmethod

Build a summary from a list of diagnostics.

Parameters:

Name Type Description Default
diagnostics list[Diagnostic]

The diagnostics produced by the run.

required
total_files int

Number of files analyzed.

0
files_with_issues int

Number of files with issues.

0
rules_executed int

Number of rules that ran.

0
duration_ms float

Execution time in milliseconds.

0.0
cache_hits int

Number of cache hits.

0
cache_misses int

Number of cache misses.

0

Returns:

Type Description
LintSummary

A LintSummary with counts derived from the diagnostics.

Source code in behave_lint/models/lint_result.py
@classmethod
def from_diagnostics(
    cls,
    diagnostics: list[Diagnostic],
    *,
    total_files: int = 0,
    files_with_issues: int = 0,
    rules_executed: int = 0,
    duration_ms: float = 0.0,
    cache_hits: int = 0,
    cache_misses: int = 0,
) -> LintSummary:
    """Build a summary from a list of diagnostics.

    Args:
        diagnostics: The diagnostics produced by the run.
        total_files: Number of files analyzed.
        files_with_issues: Number of files with issues.
        rules_executed: Number of rules that ran.
        duration_ms: Execution time in milliseconds.
        cache_hits: Number of cache hits.
        cache_misses: Number of cache misses.

    Returns:
        A LintSummary with counts derived from the diagnostics.
    """
    error_count = sum(1 for d in diagnostics if d.is_error)
    warning_count = sum(1 for d in diagnostics if d.is_warning)
    info_count = sum(1 for d in diagnostics if d.is_info)
    return cls(
        total_files=total_files,
        files_with_issues=files_with_issues,
        total_diagnostics=len(diagnostics),
        error_count=error_count,
        warning_count=warning_count,
        info_count=info_count,
        rules_executed=rules_executed,
        duration_ms=duration_ms,
        cache_hits=cache_hits,
        cache_misses=cache_misses,
    )

PerformanceImpact

Bases: Enum

Execution cost of a rule.

Members

NEGLIGIBLE: Near-zero cost (simple checks). LOW: Fast (single-pass, no I/O). MEDIUM: Moderate (multi-pass or cross-file). HIGH: Expensive (may require step definition analysis).

Source code in behave_lint/models/enums.py
class PerformanceImpact(Enum):
    """Execution cost of a rule.

    Members:
        NEGLIGIBLE: Near-zero cost (simple checks).
        LOW: Fast (single-pass, no I/O).
        MEDIUM: Moderate (multi-pass or cross-file).
        HIGH: Expensive (may require step definition analysis).
    """

    NEGLIGIBLE = "negligible"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

RuleExample dataclass

A before/after example for a rule.

Attributes:

Name Type Description
before str

Gherkin code that triggers the rule.

after str

Gherkin code after the fix, or "No fix available".

description str

Explanation of the example.

Source code in behave_lint/models/rule_metadata.py
@dataclass(frozen=True, slots=True)
class RuleExample:
    """A before/after example for a rule.

    Attributes:
        before: Gherkin code that triggers the rule.
        after: Gherkin code after the fix, or "No fix available".
        description: Explanation of the example.
    """

    before: str
    after: str
    description: str

RuleMetadata dataclass

Identity and documentation for a rule.

Attributes:

Name Type Description
rule_id str

Stable, unique identifier (e.g., "BC001").

name str

Short, human-readable, kebab-case name.

title str

One-line summary for CLI and documentation.

description str

One-paragraph description of what the rule checks.

category Category

Rule category.

default_severity Severity

Default severity when the rule is enabled.

motivation str

Why the rule exists — the problem it solves.

since str

Version when the rule was introduced.

examples list[RuleExample]

Before/after examples for documentation.

auto_fix AutoFixCapability

Auto-fix capability. Defaults to NONE.

tags list[str]

Free-form tags for filtering and grouping.

references list[str]

External references (URLs, book sections, standards).

configurable bool

Whether the rule accepts parameters.

experimental bool

Whether the rule is experimental.

deprecated bool

Whether the rule is deprecated.

deprecated_version str | None

Version in which the rule was deprecated.

replaced_by str | None

Rule ID that replaces this one, or None.

aliases list[str]

Alternative names for backward compatibility.

dependencies list[str]

Rule IDs that must execute before this rule.

conflicts list[str]

Rule IDs that conflict with this rule.

doc_url str | None

URL to rule documentation, or None.

author str | None

Author or maintainer of the rule, or None.

min_version str | None

Minimum behave-lint version required, or None.

estimated_fix_cost FixCost

Estimated effort to fix.

performance_impact PerformanceImpact

Execution cost.

educational_value EducationalValue

Pedagogical value.

Source code in behave_lint/models/rule_metadata.py
@dataclass(frozen=True, slots=True)
class RuleMetadata:
    """Identity and documentation for a rule.

    Attributes:
        rule_id: Stable, unique identifier (e.g., "BC001").
        name: Short, human-readable, kebab-case name.
        title: One-line summary for CLI and documentation.
        description: One-paragraph description of what the rule checks.
        category: Rule category.
        default_severity: Default severity when the rule is enabled.
        motivation: Why the rule exists — the problem it solves.
        since: Version when the rule was introduced.
        examples: Before/after examples for documentation.
        auto_fix: Auto-fix capability. Defaults to NONE.
        tags: Free-form tags for filtering and grouping.
        references: External references (URLs, book sections, standards).
        configurable: Whether the rule accepts parameters.
        experimental: Whether the rule is experimental.
        deprecated: Whether the rule is deprecated.
        deprecated_version: Version in which the rule was deprecated.
        replaced_by: Rule ID that replaces this one, or None.
        aliases: Alternative names for backward compatibility.
        dependencies: Rule IDs that must execute before this rule.
        conflicts: Rule IDs that conflict with this rule.
        doc_url: URL to rule documentation, or None.
        author: Author or maintainer of the rule, or None.
        min_version: Minimum behave-lint version required, or None.
        estimated_fix_cost: Estimated effort to fix.
        performance_impact: Execution cost.
        educational_value: Pedagogical value.
    """

    rule_id: str
    name: str
    title: str
    description: str
    category: Category
    default_severity: Severity
    motivation: str
    since: str
    examples: list[RuleExample] = field(default_factory=list)
    auto_fix: AutoFixCapability = AutoFixCapability.NONE
    tags: list[str] = field(default_factory=list)
    references: list[str] = field(default_factory=list)
    configurable: bool = False
    experimental: bool = False
    deprecated: bool = False
    deprecated_version: str | None = None
    replaced_by: str | None = None
    aliases: list[str] = field(default_factory=list)
    dependencies: list[str] = field(default_factory=list)
    conflicts: list[str] = field(default_factory=list)
    doc_url: str | None = None
    author: str | None = None
    min_version: str | None = None
    estimated_fix_cost: FixCost = FixCost.LOW
    performance_impact: PerformanceImpact = PerformanceImpact.NEGLIGIBLE
    educational_value: EducationalValue = EducationalValue.NONE

    def __post_init__(self) -> None:
        """Validate field constraints after initialization."""
        if not self.rule_id:
            raise ValueError("rule_id must be a non-empty string")
        if not self.name:
            raise ValueError("name must be a non-empty string")
        if not self.description:
            raise ValueError("description must be a non-empty string")
        if not self.motivation:
            raise ValueError("motivation must be a non-empty string")
        if not self.since:
            raise ValueError("since must be a non-empty string")
        if self.deprecated and not self.replaced_by and not self.deprecated_version:
            import warnings

            warnings.warn(
                f"Rule {self.rule_id} is deprecated but has no "
                "replaced_by or deprecated_version.",
                stacklevel=2,
            )

__post_init__()

Validate field constraints after initialization.

Source code in behave_lint/models/rule_metadata.py
def __post_init__(self) -> None:
    """Validate field constraints after initialization."""
    if not self.rule_id:
        raise ValueError("rule_id must be a non-empty string")
    if not self.name:
        raise ValueError("name must be a non-empty string")
    if not self.description:
        raise ValueError("description must be a non-empty string")
    if not self.motivation:
        raise ValueError("motivation must be a non-empty string")
    if not self.since:
        raise ValueError("since must be a non-empty string")
    if self.deprecated and not self.replaced_by and not self.deprecated_version:
        import warnings

        warnings.warn(
            f"Rule {self.rule_id} is deprecated but has no "
            "replaced_by or deprecated_version.",
            stacklevel=2,
        )

Severity

Bases: Enum

Importance level of a diagnostic.

Members

ERROR: Must be fixed; causes non-zero exit code. WARNING: Should be fixed; does not cause non-zero exit by default. INFO: Informational; never affects exit code. OFF: Rule is disabled; no diagnostics are produced.

Source code in behave_lint/models/enums.py
class Severity(Enum):
    """Importance level of a diagnostic.

    Members:
        ERROR: Must be fixed; causes non-zero exit code.
        WARNING: Should be fixed; does not cause non-zero exit by default.
        INFO: Informational; never affects exit code.
        OFF: Rule is disabled; no diagnostics are produced.
    """

    ERROR = "error"
    WARNING = "warning"
    INFO = "info"
    OFF = "off"

    @classmethod
    def from_string(cls, value: str) -> Severity:
        """Parse a string into a Severity member.

        Args:
            value: Case-insensitive severity string.

        Returns:
            The matching Severity member.

        Raises:
            ValueError: If the string does not match any member.
        """
        normalized = value.strip().lower()
        for member in cls:
            if member.value == normalized:
                return member
        valid = ", ".join(m.value for m in cls)
        raise ValueError(f"Invalid severity '{value}'. Expected one of: {valid}.")

from_string(value) classmethod

Parse a string into a Severity member.

Parameters:

Name Type Description Default
value str

Case-insensitive severity string.

required

Returns:

Type Description
Severity

The matching Severity member.

Raises:

Type Description
ValueError

If the string does not match any member.

Source code in behave_lint/models/enums.py
@classmethod
def from_string(cls, value: str) -> Severity:
    """Parse a string into a Severity member.

    Args:
        value: Case-insensitive severity string.

    Returns:
        The matching Severity member.

    Raises:
        ValueError: If the string does not match any member.
    """
    normalized = value.strip().lower()
    for member in cls:
        if member.value == normalized:
            return member
    valid = ", ".join(m.value for m in cls)
    raise ValueError(f"Invalid severity '{value}'. Expected one of: {valid}.")

load_config(*, config_path=None, overrides=None, env=None, start_dir=None)

Load and resolve configuration from all sources.

Merges configuration from four sources by precedence: 1. Built-in defaults (lowest) 2. pyproject.toml [tool.behave-lint] 3. Environment variables (BEHAVE_LINT_*) 4. Explicit overrides (highest)

Parameters:

Name Type Description Default
config_path str | None

Explicit path to a pyproject.toml. If None, searches the current directory and ancestors.

None
overrides dict[str, object] | None

Dictionary of configuration overrides (same keys as Config fields). These have the highest precedence.

None
env dict[str, str] | None

Environment dict. If None, uses os.environ.

None
start_dir Path | None

Directory to start searching from. Defaults to cwd.

None

Returns:

Type Description
Config

A resolved Config object.

Raises:

Type Description
ConfigError

If configuration is invalid (unknown key, invalid value, invalid severity).

Source code in behave_lint/configuration/loader.py
def load_config(
    *,
    config_path: str | None = None,
    overrides: dict[str, object] | None = None,
    env: dict[str, str] | None = None,
    start_dir: Path | None = None,
) -> Config:
    """Load and resolve configuration from all sources.

    Merges configuration from four sources by precedence:
    1. Built-in defaults (lowest)
    2. pyproject.toml [tool.behave-lint]
    3. Environment variables (BEHAVE_LINT_*)
    4. Explicit overrides (highest)

    Args:
        config_path: Explicit path to a pyproject.toml. If None,
            searches the current directory and ancestors.
        overrides: Dictionary of configuration overrides (same keys as
            Config fields). These have the highest precedence.
        env: Environment dict. If None, uses os.environ.
        start_dir: Directory to start searching from. Defaults to cwd.

    Returns:
        A resolved Config object.

    Raises:
        ConfigError: If configuration is invalid (unknown key, invalid
            value, invalid severity).
    """
    # 1. Built-in defaults
    merged = _default_config_dict()

    # 2. Load pyproject.toml (if any)
    config_file = find_config_file(start_dir=start_dir, explicit_path=config_path)
    toml_config: dict[str, object] = {}
    if config_file is not None:
        raw_toml = load_toml_config(config_file)
        check_unknown_keys(raw_toml)
        toml_config = normalize_keys(raw_toml)

    # 3. Determine profile from highest-precedence source:
    #    CLI overrides > env > pyproject.toml > defaults
    profile_name = "none"
    if overrides is not None:
        profile_name = str(overrides.get("profile", "none"))
    if profile_name == "none":
        env_profile = env.get(f"{ENV_PREFIX}PROFILE") if env else None
        if env_profile is None:
            import os

            env_profile = os.environ.get(f"{ENV_PREFIX}PROFILE")
        if env_profile:
            profile_name = env_profile
    if profile_name == "none":
        toml_profile = toml_config.get("profile", "none")
        if isinstance(toml_profile, str):
            profile_name = toml_profile

    # 4. Apply profile (after defaults, before pyproject.toml values)
    if profile_name != "none":
        from behave_lint.configuration.profiles import get_profile_config

        profile_config = get_profile_config(profile_name)
        merged = merge_configs(merged, profile_config)
        merged["profile"] = profile_name

    # 4b. Determine groups from highest-precedence source:
    #     CLI overrides > env > pyproject.toml > defaults
    group_names: list[str] = []
    if overrides is not None:
        group_raw = overrides.get("group", [])
        if isinstance(group_raw, str):
            from behave_lint.configuration.groups import parse_groups

            group_names = parse_groups(group_raw)
        elif isinstance(group_raw, list):
            group_names = list(group_raw)
    if not group_names:
        env_group = env.get(f"{ENV_PREFIX}GROUP") if env else None
        if env_group is None:
            import os

            env_group = os.environ.get(f"{ENV_PREFIX}GROUP")
        if env_group:
            from behave_lint.configuration.groups import parse_groups

            group_names = parse_groups(env_group)
    if not group_names:
        toml_group = toml_config.get("group", [])
        if isinstance(toml_group, str):
            from behave_lint.configuration.groups import parse_groups

            group_names = parse_groups(toml_group)
        elif isinstance(toml_group, list):
            group_names = list(toml_group)

    # 4c. Record group names for later expansion
    if group_names:
        merged["group"] = group_names

    # 5. pyproject.toml values (override profile)
    if toml_config:
        merged = merge_configs(merged, toml_config)

    # 6. Environment variables
    env_overrides = load_from_env(env)
    merged = merge_configs(merged, env_overrides)

    # 7. Explicit overrides (highest precedence)
    if overrides is not None:
        normalized_overrides = normalize_keys(overrides)
        merged = merge_configs(merged, normalized_overrides)

    # 8. Expand groups to rule IDs (after all merges, additive to select)
    final_groups = merged.get("group", [])
    if isinstance(final_groups, str):
        from behave_lint.configuration.groups import parse_groups

        final_groups = parse_groups(final_groups)
        merged["group"] = final_groups
    if isinstance(final_groups, list) and final_groups:
        from behave_lint.configuration.groups import (
            get_category_prefix,
            get_group_tags,
            is_category_group,
            is_valid_group,
        )

        for gname in final_groups:
            if not is_valid_group(gname):
                from behave_lint.configuration.groups import get_group_names

                valid = ", ".join(get_group_names())
                raise InvalidConfigValueError(
                    key="group",
                    value=gname,
                    expected=f"one of: {valid}",
                )

        all_rule_ids, rule_tags_map = _build_rule_mappings()

        group_rule_ids: set[str] = set()
        for gname in final_groups:
            if is_category_group(gname):
                prefix = get_category_prefix(gname)
                if prefix:
                    for rule_id in all_rule_ids:
                        if rule_id.startswith(prefix):
                            group_rule_ids.add(rule_id)
            else:
                tags = get_group_tags(gname)
                if tags:
                    for rule_id, rule_tags in rule_tags_map.items():
                        if tags & rule_tags:
                            group_rule_ids.add(rule_id)

        if group_rule_ids:
            existing_select = merged.get("select", [])
            if not isinstance(existing_select, list):
                existing_select = []
            merged["select"] = list(
                set(existing_select) | group_rule_ids,
            )

    return build_config(merged)