Source code for rude.rules.pyflakes.literals

"""
String literal rules.

F541: FStringMissingPlaceholders - f-string without placeholders
F542: TStringMissingPlaceholders - t-string without placeholders (Python 3.14+)
"""

from __future__ import annotations

from collections.abc import Iterator
from typing import TYPE_CHECKING, ClassVar

from rude.core.node_types import NodeType
from rude.core.rule import Rule
from rude.core.types import Diagnostic, Fix, Severity

if TYPE_CHECKING:
    from rude.core.node import Node


class _PrefixStringMissingPlaceholders(Rule):
    """Base for f-string/t-string missing placeholder rules."""

    _PREFIX_CHAR: ClassVar[str]
    severity: ClassVar[Severity] = Severity.WARNING
    node_types = {NodeType.STRING}

    def check(self, node: Node) -> Iterator[Diagnostic]:
        text = node.text

        if not self._has_prefix(text):
            return

        # Check if it has any interpolation children (direct children only)
        has_placeholder = any(c.type == "interpolation" for c in node.children)

        if not has_placeholder:
            new_text = self._remove_prefix(text)
            yield self.diagnostic(
                node,
                fix=Fix.replace(
                    node, new_text, description=f"Remove {self._PREFIX_CHAR}-string prefix"
                ),
            )

    def _has_prefix(self, text: str) -> bool:
        """Check if string literal has the expected prefix."""
        lower = text.lstrip()[:3].lower()
        p = self._PREFIX_CHAR
        return lower.startswith(p) or lower.startswith(f"r{p}") or lower.startswith(f"{p}r")

    def _remove_prefix(self, text: str) -> str:
        """Remove the prefix character from string literal."""
        stripped = text.lstrip()
        lower = stripped[:3].lower()
        p = self._PREFIX_CHAR

        if lower.startswith(f"r{p}"):
            # rX"..." -> r"..."
            return stripped[0] + stripped[2:]
        elif lower.startswith(f"{p}r"):
            # Xr"..." -> r"..."
            return stripped[1:]
        elif lower.startswith(p):
            # X"..." -> "..."
            return stripped[1:]
        return text


[docs] class FStringMissingPlaceholders(_PrefixStringMissingPlaceholders): """ F541: f-string without any placeholders. An f-string without placeholders is probably a mistake - the `f` prefix should be removed. Example:: x = f"hello" # F541 - no placeholders x = f"hello {x}" # OK - has placeholder x = "hello" # OK - regular string """ code: ClassVar[str] = "F541" message: ClassVar[str] = "f-string without any placeholders" _PREFIX_CHAR: ClassVar[str] = "f"
[docs] class TStringMissingPlaceholders(_PrefixStringMissingPlaceholders): """ F542: t-string without any placeholders. Rationale: A t-string without placeholders adds overhead for no benefit. Remove the ``t`` prefix to use a regular string. Example:: # Bad x = t"hello" # F542 - no placeholders # Good x = t"hello {name}" x = "hello" # plain string """ code: ClassVar[str] = "F542" message: ClassVar[str] = "t-string without any placeholders" _PREFIX_CHAR: ClassVar[str] = "t"
LITERAL_RULES = [ FStringMissingPlaceholders, TStringMissingPlaceholders, ] __all__ = [ "LITERAL_RULES", "FStringMissingPlaceholders", "TStringMissingPlaceholders", ]