#!/usr/bin/env python3
#
# Check gcc.pot file for stylistic issues as described in
# https://gcc.gnu.org/onlinedocs/gccint/Guidelines-for-Diagnostics.html,
# especially in gcc-internal-format messages.
#
# This file is part of GCC.
#
# GCC is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 3, or (at your option) any later
# version.
#
# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with GCC; see the file COPYING3. If not see
# <http://www.gnu.org/licenses/>.
import argparse
import re
from collections import Counter
from typing import Dict, Match
import polib
seen_warnings = Counter()
def location(msg: polib.POEntry):
if msg.occurrences:
occ = msg.occurrences[0]
return f'{occ[0]}:{occ[1]}'
return '<unknown location>'
def warn(msg: polib.POEntry,
diagnostic_id: str, diagnostic: str, include_msgid=True):
"""
To suppress a warning for a particular message,
add a line "#, gcclint:ignore:{diagnostic_id}" to the message.
"""
if f'gcclint:ignore:{diagnostic_id}' in msg.flags:
return
seen_warnings[diagnostic] += 1
if include_msgid:
print(f'{location(msg)}: {diagnostic} in {repr(msg.msgid)}')
else:
print(f'{location(msg)}: {diagnostic}')
def lint_gcc_internal_format(msg: polib.POEntry):
"""
Checks a single message that has the gcc-internal-format. These
messages use a variety of placeholders like %qs, %<quotes%> and
%q#E.
"""
msgid: str = msg.msgid
def outside_quotes(m: Match[str]):
before = msgid[:m.start(0)]
return before.count("%<") == before.count("%>")
def lint_matching_placeholders():
"""
Warns when literal values in placeholders are not exactly equal
in the translation. This can happen when doing copy-and-paste
translations of similar messages.
To avoid these mismatches in the first place,
structurally equal messages are found by
lint_diagnostics_differing_only_in_placeholders.
This check only applies when checking a finished translation
such as de.po, not gcc.pot.
"""
if not msg.translated():
return
in_msgid = re.findall('%<[^%]+%>', msgid)
in_msgstr = re.findall('%<[^%]+%>', msg.msgstr)
if set(in_msgid) != set(in_msgstr):
warn(msg,
'placeholder-mismatch',
f'placeholder mismatch: msgid has {in_msgid}, '
f'msgstr has {in_msgstr}',
include_msgid=False)
def lint_option_outside_quotes():
for match in re.finditer(r'\S+', msgid):
part = match.group()
if not outside_quotes(match):
continue
if part.startswith('-'):
if len(part) >= 2 and part[1].isalpha():
if part == '-INF':
continue
warn(msg,
'option-outside-quotes',
'command line option outside %<quotes%>')
if part.startswith('__builtin_'):
warn(msg,
'builtin-outside-quotes',
'builtin function outside %<quotes%>')
def lint_plain_apostrophe():
for match in re.finditer("[^%]'", msgid):
if outside_quotes(match):
warn(msg, 'apostrophe', 'apostrophe without leading %')
def lint_space_before_quote():
"""
A space before %< is often the result of string literals that
are joined by the C compiler and neither literal has a space
to separate the words.
"""
for match in re.finditer("(.?[a-zA-Z0-9])%<", msgid):
if match.group(1) != '%s':
warn(msg,
'no-space-before-quote',
'%< directly following a letter or digit')
def lint_underscore_outside_quotes():
"""
An underscore outside of quotes is used in several contexts,
and many of them violate the GCC Guidelines for Diagnostics:
* names of GCC-internal compiler functions
* names of GCC-internal data structures
* static_cast and the like (which are legitimate)
"""
for match in re.finditer("_", msgid):
if outside_quotes(match):
warn(msg,
'underscore-outside-quotes',
'underscore outside of %<quotes%>')
return
def lint_may_not():
"""
The term "may not" may either mean "it could be the case"
or "should not". These two different meanings are sometimes
hard to tell apart.
"""
if re.search(r'\bmay not\b', msgid):
warn(msg,
'ambiguous-may-not',
'the term "may not" is ambiguous')
def lint_unbalanced_quotes():
if msgid.count("%<") != msgid.count("%>"):
warn(msg,
'unbalanced-quotes',
'unbalanced %< and %> quotes')
if msg.translated():
if msg.msgstr.count("%<") != msg.msgstr.count("%>"):
warn(msg,
'unbalanced-quotes',
'unbalanced %< and %> quotes')
def lint_single_space_after_sentence():
"""
After a sentence there should be two spaces.
"""
if re.search(r'[.] [A-Z]', msgid):
warn(msg,
'single-space-after-sentence',
'single space after sentence')
def lint_non_canonical_quotes():
"""
Catches %<%s%>, which can be written in the shorter form %qs.
"""
match = re.search("%<%s%>|'%s'|\"%s\"|`%s'", msgid)
if match:
warn(msg,
'non-canonical-quotes',
f'placeholder {match.group()} should be written as %qs')
lint_option_outside_quotes()
lint_plain_apostrophe()
lint_space_before_quote()
lint_underscore_outside_quotes()
lint_may_not()
lint_unbalanced_quotes()
lint_matching_placeholders()
lint_single_space_after_sentence()
lint_non_canonical_quotes()
def lint_diagnostics_differing_only_in_placeholders(po: polib.POFile):
"""
Detects messages that are structurally the same, except that they
use different plain strings inside %<quotes%>. These messages can
be merged in order to prevent copy-and-paste mistakes by the
translators.
See bug 90119.
"""
seen: Dict[str, polib.POEntry] = {}
for msg in po:
msg: polib.POEntry
msgid = msg.msgid
normalized = re.sub('%<[^%]+%>', '%qs', msgid)
if normalized not in seen:
seen[normalized] = msg
seen[msgid] = msg
continue
prev = seen[normalized]
warn(msg,
'same-pattern',
f'same pattern for {repr(msgid)} and '
f'{repr(prev.msgid)} in {location(prev)}',
include_msgid=False)
def lint_file(po: polib.POFile):
for msg in po:
msg: polib.POEntry
if not msg.obsolete and not msg.fuzzy:
if 'gcc-internal-format' in msg.flags:
lint_gcc_internal_format(msg)
lint_diagnostics_differing_only_in_placeholders(po)
def main():
parser = argparse.ArgumentParser(description='')
parser.add_argument('file', help='pot file')
args = parser.parse_args()
po = polib.pofile(args.file)
lint_file(po)
print()
print('summary:')
for entry in seen_warnings.most_common():
if entry[1] > 1:
print(f'{entry[1]}\t{entry[0]}')
if __name__ == '__main__':
main()