Source code
Revision control
Copy as Markdown
Other Tools
# 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
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<good>%|'
r'(?:(?P<number>[1-9][0-9]*)\$)?'
r'(?P<width>\*|[0-9]+)?'
r'(?P<prec>\.(?:\*|[0-9]+)?)?'
r'(?P<spec>[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 = 'expecting {} plurals, found {}'.format(
expected_forms,
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