# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import re from difflib import SequenceMatcher from compare_locales.parser import PropertiesEntity from compare_locales import plurals from .base import Checker class PrintfException(Exception): def __init__(self, msg, pos): self.pos = pos self.msg = msg class PropertiesChecker(Checker): """Tests to run on .properties files.""" pattern = re.compile(r".*\.properties$") printf = re.compile( r"%(?P%|" r"(?:(?P[1-9][0-9]*)\$)?" r"(?P\*|[0-9]+)?" r"(?P\.(?:\*|[0-9]+)?)?" r"(?P[duxXosScpfg]))?" ) def check(self, refEnt, l10nEnt): """Test for the different variable formats.""" yield from super().check(refEnt, l10nEnt) refValue, l10nValue = refEnt.val, l10nEnt.val refSpecs = None # check for PluralForm.jsm stuff, should have the docs in the # comment # That also includes intl.properties' pluralRule, so exclude # entities with that key and values with just numbers if ( refEnt.pre_comment and "Localization_and_Plurals" in refEnt.pre_comment.all and refEnt.key != "pluralRule" and not re.match(r"\d+$", refValue) ): yield from self.check_plural(refValue, l10nValue) return # check for lost escapes raw_val = l10nEnt.raw_val for m in PropertiesEntity.escape.finditer(raw_val): if ( m.group("single") and m.group("single") not in PropertiesEntity.known_escapes ): yield ( "warning", m.start(), "unknown escape sequence, \\" + m.group("single"), "escape", ) try: refSpecs = self.getPrintfSpecs(refValue) except PrintfException: refSpecs = [] if refSpecs: yield from self.checkPrintf(refSpecs, l10nValue) return def check_plural(self, refValue, l10nValue): """Check for the stringbundle plurals logic. The common variable pattern is #1. """ known_plurals = plurals.get_plural(self.locale) if known_plurals: expected_forms = len(known_plurals) found_forms = l10nValue.count(";") + 1 msg = f"expecting {expected_forms} plurals, found {found_forms}" if expected_forms > found_forms: yield ("warning", 0, msg, "plural") if expected_forms < found_forms: yield ("warning", 0, msg, "plural") pats = {int(m.group(1)) for m in re.finditer("#([0-9]+)", refValue)} if len(pats) == 0: return lpats = {int(m.group(1)) for m in re.finditer("#([0-9]+)", l10nValue)} if pats - lpats: yield ("warning", 0, "not all variables used in l10n", "plural") return if lpats - pats: yield ("error", 0, "unreplaced variables in l10n", "plural") def checkPrintf(self, refSpecs, l10nValue): try: l10nSpecs = self.getPrintfSpecs(l10nValue) except PrintfException as e: yield ("error", e.pos, e.msg, "printf") return if refSpecs != l10nSpecs: sm = SequenceMatcher() sm.set_seqs(refSpecs, l10nSpecs) msgs = [] warn = None for action, i1, i2, j1, j2 in sm.get_opcodes(): if action == "equal": continue if action == "delete": # missing argument in l10n if i2 == len(refSpecs): # trailing specs missing, that's just a warning warn = ", ".join( "trailing argument %d `%s` missing" % (i + 1, refSpecs[i]) for i in range(i1, i2) ) else: for i in range(i1, i2): msgs.append( "argument %d `%s` missing" % (i + 1, refSpecs[i]) ) continue if action == "insert": # obsolete argument in l10n for i in range(j1, j2): msgs.append("argument %d `%s` obsolete" % (i + 1, l10nSpecs[i])) continue if action == "replace": for i, j in zip(range(i1, i2), range(j1, j2)): msgs.append( "argument %d `%s` should be `%s`" % (j + 1, l10nSpecs[j], refSpecs[i]) ) if msgs: yield ("error", 0, ", ".join(msgs), "printf") if warn is not None: yield ("warning", 0, warn, "printf") def getPrintfSpecs(self, val): hasNumber = False specs = [] for m in self.printf.finditer(val): if m.group("good") is None: # found just a '%', signal an error raise PrintfException("Found single %", m.start()) if m.group("good") == "%": # escaped % continue if (hasNumber and m.group("number") is None) or ( not hasNumber and specs and m.group("number") is not None ): # mixed style, numbered and not raise PrintfException("Mixed ordered and non-ordered args", m.start()) hasNumber = m.group("number") is not None if hasNumber: pos = int(m.group("number")) - 1 ls = len(specs) if pos >= ls: # pad specs nones = pos - ls specs[ls:pos] = nones * [None] specs.append(m.group("spec")) else: specs[pos] = m.group("spec") else: specs.append(m.group("spec")) # check for missing args if hasNumber and not all(specs): raise PrintfException("Ordered argument missing", 0) return specs