Skip to content

Commit

Permalink
feat(checks): detect extra curly braces in Python brace format
Browse files Browse the repository at this point in the history
Fixes #12197
  • Loading branch information
nijel committed Aug 5, 2024
1 parent f86d7ae commit c006da8
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Not yet released.
* Browse mode can now be navigated using keyboard, see :ref:`keyboard`.
* :http:get:`/api/components/(string:project)/(string:component)/credits/` and :http:get:`/api/projects/(string:project)/credits/` API endpoints for components and projects.
* :ref:`glossary-terminology` entries in Glossary can now only be created by users with :guilabel:`Add glossary terminology` permission.
* :ref:`check-python-brace-format` detects extra curly braces.

**Bug fixes**

Expand Down
37 changes: 33 additions & 4 deletions weblate/checks/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
from collections import Counter, defaultdict
from re import Pattern
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

from django.utils.functional import SimpleLazyObject
from django.utils.html import format_html_join
Expand All @@ -17,6 +17,8 @@
from weblate.checks.base import SourceCheck, TargetCheck

if TYPE_CHECKING:
from collections.abc import Iterable

from weblate.trans.models import Unit

PYTHON_PRINTF_MATCH = re.compile(
Expand Down Expand Up @@ -276,7 +278,9 @@ def check_target_unit(self, sources: list[str], targets: list[str], unit: Unit):
"""Check single unit, handling plurals."""
return any(self.check_generator(sources, targets, unit))

def check_generator(self, sources: list[str], targets: list[str], unit: Unit):
def check_generator(
self, sources: list[str], targets: list[str], unit: Unit
) -> Iterable[Literal[False] | dict[Literal["missing", "extra"], list[str]]]:
# Special case languages with single plural form
if len(sources) > 1 and len(targets) == 1:
yield self.check_format(sources[1], targets[0], False, unit)
Expand Down Expand Up @@ -340,7 +344,9 @@ def normalize(self, matches: list[str]) -> list[str]:
def extract_matches(self, string: str) -> list[str]:
return [self.cleanup_string(x[0]) for x in self.regexp.findall(string)]

def check_format(self, source: str, target: str, ignore_missing, unit: Unit):
def check_format(
self, source: str, target: str, ignore_missing: bool, unit: Unit
) -> Literal[False] | dict[Literal["missing", "extra"], list[str]]:
"""Check for format strings."""
if not target or not source:
return False
Expand Down Expand Up @@ -577,6 +583,27 @@ def is_position_based(self, string: str):
def format_string(self, string: str) -> str:
return f"{{{string}}}"

def check_format(
self, source: str, target: str, ignore_missing: bool, unit: Unit
) -> Literal[False] | dict[Literal["missing", "extra"], list[str]]:
result = super().check_format(source, target, ignore_missing, unit)
noquoted = target.replace("{{", "").replace("}}", "")

opening = noquoted.count("{")
closing = noquoted.count("}")

add_extra: str | None = None
if opening > closing or closing > opening:
add_extra = "{"

if add_extra is not None:
if isinstance(result, dict):
result["extra"].append(add_extra)
else:
result = {"missing": [], "extra": [add_extra]}

return result


class CSharpFormatCheck(BaseFormatCheck):
"""Check for C# format string."""
Expand Down Expand Up @@ -622,7 +649,9 @@ def should_skip(self, unit: Unit):

return super().should_skip(unit)

def check_format(self, source: str, target: str, ignore_missing, unit: Unit):
def check_format(
self, source: str, target: str, ignore_missing: bool, unit: Unit
) -> Literal[False] | dict[Literal["missing", "extra"], list[str]]:
"""Check for format strings."""
if not target or not source:
return False
Expand Down
18 changes: 18 additions & 0 deletions weblate/checks/tests/test_format_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,24 @@ def test_wrong_attribute_format(self) -> None:
self.check.check_format("{s.foo} string", "{s.bar} string", False, None)
)

def test_extra_close_bracket(self) -> None:
self.assertTrue(
self.check.check_format("{s} string", "{s}} string", False, None)
)

def test_extra_open_bracket(self) -> None:
self.assertTrue(
self.check.check_format("{s} string", "{ {s} string", False, None)
)

def test_extra_open_bracket_extra(self) -> None:
self.assertTrue(self.check.check_format("string", "{ {s} string", False, None))

def test_escape_bracket(self) -> None:
self.assertFalse(
self.check.check_format("{{ {{ {s} }}", "{{ {{ {s} }}", False, None)
)


class CSharpFormatCheckTest(CheckTestCase):
check = CSharpFormatCheck()
Expand Down

0 comments on commit c006da8

Please sign in to comment.