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