Skip to content

formatters

ruff_sync.formatters

Output formatters for CLI results.

LOGGER module-attribute

LOGGER = getLogger(__name__)

ResultFormatter

Bases: Protocol

Protocol for output formatters.

Streaming formatters (Text, GitHub, JSON) implement note / info / success / error / warning / debug / diff and provide a no-op finalize.

Accumulating formatters (GitLab, SARIF) collect issues during the run and write their structured report in finalize. finalize is always called by the CLI in a try...finally block, so all formatters receive it unconditionally.

Source code in src/ruff_sync/formatters.py
class ResultFormatter(Protocol):
    """Protocol for output formatters.

    Streaming formatters (Text, GitHub, JSON) implement ``note`` / ``info`` /
    ``success`` / ``error`` / ``warning`` / ``debug`` / ``diff`` and provide a
    no-op ``finalize``.

    Accumulating formatters (GitLab, SARIF) collect issues during the run and
    write their structured report in ``finalize``.  ``finalize`` is always
    called by the CLI in a ``try...finally`` block, so all formatters receive
    it unconditionally.
    """

    def note(self, message: str) -> None:
        """Print a status note (unconditional)."""

    def info(self, message: str, logger: logging.Logger | None = None) -> None:
        """Print an informational message."""

    def success(self, message: str) -> None:
        """Print a success message."""

    def error(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Print an error message.

        Args:
            message: Human-readable description of the issue.
            file_path: Path to the file that contains the issue.
            logger: Optional logger to use instead of the module logger.
            check_name: Machine-readable rule ID (used by structured formatters).
            drift_key: Dotted TOML key that drifted, e.g. ``"lint.select"``.
                Used by structured formatters to build stable fingerprints.
        """

    def warning(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Print a warning message.

        Args:
            message: Human-readable description of the issue.
            file_path: Path to the file that contains the issue.
            logger: Optional logger to use instead of the module logger.
            check_name: Machine-readable rule ID (used by structured formatters).
            drift_key: Dotted TOML key that drifted, e.g. ``"lint.select"``.
                Used by structured formatters to build stable fingerprints.
        """

    def debug(self, message: str, logger: logging.Logger | None = None) -> None:
        """Print a debug message."""

    def diff(self, diff_text: str) -> None:
        """Print a unified diff between configurations.

        Note:
            Structured (accumulating) formatters intentionally ignore this
            method — diffs are not representable in JSON report schemas.
        """

    def finalize(self) -> None:
        """Finalize and flush all output.

        Streaming formatters (Text, GitHub, JSON) implement this as a no-op.
        Accumulating formatters (GitLab, SARIF) write their collected report
        here.  The CLI calls this unconditionally inside a ``try...finally``
        block so it is always executed, even when an exception occurred.
        """

note

note(message)

Print a status note (unconditional).

Source code in src/ruff_sync/formatters.py
def note(self, message: str) -> None:
    """Print a status note (unconditional)."""

info

info(message, logger=None)

Print an informational message.

Source code in src/ruff_sync/formatters.py
def info(self, message: str, logger: logging.Logger | None = None) -> None:
    """Print an informational message."""

success

success(message)

Print a success message.

Source code in src/ruff_sync/formatters.py
def success(self, message: str) -> None:
    """Print a success message."""

error

error(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Print an error message.

Parameters:

Name Type Description Default
message str

Human-readable description of the issue.

required
file_path Path | None

Path to the file that contains the issue.

None
logger Logger | None

Optional logger to use instead of the module logger.

None
check_name str

Machine-readable rule ID (used by structured formatters).

_DEFAULT_CHECK_NAME
drift_key str | None

Dotted TOML key that drifted, e.g. "lint.select". Used by structured formatters to build stable fingerprints.

None
Source code in src/ruff_sync/formatters.py
def error(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Print an error message.

    Args:
        message: Human-readable description of the issue.
        file_path: Path to the file that contains the issue.
        logger: Optional logger to use instead of the module logger.
        check_name: Machine-readable rule ID (used by structured formatters).
        drift_key: Dotted TOML key that drifted, e.g. ``"lint.select"``.
            Used by structured formatters to build stable fingerprints.
    """

warning

warning(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Print a warning message.

Parameters:

Name Type Description Default
message str

Human-readable description of the issue.

required
file_path Path | None

Path to the file that contains the issue.

None
logger Logger | None

Optional logger to use instead of the module logger.

None
check_name str

Machine-readable rule ID (used by structured formatters).

_DEFAULT_CHECK_NAME
drift_key str | None

Dotted TOML key that drifted, e.g. "lint.select". Used by structured formatters to build stable fingerprints.

None
Source code in src/ruff_sync/formatters.py
def warning(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Print a warning message.

    Args:
        message: Human-readable description of the issue.
        file_path: Path to the file that contains the issue.
        logger: Optional logger to use instead of the module logger.
        check_name: Machine-readable rule ID (used by structured formatters).
        drift_key: Dotted TOML key that drifted, e.g. ``"lint.select"``.
            Used by structured formatters to build stable fingerprints.
    """

debug

debug(message, logger=None)

Print a debug message.

Source code in src/ruff_sync/formatters.py
def debug(self, message: str, logger: logging.Logger | None = None) -> None:
    """Print a debug message."""

diff

diff(diff_text)

Print a unified diff between configurations.

Note

Structured (accumulating) formatters intentionally ignore this method — diffs are not representable in JSON report schemas.

Source code in src/ruff_sync/formatters.py
def diff(self, diff_text: str) -> None:
    """Print a unified diff between configurations.

    Note:
        Structured (accumulating) formatters intentionally ignore this
        method — diffs are not representable in JSON report schemas.
    """

finalize

finalize()

Finalize and flush all output.

Streaming formatters (Text, GitHub, JSON) implement this as a no-op. Accumulating formatters (GitLab, SARIF) write their collected report here. The CLI calls this unconditionally inside a try...finally block so it is always executed, even when an exception occurred.

Source code in src/ruff_sync/formatters.py
def finalize(self) -> None:
    """Finalize and flush all output.

    Streaming formatters (Text, GitHub, JSON) implement this as a no-op.
    Accumulating formatters (GitLab, SARIF) write their collected report
    here.  The CLI calls this unconditionally inside a ``try...finally``
    block so it is always executed, even when an exception occurred.
    """

TextFormatter

Standard text output formatter.

Delegates diagnostic messages (info, warning, error, debug) to the project logger to ensure they benefit from standard logging configuration (colors, streams). Primary command feedback (note, success) is printed to stdout.

Source code in src/ruff_sync/formatters.py
class TextFormatter:
    """Standard text output formatter.

    Delegates diagnostic messages (info, warning, error, debug) to the project
    logger to ensure they benefit from standard logging configuration (colors,
    streams).  Primary command feedback (note, success) is printed to stdout.
    """

    def note(self, message: str) -> None:
        """Print a status note to stdout."""
        print(message)

    def info(self, message: str, logger: logging.Logger | None = None) -> None:
        """Log an info message."""
        (logger or LOGGER).info(message)

    def success(self, message: str) -> None:
        """Print a success message to stdout."""
        print(message)

    def error(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Log an error message."""
        (logger or LOGGER).error(message)

    def warning(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Log a warning message."""
        (logger or LOGGER).warning(message)

    def debug(self, message: str, logger: logging.Logger | None = None) -> None:
        """Log a debug message."""
        (logger or LOGGER).debug(message)

    def diff(self, diff_text: str) -> None:
        """Print a unified diff directly to stdout."""
        print(diff_text, end="")

    def finalize(self) -> None:
        """No-op for streaming formatters."""

note

note(message)

Print a status note to stdout.

Source code in src/ruff_sync/formatters.py
def note(self, message: str) -> None:
    """Print a status note to stdout."""
    print(message)

info

info(message, logger=None)

Log an info message.

Source code in src/ruff_sync/formatters.py
def info(self, message: str, logger: logging.Logger | None = None) -> None:
    """Log an info message."""
    (logger or LOGGER).info(message)

success

success(message)

Print a success message to stdout.

Source code in src/ruff_sync/formatters.py
def success(self, message: str) -> None:
    """Print a success message to stdout."""
    print(message)

error

error(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Log an error message.

Source code in src/ruff_sync/formatters.py
def error(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Log an error message."""
    (logger or LOGGER).error(message)

warning

warning(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Log a warning message.

Source code in src/ruff_sync/formatters.py
def warning(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Log a warning message."""
    (logger or LOGGER).warning(message)

debug

debug(message, logger=None)

Log a debug message.

Source code in src/ruff_sync/formatters.py
def debug(self, message: str, logger: logging.Logger | None = None) -> None:
    """Log a debug message."""
    (logger or LOGGER).debug(message)

diff

diff(diff_text)

Print a unified diff directly to stdout.

Source code in src/ruff_sync/formatters.py
def diff(self, diff_text: str) -> None:
    """Print a unified diff directly to stdout."""
    print(diff_text, end="")

finalize

finalize()

No-op for streaming formatters.

Source code in src/ruff_sync/formatters.py
def finalize(self) -> None:
    """No-op for streaming formatters."""

GithubFormatter

GitHub Actions output formatter.

Emits ::error:: and ::warning:: workflow commands for inline annotations.

Source code in src/ruff_sync/formatters.py
class GithubFormatter:
    """GitHub Actions output formatter.

    Emits ``::error::`` and ``::warning::`` workflow commands for inline
    annotations.
    """

    @staticmethod
    def _escape(value: str, is_property: bool = False) -> str:
        r"""Escapes characters for GitHub Actions workflow commands.

        GitHub requires percent-encoding for '%', '\r', and '\n' in all
        messages.  Additionally, property values (like file and title) require
        escaping for ':' and ','.
        """
        escaped = value.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
        if is_property:
            return escaped.replace(":", "%3A").replace(",", "%2C")
        return escaped

    def note(self, message: str) -> None:
        """Print a status note (standard stdout)."""
        print(message)

    def info(self, message: str, logger: logging.Logger | None = None) -> None:
        """Print an info message (delegates to logger)."""
        (logger or LOGGER).info(message)

    def success(self, message: str) -> None:
        """Print a success message (standard stdout)."""
        print(message)

    def error(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Print an error message as a GitHub Action error annotation."""
        # Delegate standard log output to the logger to preserve context
        (logger or LOGGER).error(message)

        file_val = self._escape(str(file_path), is_property=True) if file_path else ""
        file_arg = f"file={file_val},line=1," if file_path else ""
        title_val = self._escape("Ruff Sync Error", is_property=True)

        clean_msg = message.removeprefix("❌ ").removeprefix("⚠️ ")
        escaped_msg = self._escape(clean_msg)
        print(f"::error {file_arg}title={title_val}::{escaped_msg}")

    def warning(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Print a warning message as a GitHub Action warning annotation."""
        (logger or LOGGER).warning(message)

        file_val = self._escape(str(file_path), is_property=True) if file_path else ""
        file_arg = f"file={file_val},line=1," if file_path else ""
        title_val = self._escape("Ruff Sync Warning", is_property=True)

        clean_msg = message.removeprefix("❌ ").removeprefix("⚠️ ")
        escaped_msg = self._escape(clean_msg)
        print(f"::warning {file_arg}title={title_val}::{escaped_msg}")

    def debug(self, message: str, logger: logging.Logger | None = None) -> None:
        """Print a debug message as a GitHub Action debug annotation."""
        (logger or LOGGER).debug(message)
        escaped_msg = self._escape(message)
        print(f"::debug::{escaped_msg}")

    def diff(self, diff_text: str) -> None:
        """Print a unified diff in GitHub Actions logs (standard stdout)."""
        print(diff_text, end="")

    def finalize(self) -> None:
        """No-op for streaming formatters."""

note

note(message)

Print a status note (standard stdout).

Source code in src/ruff_sync/formatters.py
def note(self, message: str) -> None:
    """Print a status note (standard stdout)."""
    print(message)

info

info(message, logger=None)

Print an info message (delegates to logger).

Source code in src/ruff_sync/formatters.py
def info(self, message: str, logger: logging.Logger | None = None) -> None:
    """Print an info message (delegates to logger)."""
    (logger or LOGGER).info(message)

success

success(message)

Print a success message (standard stdout).

Source code in src/ruff_sync/formatters.py
def success(self, message: str) -> None:
    """Print a success message (standard stdout)."""
    print(message)

error

error(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Print an error message as a GitHub Action error annotation.

Source code in src/ruff_sync/formatters.py
def error(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Print an error message as a GitHub Action error annotation."""
    # Delegate standard log output to the logger to preserve context
    (logger or LOGGER).error(message)

    file_val = self._escape(str(file_path), is_property=True) if file_path else ""
    file_arg = f"file={file_val},line=1," if file_path else ""
    title_val = self._escape("Ruff Sync Error", is_property=True)

    clean_msg = message.removeprefix("❌ ").removeprefix("⚠️ ")
    escaped_msg = self._escape(clean_msg)
    print(f"::error {file_arg}title={title_val}::{escaped_msg}")

warning

warning(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Print a warning message as a GitHub Action warning annotation.

Source code in src/ruff_sync/formatters.py
def warning(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Print a warning message as a GitHub Action warning annotation."""
    (logger or LOGGER).warning(message)

    file_val = self._escape(str(file_path), is_property=True) if file_path else ""
    file_arg = f"file={file_val},line=1," if file_path else ""
    title_val = self._escape("Ruff Sync Warning", is_property=True)

    clean_msg = message.removeprefix("❌ ").removeprefix("⚠️ ")
    escaped_msg = self._escape(clean_msg)
    print(f"::warning {file_arg}title={title_val}::{escaped_msg}")

debug

debug(message, logger=None)

Print a debug message as a GitHub Action debug annotation.

Source code in src/ruff_sync/formatters.py
def debug(self, message: str, logger: logging.Logger | None = None) -> None:
    """Print a debug message as a GitHub Action debug annotation."""
    (logger or LOGGER).debug(message)
    escaped_msg = self._escape(message)
    print(f"::debug::{escaped_msg}")

diff

diff(diff_text)

Print a unified diff in GitHub Actions logs (standard stdout).

Source code in src/ruff_sync/formatters.py
def diff(self, diff_text: str) -> None:
    """Print a unified diff in GitHub Actions logs (standard stdout)."""
    print(diff_text, end="")

finalize

finalize()

No-op for streaming formatters.

Source code in src/ruff_sync/formatters.py
def finalize(self) -> None:
    """No-op for streaming formatters."""

JsonFormatter

JSON output formatter (newline-delimited JSON, one record per line).

Source code in src/ruff_sync/formatters.py
class JsonFormatter:
    """JSON output formatter (newline-delimited JSON, one record per line)."""

    def note(self, message: str) -> None:
        """Print a status note as JSON."""
        print(json.dumps({"level": "note", "message": message}))

    def info(self, message: str, logger: logging.Logger | None = None) -> None:
        """Print an info message as JSON."""
        data = {"level": "info", "message": message}
        if logger:
            data["logger"] = logger.name
        print(json.dumps(data))

    def success(self, message: str) -> None:
        """Print a success message as JSON."""
        print(json.dumps({"level": "success", "message": message}))

    def error(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Print an error message as JSON."""
        data = {"level": "error", "message": message}
        if file_path:
            data["file"] = str(file_path)
        if logger:
            data["logger"] = logger.name
        if drift_key:
            data["drift_key"] = drift_key
        data["check_name"] = check_name
        print(json.dumps(data))

    def warning(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Print a warning message as JSON."""
        data = {"level": "warning", "message": message}
        if file_path:
            data["file"] = str(file_path)
        if logger:
            data["logger"] = logger.name
        if drift_key:
            data["drift_key"] = drift_key
        data["check_name"] = check_name
        print(json.dumps(data))

    def debug(self, message: str, logger: logging.Logger | None = None) -> None:
        """Print a debug message as JSON."""
        data = {"level": "debug", "message": message}
        if logger:
            data["logger"] = logger.name
        print(json.dumps(data))

    def diff(self, diff_text: str) -> None:
        """Print a unified diff as JSON."""
        # Strip trailing newline if any, as it's common in diff text
        print(json.dumps({"level": "diff", "message": diff_text.strip()}))

    def finalize(self) -> None:
        """No-op for streaming formatters."""

note

note(message)

Print a status note as JSON.

Source code in src/ruff_sync/formatters.py
def note(self, message: str) -> None:
    """Print a status note as JSON."""
    print(json.dumps({"level": "note", "message": message}))

info

info(message, logger=None)

Print an info message as JSON.

Source code in src/ruff_sync/formatters.py
def info(self, message: str, logger: logging.Logger | None = None) -> None:
    """Print an info message as JSON."""
    data = {"level": "info", "message": message}
    if logger:
        data["logger"] = logger.name
    print(json.dumps(data))

success

success(message)

Print a success message as JSON.

Source code in src/ruff_sync/formatters.py
def success(self, message: str) -> None:
    """Print a success message as JSON."""
    print(json.dumps({"level": "success", "message": message}))

error

error(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Print an error message as JSON.

Source code in src/ruff_sync/formatters.py
def error(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Print an error message as JSON."""
    data = {"level": "error", "message": message}
    if file_path:
        data["file"] = str(file_path)
    if logger:
        data["logger"] = logger.name
    if drift_key:
        data["drift_key"] = drift_key
    data["check_name"] = check_name
    print(json.dumps(data))

warning

warning(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Print a warning message as JSON.

Source code in src/ruff_sync/formatters.py
def warning(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Print a warning message as JSON."""
    data = {"level": "warning", "message": message}
    if file_path:
        data["file"] = str(file_path)
    if logger:
        data["logger"] = logger.name
    if drift_key:
        data["drift_key"] = drift_key
    data["check_name"] = check_name
    print(json.dumps(data))

debug

debug(message, logger=None)

Print a debug message as JSON.

Source code in src/ruff_sync/formatters.py
def debug(self, message: str, logger: logging.Logger | None = None) -> None:
    """Print a debug message as JSON."""
    data = {"level": "debug", "message": message}
    if logger:
        data["logger"] = logger.name
    print(json.dumps(data))

diff

diff(diff_text)

Print a unified diff as JSON.

Source code in src/ruff_sync/formatters.py
def diff(self, diff_text: str) -> None:
    """Print a unified diff as JSON."""
    # Strip trailing newline if any, as it's common in diff text
    print(json.dumps({"level": "diff", "message": diff_text.strip()}))

finalize

finalize()

No-op for streaming formatters.

Source code in src/ruff_sync/formatters.py
def finalize(self) -> None:
    """No-op for streaming formatters."""

GitlabLines

Bases: TypedDict

GitLab Code Quality report lines.

Source code in src/ruff_sync/formatters.py
class GitlabLines(TypedDict):
    """GitLab Code Quality report lines."""

    begin: int

begin instance-attribute

begin

GitlabLocation

Bases: TypedDict

GitLab Code Quality report location.

Source code in src/ruff_sync/formatters.py
class GitlabLocation(TypedDict):
    """GitLab Code Quality report location."""

    path: str
    lines: GitlabLines

path instance-attribute

path

lines instance-attribute

lines

GitlabIssue

Bases: TypedDict

GitLab Code Quality report issue.

Source code in src/ruff_sync/formatters.py
class GitlabIssue(TypedDict):
    """GitLab Code Quality report issue."""

    description: str
    check_name: str
    fingerprint: str
    severity: Literal["info", "minor", "major", "critical", "blocker"]
    location: GitlabLocation

description instance-attribute

description

check_name instance-attribute

check_name

fingerprint instance-attribute

fingerprint

severity instance-attribute

severity

location instance-attribute

location

GitlabFormatter

GitLab Code Quality report formatter.

Accumulates issues during the run and writes a single valid JSON array to stdout in finalize(). The CI job redirects stdout to a file::

ruff-sync check --output-format gitlab > gl-code-quality-report.json

An empty array ([]) is emitted when no issues were collected, which signals to GitLab that previously reported issues are now resolved.

Fingerprints are deterministic MD5 hashes so GitLab can track whether an issue was introduced or resolved between branches.

Source code in src/ruff_sync/formatters.py
class GitlabFormatter:
    """GitLab Code Quality report formatter.

    Accumulates issues during the run and writes a single valid JSON array to
    stdout in ``finalize()``.  The CI job redirects stdout to a file::

        ruff-sync check --output-format gitlab > gl-code-quality-report.json

    An empty array (``[]``) is emitted when no issues were collected, which
    signals to GitLab that previously reported issues are now resolved.

    Fingerprints are deterministic MD5 hashes so GitLab can track whether an
    issue was introduced or resolved between branches.
    """

    def __init__(self) -> None:
        """Initialise an empty issue list."""
        self._issues: list[GitlabIssue] = []

    # ------------------------------------------------------------------
    # Protocol methods
    # ------------------------------------------------------------------

    def note(self, message: str) -> None:
        """Delegate to logger only; not representable in the Code Quality schema."""
        LOGGER.info(message)

    def info(self, message: str, logger: logging.Logger | None = None) -> None:
        """Delegate to logger only; not included in the structured report."""
        (logger or LOGGER).info(message)

    def success(self, message: str) -> None:
        """Delegate to logger only; not representable in the Code Quality schema."""
        LOGGER.info(message)

    def error(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Accumulate a major-severity Code Quality issue."""
        (logger or LOGGER).error(message)
        self._issues.append(
            self._make_issue(
                description=message,
                check_name=check_name,
                severity="major",
                file_path=file_path,
                drift_key=drift_key,
            )
        )

    def warning(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Accumulate a minor-severity Code Quality issue."""
        (logger or LOGGER).warning(message)
        self._issues.append(
            self._make_issue(
                description=message,
                check_name=check_name,
                severity="minor",
                file_path=file_path,
                drift_key=drift_key,
            )
        )

    def debug(self, message: str, logger: logging.Logger | None = None) -> None:
        """Delegate to logger only; not included in the structured report."""
        (logger or LOGGER).debug(message)

    def diff(self, diff_text: str) -> None:
        """Ignored by structured formatters — diffs are not representable in JSON report schemas."""

    def finalize(self) -> None:
        """Write the collected issues as a GitLab Code Quality JSON array to stdout.

        Always produces valid JSON: an empty array ``[]`` when no issues were
        collected (signals resolution of previously reported issues to GitLab).
        """
        print(json.dumps(self._issues, indent=2))

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _make_issue(
        self,
        description: str,
        check_name: str,
        severity: Literal["info", "minor", "major", "critical", "blocker"],
        file_path: pathlib.Path | None,
        drift_key: str | None,
    ) -> GitlabIssue:
        """Build a single Code Quality issue object."""
        # Normalize location.path to be relative to the repo root (no absolute paths).
        if file_path:
            if file_path.is_absolute():
                try:
                    path = str(file_path.relative_to(pathlib.Path.cwd()))
                except ValueError:
                    # If the absolute path is outside the CWD (repo root in CI),
                    # fall back to the filename so GitLab can at least attempt a map.
                    path = file_path.name
            else:
                path = str(file_path)
        else:
            path = "pyproject.toml"

        return {
            "description": description,
            "check_name": check_name,
            "fingerprint": self._make_fingerprint(path, check_name, drift_key),
            "severity": severity,
            "location": {"path": path, "lines": {"begin": 1}},
        }

    @staticmethod
    def _make_fingerprint(path: str, check_name: str, drift_key: str | None) -> str:
        """Return a stable MD5 fingerprint for a Code Quality issue.

        The fingerprint must be deterministic (same inputs → same output on
        every pipeline run) so GitLab can track introduced vs resolved issues.
        Never include timestamps, UUIDs, or any other runtime-variable data.
        """
        # Include check_name to prevent collisions if multiple rules trigger on the same key.
        raw = f"ruff-sync:{check_name}:{path}:{drift_key or ''}"
        return hashlib.md5(raw.encode()).hexdigest()  # noqa: S324

__init__

__init__()

Initialise an empty issue list.

Source code in src/ruff_sync/formatters.py
def __init__(self) -> None:
    """Initialise an empty issue list."""
    self._issues: list[GitlabIssue] = []

note

note(message)

Delegate to logger only; not representable in the Code Quality schema.

Source code in src/ruff_sync/formatters.py
def note(self, message: str) -> None:
    """Delegate to logger only; not representable in the Code Quality schema."""
    LOGGER.info(message)

info

info(message, logger=None)

Delegate to logger only; not included in the structured report.

Source code in src/ruff_sync/formatters.py
def info(self, message: str, logger: logging.Logger | None = None) -> None:
    """Delegate to logger only; not included in the structured report."""
    (logger or LOGGER).info(message)

success

success(message)

Delegate to logger only; not representable in the Code Quality schema.

Source code in src/ruff_sync/formatters.py
def success(self, message: str) -> None:
    """Delegate to logger only; not representable in the Code Quality schema."""
    LOGGER.info(message)

error

error(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Accumulate a major-severity Code Quality issue.

Source code in src/ruff_sync/formatters.py
def error(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Accumulate a major-severity Code Quality issue."""
    (logger or LOGGER).error(message)
    self._issues.append(
        self._make_issue(
            description=message,
            check_name=check_name,
            severity="major",
            file_path=file_path,
            drift_key=drift_key,
        )
    )

warning

warning(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Accumulate a minor-severity Code Quality issue.

Source code in src/ruff_sync/formatters.py
def warning(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Accumulate a minor-severity Code Quality issue."""
    (logger or LOGGER).warning(message)
    self._issues.append(
        self._make_issue(
            description=message,
            check_name=check_name,
            severity="minor",
            file_path=file_path,
            drift_key=drift_key,
        )
    )

debug

debug(message, logger=None)

Delegate to logger only; not included in the structured report.

Source code in src/ruff_sync/formatters.py
def debug(self, message: str, logger: logging.Logger | None = None) -> None:
    """Delegate to logger only; not included in the structured report."""
    (logger or LOGGER).debug(message)

diff

diff(diff_text)

Ignored by structured formatters — diffs are not representable in JSON report schemas.

Source code in src/ruff_sync/formatters.py
def diff(self, diff_text: str) -> None:
    """Ignored by structured formatters — diffs are not representable in JSON report schemas."""

finalize

finalize()

Write the collected issues as a GitLab Code Quality JSON array to stdout.

Always produces valid JSON: an empty array [] when no issues were collected (signals resolution of previously reported issues to GitLab).

Source code in src/ruff_sync/formatters.py
def finalize(self) -> None:
    """Write the collected issues as a GitLab Code Quality JSON array to stdout.

    Always produces valid JSON: an empty array ``[]`` when no issues were
    collected (signals resolution of previously reported issues to GitLab).
    """
    print(json.dumps(self._issues, indent=2))

SarifResult

Bases: TypedDict

A single SARIF result (finding).

Source code in src/ruff_sync/formatters.py
class SarifResult(TypedDict, total=False):
    """A single SARIF result (finding)."""

    ruleId: str
    level: Literal["error", "warning", "note"]
    message: dict[str, str]
    locations: list[dict[str, Any]]
    fingerprints: dict[str, str]
    properties: dict[str, str]

ruleId instance-attribute

ruleId

level instance-attribute

level

message instance-attribute

message

locations instance-attribute

locations

fingerprints instance-attribute

fingerprints

properties instance-attribute

properties

SarifFormatter

SARIF v2.1.0 output formatter.

Accumulates results during the run and writes a complete SARIF document to stdout in finalize(). The CI job redirects stdout to a file::

ruff-sync check --output-format sarif > results.sarif

An empty results list is emitted when no issues were found.

Schema: https://json.schemastore.org/sarif-2.1.0.json

Source code in src/ruff_sync/formatters.py
class SarifFormatter:
    """SARIF v2.1.0 output formatter.

    Accumulates results during the run and writes a complete SARIF document to
    stdout in ``finalize()``.  The CI job redirects stdout to a file::

        ruff-sync check --output-format sarif > results.sarif

    An empty ``results`` list is emitted when no issues were found.

    Schema: https://json.schemastore.org/sarif-2.1.0.json
    """

    _RULE_ID: Final[str] = "RUFF-SYNC-CONFIG-DRIFT"
    _SCHEMA: Final[str] = "https://json.schemastore.org/sarif-2.1.0.json"
    _SARIF_VERSION: Final[str] = "2.1.0"

    def __init__(self) -> None:
        """Initialise with an empty result list."""
        self._results: list[SarifResult] = []

    # ------------------------------------------------------------------
    # Protocol methods
    # ------------------------------------------------------------------

    def note(self, message: str) -> None:
        """Delegate to logger only; not representable in the SARIF schema."""
        LOGGER.info(message)

    def info(self, message: str, logger: logging.Logger | None = None) -> None:
        """Delegate to logger only; not included in the structured report."""
        (logger or LOGGER).info(message)

    def success(self, message: str) -> None:
        """Delegate to logger only; not representable in the SARIF schema."""
        LOGGER.info(message)

    def error(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Accumulate an error-level SARIF result."""
        (logger or LOGGER).error(message)
        self._results.append(
            self._make_result(
                message=message,
                level="error",
                file_path=file_path,
                check_name=check_name,
                drift_key=drift_key,
            )
        )

    def warning(
        self,
        message: str,
        file_path: pathlib.Path | None = None,
        logger: logging.Logger | None = None,
        check_name: str = _DEFAULT_CHECK_NAME,
        drift_key: str | None = None,
    ) -> None:
        """Accumulate a warning-level SARIF result."""
        (logger or LOGGER).warning(message)
        self._results.append(
            self._make_result(
                message=message,
                level="warning",
                file_path=file_path,
                check_name=check_name,
                drift_key=drift_key,
            )
        )

    def debug(self, message: str, logger: logging.Logger | None = None) -> None:
        """Delegate to logger only; not included in the structured report."""
        (logger or LOGGER).debug(message)

    def diff(self, diff_text: str) -> None:
        """Ignored by structured formatters — diffs are not representable in SARIF."""

    def finalize(self) -> None:
        """Write the collected results as a SARIF v2.1.0 document to stdout.

        Rules are de-duplicated from the accumulated results so the ``rules``
        list contains one entry per unique ``ruleId`` seen across all findings.
        """
        seen_ids: set[str] = set()
        rules: list[dict[str, Any]] = []
        for result in self._results:
            rule_id = result["ruleId"]
            if rule_id not in seen_ids:
                seen_ids.add(rule_id)
                rules.append(
                    {
                        "id": rule_id,
                        "name": "ConfigDrift",
                        "shortDescription": {
                            "text": "Ruff configuration has drifted from upstream."
                        },
                        "helpUri": "https://github.com/Kilo59/ruff-sync",
                    }
                )
        if not rules:
            # No findings — emit a single placeholder rule so the SARIF document
            # is always schema-valid (a tool with zero rules is still valid, but
            # some consumers require at least one rule entry).
            rules = [
                {
                    "id": self._RULE_ID,
                    "name": "ConfigDrift",
                    "shortDescription": {"text": "Ruff configuration has drifted from upstream."},
                    "helpUri": "https://github.com/Kilo59/ruff-sync",
                }
            ]
        sarif_doc: dict[str, Any] = {
            "version": self._SARIF_VERSION,
            "$schema": self._SCHEMA,
            "runs": [
                {
                    "tool": {
                        "driver": {
                            "name": "ruff-sync",
                            "informationUri": "https://github.com/Kilo59/ruff-sync",
                            "rules": rules,
                        }
                    },
                    "results": self._results,
                }
            ],
        }
        print(json.dumps(sarif_doc, indent=2))

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _make_result(
        self,
        message: str,
        level: Literal["error", "warning", "note"],
        file_path: pathlib.Path | None,
        check_name: str | None = None,
        drift_key: str | None = None,
    ) -> SarifResult:
        """Build a single SARIF result object.

        Args:
            message: Human-readable finding text.
            level: SARIF severity level.
            file_path: Source file the finding belongs to.
            check_name: Machine-readable rule identifier.  When provided a
                granular ``ruleId`` of the form ``check_name:drift_key`` is
                used so code-scanning UIs can group findings per key.
            drift_key: Dotted TOML key that drifted (e.g. ``"lint.select"``).
                Included in ``properties`` and used to derive a stable fingerprint.
        """
        artifact_uri = _path_to_artifact_uri(file_path)

        # Build a granular ruleId so findings for different keys are
        # distinguishable in code-scanning UIs (e.g. GitHub Advanced Security).
        if drift_key is not None and check_name is not None:
            rule_id = f"{check_name}:{drift_key}"
        elif check_name is not None:
            rule_id = check_name
        else:
            rule_id = self._RULE_ID

        result: SarifResult = {
            "ruleId": rule_id,
            "level": level,
            "message": {"text": message},
            "locations": [
                {
                    "physicalLocation": {
                        "artifactLocation": {"uri": artifact_uri, "uriBaseId": "%SRCROOT%"},
                        "region": {"startLine": 1},
                    }
                }
            ],
        }

        # Populate custom properties for tooling that reads SARIF property bags.
        props: dict[str, str] = {}
        if check_name is not None:
            props["check_name"] = check_name
        if drift_key is not None:
            props["drift_key"] = drift_key
        if props:
            result["properties"] = props

        # Derive a stable fingerprint so consumers can deduplicate across runs.
        # Never include timestamps or UUIDs — only stable, content-derived data.
        fingerprint = self._make_fingerprint(artifact_uri, rule_id, drift_key)
        # Use a custom SARIF fingerprint key to reflect that this is not a line-hash.
        result["fingerprints"] = {"ruff-sync-fingerprint/v1": fingerprint}

        return result

    @staticmethod
    def _make_fingerprint(artifact_uri: str, rule_id: str, drift_key: str | None) -> str:
        """Return a stable MD5 fingerprint for a SARIF result.

        The fingerprint must be deterministic (same inputs → same output on
        every pipeline run) so consumers can track introduced vs resolved
        findings.  Never include timestamps, UUIDs, or any other
        runtime-variable data.
        """
        raw = f"ruff-sync:{rule_id}:{artifact_uri}:{drift_key or ''}"
        return hashlib.md5(raw.encode()).hexdigest()  # noqa: S324

__init__

__init__()

Initialise with an empty result list.

Source code in src/ruff_sync/formatters.py
def __init__(self) -> None:
    """Initialise with an empty result list."""
    self._results: list[SarifResult] = []

note

note(message)

Delegate to logger only; not representable in the SARIF schema.

Source code in src/ruff_sync/formatters.py
def note(self, message: str) -> None:
    """Delegate to logger only; not representable in the SARIF schema."""
    LOGGER.info(message)

info

info(message, logger=None)

Delegate to logger only; not included in the structured report.

Source code in src/ruff_sync/formatters.py
def info(self, message: str, logger: logging.Logger | None = None) -> None:
    """Delegate to logger only; not included in the structured report."""
    (logger or LOGGER).info(message)

success

success(message)

Delegate to logger only; not representable in the SARIF schema.

Source code in src/ruff_sync/formatters.py
def success(self, message: str) -> None:
    """Delegate to logger only; not representable in the SARIF schema."""
    LOGGER.info(message)

error

error(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Accumulate an error-level SARIF result.

Source code in src/ruff_sync/formatters.py
def error(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Accumulate an error-level SARIF result."""
    (logger or LOGGER).error(message)
    self._results.append(
        self._make_result(
            message=message,
            level="error",
            file_path=file_path,
            check_name=check_name,
            drift_key=drift_key,
        )
    )

warning

warning(
    message,
    file_path=None,
    logger=None,
    check_name=_DEFAULT_CHECK_NAME,
    drift_key=None,
)

Accumulate a warning-level SARIF result.

Source code in src/ruff_sync/formatters.py
def warning(
    self,
    message: str,
    file_path: pathlib.Path | None = None,
    logger: logging.Logger | None = None,
    check_name: str = _DEFAULT_CHECK_NAME,
    drift_key: str | None = None,
) -> None:
    """Accumulate a warning-level SARIF result."""
    (logger or LOGGER).warning(message)
    self._results.append(
        self._make_result(
            message=message,
            level="warning",
            file_path=file_path,
            check_name=check_name,
            drift_key=drift_key,
        )
    )

debug

debug(message, logger=None)

Delegate to logger only; not included in the structured report.

Source code in src/ruff_sync/formatters.py
def debug(self, message: str, logger: logging.Logger | None = None) -> None:
    """Delegate to logger only; not included in the structured report."""
    (logger or LOGGER).debug(message)

diff

diff(diff_text)

Ignored by structured formatters — diffs are not representable in SARIF.

Source code in src/ruff_sync/formatters.py
def diff(self, diff_text: str) -> None:
    """Ignored by structured formatters — diffs are not representable in SARIF."""

finalize

finalize()

Write the collected results as a SARIF v2.1.0 document to stdout.

Rules are de-duplicated from the accumulated results so the rules list contains one entry per unique ruleId seen across all findings.

Source code in src/ruff_sync/formatters.py
def finalize(self) -> None:
    """Write the collected results as a SARIF v2.1.0 document to stdout.

    Rules are de-duplicated from the accumulated results so the ``rules``
    list contains one entry per unique ``ruleId`` seen across all findings.
    """
    seen_ids: set[str] = set()
    rules: list[dict[str, Any]] = []
    for result in self._results:
        rule_id = result["ruleId"]
        if rule_id not in seen_ids:
            seen_ids.add(rule_id)
            rules.append(
                {
                    "id": rule_id,
                    "name": "ConfigDrift",
                    "shortDescription": {
                        "text": "Ruff configuration has drifted from upstream."
                    },
                    "helpUri": "https://github.com/Kilo59/ruff-sync",
                }
            )
    if not rules:
        # No findings — emit a single placeholder rule so the SARIF document
        # is always schema-valid (a tool with zero rules is still valid, but
        # some consumers require at least one rule entry).
        rules = [
            {
                "id": self._RULE_ID,
                "name": "ConfigDrift",
                "shortDescription": {"text": "Ruff configuration has drifted from upstream."},
                "helpUri": "https://github.com/Kilo59/ruff-sync",
            }
        ]
    sarif_doc: dict[str, Any] = {
        "version": self._SARIF_VERSION,
        "$schema": self._SCHEMA,
        "runs": [
            {
                "tool": {
                    "driver": {
                        "name": "ruff-sync",
                        "informationUri": "https://github.com/Kilo59/ruff-sync",
                        "rules": rules,
                    }
                },
                "results": self._results,
            }
        ],
    }
    print(json.dumps(sarif_doc, indent=2))

get_formatter

get_formatter(output_format)

Return the corresponding formatter for the given format.

Source code in src/ruff_sync/formatters.py
def get_formatter(output_format: OutputFormat) -> ResultFormatter:
    """Return the corresponding formatter for the given format."""
    match output_format:
        case OutputFormat.TEXT:
            return TextFormatter()
        case OutputFormat.GITHUB:
            return GithubFormatter()
        case OutputFormat.JSON:
            return JsonFormatter()
        case OutputFormat.GITLAB:
            return GitlabFormatter()
        case OutputFormat.SARIF:
            return SarifFormatter()