def check_string(self, ctx, message, s): prefix = message_repr(message, template='{}:') fmt = None try: fmt = backend.FormatString(s) except backend.ArgumentTypeMismatch as exc: [s, key, types] = exc.args # pylint: disable=unbalanced-tuple-unpacking self.tag( 'python-format-string-error', prefix, tags.safestr(exc.message), tags.safestr(key), tags.safestr(', '.join(sorted(x for x in types))), ) except backend.Error as exc: self.tag('python-format-string-error', prefix, tags.safestr(exc.message), *exc.args[:1]) if fmt is None: return for warn in fmt.warnings: try: raise warn except backend.RedundantFlag as exc: if len(exc.args) == 2: [s, *args] = exc.args else: [s, a1, a2] = exc.args if a1 == a2: args = ['duplicate', a1] else: args = [a1, tags.safe_format('overridden by {}', a2)] args += ['in', s] self.tag('python-format-string-redundant-flag', prefix, *args) except backend.RedundantPrecision as exc: [s, a] = exc.args self.tag('python-format-string-redundant-precision', prefix, a, 'in', s) except backend.RedundantLength as exc: [s, a] = exc.args self.tag('python-format-string-redundant-length', prefix, a, 'in', s) except backend.ObsoleteConversion as exc: [s, c1, c2] = exc.args args = [c1, '=>', c2] if s != c1: args += ['in', s] self.tag('python-format-string-obsolete-conversion', prefix, *args) if ctx.is_template: if len(fmt.seq_conversions) > 1: self.tag('python-format-string-multiple-unnamed-arguments', message_repr(message)) elif len(fmt.seq_conversions) == 1: arg_for_plural = (message.msgid_plural is not None and fmt.seq_conversions[0].type == 'int') if arg_for_plural: self.tag('python-format-string-unnamed-plural-argument', message_repr(message)) return fmt
def check_msgids(self, message, msgid_fmts): if msgid_fmts.get(0) is not None: try: [[arg]] = msgid_fmts[0].arguments except ValueError: pass else: if arg.type == 'int *': self.tag('qt-plural-format-mistaken-for-c-format', message_repr(message))
def check_string(self, ctx, message, s): prefix = message_repr(message, template='{}:') fmt = None try: fmt = backend.FormatString(s) except backend.MissingArgument as exc: self.tag( 'c-format-string-error', prefix, tags.safestr(exc.message), tags.safestr('{1}$'.format(*exc.args)), ) except backend.ArgumentTypeMismatch as exc: self.tag( 'c-format-string-error', prefix, tags.safestr(exc.message), tags.safestr('{1}$'.format(*exc.args)), tags.safestr(', '.join(sorted(x for x in exc.args[2]))), ) except backend.FlagError as exc: [conv, flag] = exc.args # pylint: disable=unbalanced-tuple-unpacking self.tag('c-format-string-error', prefix, tags.safestr(exc.message), flag, tags.safestr('in'), conv) except backend.Error as exc: self.tag('c-format-string-error', prefix, tags.safestr(exc.message), *exc.args[:1]) if fmt is None: return for warn in fmt.warnings: try: raise warn except backend.RedundantFlag as exc: if len(exc.args) == 2: [s, *args] = exc.args else: [s, a1, a2] = exc.args if a1 == a2: args = ['duplicate', a1] else: args = [a1, tags.safe_format('overridden by {}', a2)] args += ['in', s] self.tag('c-format-string-redundant-flag', prefix, *args) except backend.NonPortableConversion as exc: [s, c1, c2] = exc.args args = [c1, '=>', c2] if s != c1: args += ['in', s] self.tag('c-format-string-non-portable-conversion', prefix, *args) return fmt
def check_args(self, message, src_loc, src_fmt, dst_loc, dst_fmt, *, omitted_int_conv_ok=False): prefix = message_repr(message, template='{}:') src_args = src_fmt.arguments dst_args = dst_fmt.arguments if len(dst_args) > len(src_args): self.tag( 'c-format-string-excess-arguments', prefix, len(dst_args), tags.safestr('({})'.format(dst_loc)), '>', len(src_args), tags.safestr('({})'.format(src_loc)), ) elif len(dst_args) < len(src_args): if omitted_int_conv_ok: n_args_omitted = len(src_args) - len(dst_args) omitted_int_conv_ok = src_fmt.get_last_integer_conversion( n=n_args_omitted) if not omitted_int_conv_ok: self.tag( 'c-format-string-missing-arguments', prefix, len(dst_args), tags.safestr('({})'.format(dst_loc)), '<', len(src_args), tags.safestr('({})'.format(src_loc)), ) for src_arg, dst_arg in zip(src_args, dst_args): src_arg = src_arg[0] dst_arg = dst_arg[0] if src_arg.type != dst_arg.type: self.tag( 'c-format-string-argument-type-mismatch', prefix, tags.safestr(dst_arg.type), tags.safestr('({})'.format(dst_loc)), '!=', tags.safestr(src_arg.type), tags.safestr('({})'.format(src_loc)), )
def _check_message_xml_format(self, ctx, message, flags): if ctx.encoding is None: return prefix = message_repr(message, template='{}:') try: xml.check_fragment(message.msgid) except xml.SyntaxError as exc: if ctx.is_template: self.tag('malformed-xml', prefix, tags.safestr(exc)) return if flags.fuzzy: return if not message.msgstr: return try: xml.check_fragment(message.msgstr) except xml.SyntaxError as exc: self.tag('malformed-xml', prefix, tags.safestr(exc))
def _check_message_flags(self, message): info = misc.Namespace() info.fuzzy = False info.range_min = 0 info.range_max = 1e999 # +inf info.formats = None flags = collections.Counter(message.flags) wrap = None format_flags = collections.defaultdict(dict) range_flags = collections.defaultdict(collections.Counter) for flag, n in sorted(flags.items()): known_flag = True if flag == 'fuzzy': info.fuzzy = True elif flag in {'wrap', 'no-wrap'}: new_wrap = flag == 'wrap' if wrap == (not new_wrap): self.tag('conflicting-message-flags', message_repr(message, template='{}:'), 'wrap', 'no-wrap') else: wrap = new_wrap elif flag.startswith('range:'): if message.msgid_plural is None: self.tag('range-flag-without-plural-string') match = re.match(r'\A([0-9]+)[.][.]([0-9]+)\Z', flag[6:].strip(' \t\r\f\v')) if match is not None: i, j = map(int, match.groups()) if i < j: info.range_min = i info.range_max = j else: match = None if match is None: self.tag('invalid-range-flag', message_repr(message, template='{}:'), flag) else: range_flags[i, j][flag] += n n = 0 elif flag.endswith('-format'): known_flag = False for prefix in 'no-', 'possible-', 'impossible-', '': tp = prefix.rstrip('-') if not flag.startswith(prefix): continue string_format = flag[len(prefix):-7] if string_format in gettext.string_formats: known_flag = True format_flags[tp][string_format] = flag break else: known_flag = False if not known_flag: self.tag('unknown-message-flag', message_repr(message, template='{}:'), flag) if n > 1 and flag: self.tag('duplicate-message-flag', message_repr(message, template='{}:'), flag) if len(range_flags) > 1: [range1, range2] = heapq.nsmallest(2, range_flags.keys()) self.tag( 'conflicting-message-flags', message_repr(message, template='{}:'), min(range_flags[range1].keys()), min(range_flags[range2].keys()), ) elif len(range_flags) == 1: [range_flags] = range_flags.values() if sum(range_flags.values()) > 1: self.tag('duplicate-message-flag', message_repr(message, template='{}:'), min(range_flags.keys())) positive_format_flags = format_flags[''] info.formats = frozenset(positive_format_flags) for fmt1, flag1 in sorted(positive_format_flags.items()): for fmt2, flag2 in sorted(positive_format_flags.items()): if fmt1 >= fmt2: continue fmt_ex1 = gettext.string_formats[fmt1] fmt_ex2 = gettext.string_formats[fmt2] if fmt_ex1 & fmt_ex2: # the formats are, at least to some extent, compatible continue self.tag('conflicting-message-flags', message_repr(message, template='{}:'), flag1, flag2) for positive_key, negative_key in [('', 'no'), ('', 'impossible'), ('possible', 'impossible')]: positive_format_flags = format_flags[positive_key] negative_format_flags = format_flags[negative_key] conflicting_formats = frozenset(positive_format_flags) & frozenset( negative_format_flags) for fmt in sorted(conflicting_formats): self.tag( 'conflicting-message-flags', message_repr(message, template='{}:'), positive_format_flags[fmt], negative_format_flags[fmt], ) positive_format_flags = format_flags[''] possible_format_flags = format_flags['possible'] redundant_formats = frozenset(positive_format_flags) & frozenset( possible_format_flags) for fmt in sorted(redundant_formats): self.tag( 'redundant-message-flag', message_repr(message, template='{}:'), possible_format_flags[fmt], tags.safe_format('(implied by {flag})'.format( flag=positive_format_flags[fmt]))) return info
def check_messages(self, ctx): found_unusual_characters = set() msgid_counter = collections.Counter() for message in ctx.file: if message.obsolete: continue if is_header_entry(message): continue flags = self._check_message_flags(message) self._check_message_formats(ctx, message, flags) msgid_counter[message.msgid, message.msgctxt] += 1 if msgid_counter[message.msgid, message.msgctxt] == 2: self.tag('duplicate-message-definition', message_repr(message)) has_msgstr = bool(message.msgstr) has_msgstr_plural = any(message.msgstr_plural.values()) if ctx.is_template: if has_msgstr or has_msgstr_plural: self.tag('translation-in-template', message_repr(message)) leading_lf = message.msgid.startswith('\n') trailing_lf = message.msgid.endswith('\n') has_previous_msgid = any(s is not None for s in [ message.previous_msgctxt, message.previous_msgid, message.previous_msgid_plural, ]) if has_previous_msgid and not flags.fuzzy: self.tag('stray-previous-msgid', message_repr(message)) strings = [] if message.msgid_plural is not None: strings += [message.msgid_plural] if not flags.fuzzy: if has_msgstr: strings += [message.msgstr] if has_msgstr_plural: strings += message.msgstr_plural.values( ) # the order doesn't matter here for s in strings: if s.startswith('\n') != leading_lf: self.tag('inconsistent-leading-newlines', message_repr(message)) break for s in strings: if s.endswith('\n') != trailing_lf: self.tag('inconsistent-trailing-newlines', message_repr(message)) break strings = [] if has_msgstr: strings += [message.msgstr] if has_msgstr_plural: strings += misc.sorted_vk(message.msgstr_plural) if ctx.encoding is not None: msgid_uc = (set(find_unusual_characters(message.msgid)) | set( find_unusual_characters(message.msgid_plural or ''))) for msgstr in strings: msgstr_uc = set(find_unusual_characters(msgstr)) uc = msgstr_uc - msgid_uc - found_unusual_characters if not uc: continue names = ', '.join('U+{:04X} {}'.format( ord(ch), encinfo.get_character_name(ch)) for ch in sorted(uc)) self.tag('unusual-character-in-translation', message_repr(message, template='{}:'), tags.safestr(names)) found_unusual_characters |= uc if not flags.fuzzy: for msgstr in strings: conflict_marker = gettext.search_for_conflict_marker( msgstr) if conflict_marker is not None: conflict_marker = conflict_marker.group(0) self.tag('conflict-marker-in-translation', message_repr(message), conflict_marker) break if has_msgstr_plural and not all( message.msgstr_plural.values()): self.tag('partially-translated-message', message_repr(message)) if len(msgid_counter) == 0: possible_hidden_strings = False if isinstance(ctx.file, polib.MOFile): possible_hidden_strings = ctx.file.possible_hidden_strings if not possible_hidden_strings: self.tag('empty-file')
def check_plurals(self, ctx): ctx.plural_preimage = None plural_forms = ctx.metadata['Plural-Forms'] if len(plural_forms) > 1: self.tag('duplicate-header-field-plural-forms') plural_forms = sorted(set(plural_forms)) if len(plural_forms) > 1: return if len(plural_forms) == 1: [plural_forms] = plural_forms else: assert len(plural_forms) == 0 plural_forms = None correct_plural_forms = None if ctx.language is not None: correct_plural_forms = ctx.language.get_plural_forms() has_plurals = False # messages with plural forms (translated or not)? expected_nplurals = {} # number of plurals in _translated_ messages for message in ctx.file: if message.obsolete: continue if message.msgid_plural is not None: has_plurals = True if not message.translated(): continue expected_nplurals[len(message.msgstr_plural)] = message if len(expected_nplurals) > 1: break if len(expected_nplurals) > 1: args = [] for n, message in sorted(expected_nplurals.items()): args += [n, message_repr(message, template='({})'), '!='] self.tag('inconsistent-number-of-plural-forms', *args[:-1]) if ctx.is_template: plural_forms_hint = 'nplurals=INTEGER; plural=EXPRESSION;' elif correct_plural_forms: plural_forms_hint = tags.safe_format( ' or '.join('{}' for s in correct_plural_forms), *correct_plural_forms) else: plural_forms_hint = 'nplurals=<n>; plural=<expression>' if plural_forms is None: if has_plurals: if expected_nplurals: self.tag('no-required-plural-forms-header-field', plural_forms_hint) else: self.tag('no-plural-forms-header-field', plural_forms_hint) return if ctx.is_template: return try: (n, expr, ljunk, rjunk) = gettext.parse_plural_forms(plural_forms, strict=False) except gettext.PluralFormsSyntaxError: if has_plurals: self.tag('syntax-error-in-plural-forms', plural_forms, '=>', plural_forms_hint) else: self.tag('syntax-error-in-unused-plural-forms', plural_forms, '=>', plural_forms_hint) return if ljunk: self.tag('leading-junk-in-plural-forms', ljunk) if rjunk: self.tag('trailing-junk-in-plural-forms', rjunk) if len(expected_nplurals) == 1: [expected_nplurals] = expected_nplurals.keys() if n != expected_nplurals: self.tag('incorrect-number-of-plural-forms', n, tags.safestr('(Plural-Forms header field)'), '!=', expected_nplurals, tags.safestr('(number of msgstr items)')) locally_correct_n = locally_correct_expr = None if correct_plural_forms is not None: locally_correct_plural_forms = [ (i, expression) for i, expression in map( gettext.parse_plural_forms, correct_plural_forms) if i == n ] if not locally_correct_plural_forms: if has_plurals: self.tag('unusual-plural-forms', plural_forms, '=>', plural_forms_hint) else: self.tag('unusual-unused-plural-forms', plural_forms, '=>', plural_forms_hint) elif len(locally_correct_plural_forms) == 1: [[locally_correct_n, locally_correct_expr]] = locally_correct_plural_forms plural_preimage = collections.defaultdict(list) unusual_plural_forms = False codomain_limit = 200 try: for i in range(codomain_limit): fi = expr(i) if fi >= n: message = tags.safe_format('f({}) = {} >= {}'.format( i, fi, n)) if has_plurals: self.tag('codomain-error-in-plural-forms', message) else: self.tag('codomain-error-in-unused-plural-forms', message) break plural_preimage[fi] += [i] if (n == locally_correct_n) and (fi != locally_correct_expr(i)) and ( not unusual_plural_forms): if has_plurals: self.tag('unusual-plural-forms', plural_forms, '=>', plural_forms_hint) else: self.tag('unusual-unused-plural-forms', plural_forms, '=>', plural_forms_hint) unusual_plural_forms = True else: ctx.plural_preimage = dict(plural_preimage) except OverflowError: message = tags.safe_format('f({}): integer overflow', i) if has_plurals: self.tag('arithmetic-error-in-plural-forms', message) else: self.tag('arithmetic-error-in-unused-plural-forms', message) except ZeroDivisionError: message = tags.safe_format('f({}): division by zero', i) if has_plurals: self.tag('arithmetic-error-in-plural-forms', message) else: self.tag('arithmetic-error-in-unused-plural-forms', message) codomain = expr.codomain() if codomain is not None: (x, y) = codomain uncov_rngs = [] if x > 0: uncov_rngs += [range(0, x)] if y + 1 < n: uncov_rngs += [range(y + 1, n)] if (not uncov_rngs) and (ctx.plural_preimage is not None): period = expr.period() if period is None: period = (0, 1e999) if sum(period) < codomain_limit: for i in sorted(ctx.plural_preimage): if (i > 0) and (i - 1 not in ctx.plural_preimage): uncov_rngs += [range(i - 1, i)] break if (i + 1 < n) and (i + 1 not in ctx.plural_preimage): uncov_rngs += [range(i + 1, i + 2)] break for rng in uncov_rngs: rng = misc.format_range(rng, max=5) message = tags.safestr('f(x) != {}'.format(rng)) if has_plurals: self.tag('codomain-error-in-plural-forms', message) else: self.tag('codomain-error-in-unused-plural-forms', message) ctx.plural_preimage = None
def check_args(self, message, src_loc, src_fmt, dst_loc, dst_fmt, *, omitted_int_conv_ok=False): prefix = message_repr(message, template='{}:') # unnamed arguments: src_args = src_fmt.seq_arguments dst_args = dst_fmt.seq_arguments if len(dst_args) != len(src_args): self.tag( 'python-format-string-argument-number-mismatch', prefix, len(dst_args), tags.safestr('({})'.format(dst_loc)), '!=', len(src_args), tags.safestr('({})'.format(src_loc)), ) for src_arg, dst_arg in zip(src_args, dst_args): if src_arg.type != dst_arg.type: self.tag( 'python-format-string-argument-type-mismatch', prefix, tags.safestr(dst_arg.type), tags.safestr('({})'.format(dst_loc)), '!=', tags.safestr(src_arg.type), tags.safestr('({})'.format(src_loc)), ) # named arguments: src_args = src_fmt.map_arguments dst_args = dst_fmt.map_arguments for key in sorted(dst_args.keys() & src_args.keys()): src_arg = src_args[key][0] dst_arg = dst_args[key][0] if src_arg.type != dst_arg.type: self.tag( 'python-format-string-argument-type-mismatch', prefix, tags.safestr(dst_arg.type), tags.safestr('({})'.format(dst_loc)), '!=', tags.safestr(src_arg.type), tags.safestr('({})'.format(src_loc)), ) for key in sorted(dst_args.keys() - src_args.keys()): self.tag( 'python-format-string-unknown-argument', prefix, key, tags.safestr('in'), tags.safestr(dst_loc), tags.safestr('but not in'), tags.safestr(src_loc), ) missing_keys = src_args.keys() - dst_args.keys() if len(missing_keys) == 1 and omitted_int_conv_ok: [missing_key] = missing_keys if all(arg.type == 'int' for arg in src_args[missing_key]): missing_keys = set() for key in sorted(missing_keys): self.tag( 'python-format-string-missing-argument', prefix, key, tags.safestr('not in'), tags.safestr(dst_loc), tags.safestr('while in'), tags.safestr(src_loc), )