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/.
4from __future__ import absolute_import
5from __future__ import unicode_literals
6
7from collections import Counter
8import os
9
10from compare_locales import parser, checks
11from compare_locales.paths import File, REFERENCE_LOCALE
12
13
14class L10nLinter(object):
15
16    def lint(self, files, get_reference_and_tests):
17        results = []
18        for path in files:
19            if not parser.hasParser(path):
20                continue
21            ref, extra_tests = get_reference_and_tests(path)
22            results.extend(self.lint_file(path, ref, extra_tests))
23        return results
24
25    def lint_file(self, path, ref, extra_tests):
26        file_parser = parser.getParser(path)
27        if ref is not None and os.path.isfile(ref):
28            file_parser.readFile(ref)
29            reference = file_parser.parse()
30        else:
31            reference = {}
32        file_parser.readFile(path)
33        current = file_parser.parse()
34        checker = checks.getChecker(
35            File(path, path, locale=REFERENCE_LOCALE),
36            extra_tests=extra_tests
37        )
38        if checker and checker.needs_reference:
39            checker.set_reference(current)
40        linter = EntityLinter(current, checker, reference)
41        for current_entity in current:
42            for result in linter.lint_entity(current_entity):
43                result['path'] = path
44                yield result
45
46
47class EntityLinter(object):
48    '''Factored out helper to run linters on a single entity.'''
49    def __init__(self, current, checker, reference):
50        self.key_count = Counter(entity.key for entity in current)
51        self.checker = checker
52        self.reference = reference
53
54    def lint_entity(self, current_entity):
55        res = self.handle_junk(current_entity)
56        if res:
57            yield res
58            return
59        for res in self.lint_full_entity(current_entity):
60            yield res
61        for res in self.lint_value(current_entity):
62            yield res
63
64    def lint_full_entity(self, current_entity):
65        '''Checks that go good or bad for a full entity,
66        without a particular spot inside the entity.
67        '''
68        lineno = col = None
69        if self.key_count[current_entity.key] > 1:
70            lineno, col = current_entity.position()
71            yield {
72                'lineno': lineno,
73                'column': col,
74                'level': 'error',
75                'message': 'Duplicate string with ID: {}'.format(
76                    current_entity.key
77                )
78            }
79
80        if current_entity.key in self.reference:
81            reference_entity = self.reference[current_entity.key]
82            if not current_entity.equals(reference_entity):
83                if lineno is None:
84                    lineno, col = current_entity.position()
85                msg = 'Changes to string require a new ID: {}'.format(
86                    current_entity.key
87                )
88                yield {
89                    'lineno': lineno,
90                    'column': col,
91                    'level': 'warning',
92                    'message': msg,
93                }
94
95    def lint_value(self, current_entity):
96        '''Checks that error on particular locations in the entity value.
97        '''
98        if self.checker:
99            for tp, pos, msg, cat in self.checker.check(
100                current_entity, current_entity
101            ):
102                if isinstance(pos, checks.EntityPos):
103                    lineno, col = current_entity.position(pos)
104                else:
105                    lineno, col = current_entity.value_position(pos)
106                yield {
107                    'lineno': lineno,
108                    'column': col,
109                    'level': tp,
110                    'message': msg,
111                }
112
113    def handle_junk(self, current_entity):
114        if not isinstance(current_entity, parser.Junk):
115            return None
116
117        lineno, col = current_entity.position()
118        return {
119            'lineno': lineno,
120            'column': col,
121            'level': 'error',
122            'message': current_entity.error_message()
123        }
124