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