1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import 6from __future__ import unicode_literals 7import re 8from difflib import SequenceMatcher 9from six.moves import range 10from six.moves import zip 11 12from compare_locales.parser import PropertiesEntity 13from compare_locales import plurals 14from .base import Checker 15 16 17class PrintfException(Exception): 18 def __init__(self, msg, pos): 19 self.pos = pos 20 self.msg = msg 21 22 23class PropertiesChecker(Checker): 24 '''Tests to run on .properties files. 25 ''' 26 pattern = re.compile(r'.*\.properties$') 27 printf = re.compile(r'%(?P<good>%|' 28 r'(?:(?P<number>[1-9][0-9]*)\$)?' 29 r'(?P<width>\*|[0-9]+)?' 30 r'(?P<prec>\.(?:\*|[0-9]+)?)?' 31 r'(?P<spec>[duxXosScpfg]))?') 32 33 def check(self, refEnt, l10nEnt): 34 '''Test for the different variable formats. 35 ''' 36 for encoding_trouble in super( 37 PropertiesChecker, self 38 ).check(refEnt, l10nEnt): 39 yield encoding_trouble 40 refValue, l10nValue = refEnt.val, l10nEnt.val 41 refSpecs = None 42 # check for PluralForm.jsm stuff, should have the docs in the 43 # comment 44 # That also includes intl.properties' pluralRule, so exclude 45 # entities with that key and values with just numbers 46 if (refEnt.pre_comment 47 and 'Localization_and_Plurals' in refEnt.pre_comment.all 48 and refEnt.key != 'pluralRule' 49 and not re.match(r'\d+$', refValue)): 50 for msg_tuple in self.check_plural(refValue, l10nValue): 51 yield msg_tuple 52 return 53 # check for lost escapes 54 raw_val = l10nEnt.raw_val 55 for m in PropertiesEntity.escape.finditer(raw_val): 56 if m.group('single') and \ 57 m.group('single') not in PropertiesEntity.known_escapes: 58 yield ('warning', m.start(), 59 'unknown escape sequence, \\' + m.group('single'), 60 'escape') 61 try: 62 refSpecs = self.getPrintfSpecs(refValue) 63 except PrintfException: 64 refSpecs = [] 65 if refSpecs: 66 for t in self.checkPrintf(refSpecs, l10nValue): 67 yield t 68 return 69 70 def check_plural(self, refValue, l10nValue): 71 '''Check for the stringbundle plurals logic. 72 The common variable pattern is #1. 73 ''' 74 known_plurals = plurals.get_plural(self.locale) 75 if known_plurals: 76 expected_forms = len(known_plurals) 77 found_forms = l10nValue.count(';') + 1 78 msg = 'expecting {} plurals, found {}'.format( 79 expected_forms, 80 found_forms 81 ) 82 if expected_forms > found_forms: 83 yield ('warning', 0, msg, 'plural') 84 if expected_forms < found_forms: 85 yield ('warning', 0, msg, 'plural') 86 pats = set(int(m.group(1)) for m in re.finditer('#([0-9]+)', 87 refValue)) 88 if len(pats) == 0: 89 return 90 lpats = set(int(m.group(1)) for m in re.finditer('#([0-9]+)', 91 l10nValue)) 92 if pats - lpats: 93 yield ('warning', 0, 'not all variables used in l10n', 94 'plural') 95 return 96 if lpats - pats: 97 yield ('error', 0, 'unreplaced variables in l10n', 98 'plural') 99 100 def checkPrintf(self, refSpecs, l10nValue): 101 try: 102 l10nSpecs = self.getPrintfSpecs(l10nValue) 103 except PrintfException as e: 104 yield ('error', e.pos, e.msg, 'printf') 105 return 106 if refSpecs != l10nSpecs: 107 sm = SequenceMatcher() 108 sm.set_seqs(refSpecs, l10nSpecs) 109 msgs = [] 110 warn = None 111 for action, i1, i2, j1, j2 in sm.get_opcodes(): 112 if action == 'equal': 113 continue 114 if action == 'delete': 115 # missing argument in l10n 116 if i2 == len(refSpecs): 117 # trailing specs missing, that's just a warning 118 warn = ', '.join('trailing argument %d `%s` missing' % 119 (i+1, refSpecs[i]) 120 for i in range(i1, i2)) 121 else: 122 for i in range(i1, i2): 123 msgs.append('argument %d `%s` missing' % 124 (i+1, refSpecs[i])) 125 continue 126 if action == 'insert': 127 # obsolete argument in l10n 128 for i in range(j1, j2): 129 msgs.append('argument %d `%s` obsolete' % 130 (i+1, l10nSpecs[i])) 131 continue 132 if action == 'replace': 133 for i, j in zip(range(i1, i2), range(j1, j2)): 134 msgs.append('argument %d `%s` should be `%s`' % 135 (j+1, l10nSpecs[j], refSpecs[i])) 136 if msgs: 137 yield ('error', 0, ', '.join(msgs), 'printf') 138 if warn is not None: 139 yield ('warning', 0, warn, 'printf') 140 141 def getPrintfSpecs(self, val): 142 hasNumber = False 143 specs = [] 144 for m in self.printf.finditer(val): 145 if m.group("good") is None: 146 # found just a '%', signal an error 147 raise PrintfException('Found single %', m.start()) 148 if m.group("good") == '%': 149 # escaped % 150 continue 151 if ((hasNumber and m.group('number') is None) or 152 (not hasNumber and specs and 153 m.group('number') is not None)): 154 # mixed style, numbered and not 155 raise PrintfException('Mixed ordered and non-ordered args', 156 m.start()) 157 hasNumber = m.group('number') is not None 158 if hasNumber: 159 pos = int(m.group('number')) - 1 160 ls = len(specs) 161 if pos >= ls: 162 # pad specs 163 nones = pos - ls 164 specs[ls:pos] = nones*[None] 165 specs.append(m.group('spec')) 166 else: 167 specs[pos] = m.group('spec') 168 else: 169 specs.append(m.group('spec')) 170 # check for missing args 171 if hasNumber and not all(specs): 172 raise PrintfException('Ordered argument missing', 0) 173 return specs 174