From: Bruno Haible Date: Thu, 3 Jul 2025 14:56:01 +0000 (+0200) Subject: xgettext: Check msgid and msgid_plural for compatibility as format strings. X-Git-Tag: v0.26~24 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=94906022d9ffdebb4aacbb894de8076a9a917e31;p=thirdparty%2Fgettext.git xgettext: Check msgid and msgid_plural for compatibility as format strings. * gettext-tools/src/if-error.h: Include . (if_verror): New declaration. * gettext-tools/src/if-error.c (if_verror): New function, extracted from if_error. (if_error): Invoke it. * gettext-tools/src/xg-check.h (xgettext_check_message_list): Renamed from syntax_check_message_list. * gettext-tools/src/xg-check.c: Include format.h, if-error.h. (struct formatstring_error_logger_locals): New type. (formatstring_error_logger, format_check_message): New functions. (xgettext_check_message_list): Renamed from syntax_check_message_list. Invoke also format_check_message. * gettext-tools/src/xgettext.c (main): Invoke xgettext_check_message_list instead of syntax_check_message_list. * gettext-tools/tests/xgettext-19: New file. * gettext-tools/tests/Makefile.am (TESTS): Add it. * NEWS: Mention the change. --- diff --git a/NEWS b/NEWS index 4cbcd9ba4..3a0be9fd0 100644 --- a/NEWS +++ b/NEWS @@ -22,6 +22,15 @@ Version 0.26 - July 2025 - xgettext now recognizes the \c, \u, and \U escape sequences in dollar- single-quoted strings $'...'. +# Improvements for maintainers: + * xgettext: + - When extracting a message with plural that is some format string, + xgettext now verifies that the msgid and msgid_plural are compatible + as format strings. For most format string types, this still allows + omitting from msgid a placeholder that is used in msgid_plural. But + when a placeholder is used in both msgid and msgid_plural, its type + must be the same in both. + # Improvements for translators: * msggrep: - msggrep accepts two new options -W/--workflow-flags and -S/--sticky-flags diff --git a/gettext-tools/src/if-error.c b/gettext-tools/src/if-error.c index 75c0a5a89..de0e52390 100644 --- a/gettext-tools/src/if-error.c +++ b/gettext-tools/src/if-error.c @@ -1,5 +1,5 @@ /* Error handling during reading of input files. - Copyright (C) 2023-2024 Free Software Foundation, Inc. + Copyright (C) 2023-2025 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -37,15 +37,13 @@ void -if_error (int severity, - const char *filename, size_t lineno, size_t column, - bool multiline, const char *format, ...) +if_verror (int severity, + const char *filename, size_t lineno, size_t column, + bool multiline, const char *format, va_list args) { const char *prefix_tail = (severity == IF_SEVERITY_WARNING ? _("warning: ") : _("error: ")); - va_list args; - va_start (args, format); char *message_text = xvasprintf (format, args); if (message_text == NULL) message_text = xstrdup (severity == IF_SEVERITY_WARNING @@ -104,7 +102,18 @@ if_error (int severity, } error_with_progname = true; - va_end (args); if (severity == IF_SEVERITY_FATAL_ERROR) exit (EXIT_FAILURE); } + +void +if_error (int severity, + const char *filename, size_t lineno, size_t column, + bool multiline, const char *format, ...) +{ + va_list args; + + va_start (args, format); + if_verror (severity, filename, lineno, column, multiline, format, args); + va_end (args); +} diff --git a/gettext-tools/src/if-error.h b/gettext-tools/src/if-error.h index 23cc50485..5b8c89372 100644 --- a/gettext-tools/src/if-error.h +++ b/gettext-tools/src/if-error.h @@ -1,5 +1,5 @@ /* Error handling during reading of input files. - Copyright (C) 2023 Free Software Foundation, Inc. + Copyright (C) 2023-2025 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ #ifndef _IF_ERROR_H #define _IF_ERROR_H +#include #include #include @@ -50,6 +51,13 @@ extern void if_error (int severity, __attribute__ ((__format__ (__printf__, 6, 7))) #endif ; +extern void if_verror (int severity, + const char *filename, size_t lineno, size_t column, + bool multiline, const char *format, va_list args) +#if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1) + __attribute__ ((__format__ (__printf__, 6, 0))) +#endif + ; #ifdef __cplusplus diff --git a/gettext-tools/src/xg-check.c b/gettext-tools/src/xg-check.c index 4dc707832..079027737 100644 --- a/gettext-tools/src/xg-check.c +++ b/gettext-tools/src/xg-check.c @@ -1,5 +1,5 @@ /* Checking of messages in POT files: so-called "syntax checks". - Copyright (C) 2015-2023 Free Software Foundation, Inc. + Copyright (C) 2015-2025 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -14,7 +14,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -/* Written by Daiki Ueno . */ +/* Written by Daiki Ueno and Bruno Haible . */ #ifdef HAVE_CONFIG_H # include @@ -29,7 +29,9 @@ #include "xalloc.h" #include "xvasprintf.h" #include "message.h" +#include "format.h" #include "po-xerror.h" +#include "if-error.h" #include "sentence.h" #include "c-ctype.h" #include "unictype.h" @@ -340,10 +342,84 @@ syntax_check_message (const message_ty *mp) } -/* Perform all syntax checks on a message list. +/* Signal an error when checking format strings. */ +struct formatstring_error_logger_locals +{ + const lex_pos_ty *pos; +}; +static void +formatstring_error_logger (void *data, const char *format, ...) +#if defined __GNUC__ && ((__GNUC__ == 2 && __GNUC_MINOR__ >= 7) || __GNUC__ > 2) + __attribute__ ((__format__ (__printf__, 2, 3))) +#endif +; +static void +formatstring_error_logger (void *data, const char *format, ...) +{ + struct formatstring_error_logger_locals *l = + (struct formatstring_error_logger_locals *) data; + va_list args; + + va_start (args, format); + if_verror (IF_SEVERITY_ERROR, + l->pos->file_name, l->pos->line_number, (size_t)(-1), false, + format, args); + va_end (args); +} + + +/* Perform all format checks on a non-obsolete message. + Return the number of errors that were seen. */ +static int +format_check_message (const message_ty *mp) +{ + int seen_errors = 0; + size_t i; + + if (mp->msgid_plural != NULL) + { + /* Look for format string incompatibilities between msgid and + msgid_plural. */ + for (i = 0; i < NFORMATS; i++) + if (possible_format_p (mp->is_format[i])) + { + struct formatstring_parser *parser = formatstring_parsers[i]; + char *invalid_reason1 = NULL; + void *descr1 = + parser->parse (mp->msgid, false, NULL, &invalid_reason1); + char *invalid_reason2 = NULL; + void *descr2 = + parser->parse (mp->msgid_plural, false, NULL, &invalid_reason2); + + if (descr1 != NULL && descr2 != NULL) + { + struct formatstring_error_logger_locals locals; + locals.pos = &mp->pos; + if (parser->check (descr2, descr1, false, + formatstring_error_logger, &locals, + "msgid_plural", "msgid")) + seen_errors++; + } + + if (descr2 != NULL) + parser->free (descr2); + else + free (invalid_reason2); + if (descr1 != NULL) + parser->free (descr1); + else + free (invalid_reason1); + } + } + + return seen_errors; +} + + +/* Perform all checks on a message list. Return the number of errors that were seen. */ int -syntax_check_message_list (message_list_ty *mlp) +xgettext_check_message_list (message_list_ty *mlp) { int seen_errors = 0; size_t j; @@ -353,7 +429,7 @@ syntax_check_message_list (message_list_ty *mlp) message_ty *mp = mlp->item[j]; if (!is_header (mp)) - seen_errors += syntax_check_message (mp); + seen_errors += syntax_check_message (mp) + format_check_message (mp); } return seen_errors; diff --git a/gettext-tools/src/xg-check.h b/gettext-tools/src/xg-check.h index bda684e37..917613bb8 100644 --- a/gettext-tools/src/xg-check.h +++ b/gettext-tools/src/xg-check.h @@ -1,5 +1,5 @@ /* Checking of messages in POT files: so-called "syntax checks". - Copyright (C) 2015-2023 Free Software Foundation, Inc. + Copyright (C) 2015-2025 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,9 +27,9 @@ extern "C" { #endif -/* Perform all syntax checks on a message list. +/* Perform all checks on a message list. Return the number of errors that were seen. */ -extern int syntax_check_message_list (message_list_ty *mlp); +extern int xgettext_check_message_list (message_list_ty *mlp); #ifdef __cplusplus diff --git a/gettext-tools/src/xgettext.c b/gettext-tools/src/xgettext.c index f44e93481..e21813a08 100644 --- a/gettext-tools/src/xgettext.c +++ b/gettext-tools/src/xgettext.c @@ -1097,14 +1097,14 @@ xgettext cannot work without keywords to look for")); else if (sort_by_msgid) msgdomain_list_sort_by_msgid (mdlp); - /* Check syntax of messages. */ + /* Check msgid and msgid_plural of messages. */ { int nerrors = 0; for (i = 0; i < mdlp->nitems; i++) { message_list_ty *mlp = mdlp->item[i]->messages; - nerrors += syntax_check_message_list (mlp); + nerrors += xgettext_check_message_list (mlp); } /* Exit with status 1 on any error. */ diff --git a/gettext-tools/tests/Makefile.am b/gettext-tools/tests/Makefile.am index 6f02cc69c..06967ae3c 100644 --- a/gettext-tools/tests/Makefile.am +++ b/gettext-tools/tests/Makefile.am @@ -85,7 +85,7 @@ TESTS = gettext-1 gettext-2 \ xgettext-2 xgettext-3 xgettext-4 xgettext-5 xgettext-6 \ xgettext-7 xgettext-8 xgettext-9 xgettext-10 xgettext-11 xgettext-12 \ xgettext-13 xgettext-14 xgettext-15 xgettext-16 xgettext-17 \ - xgettext-18 \ + xgettext-18 xgettext-19 \ xgettext-combine-1 xgettext-combine-2 xgettext-combine-3 \ xgettext-git-1 \ xgettext-appdata-1 xgettext-appdata-2 xgettext-appdata-3 \ diff --git a/gettext-tools/tests/xgettext-19 b/gettext-tools/tests/xgettext-19 new file mode 100755 index 000000000..369572b22 --- /dev/null +++ b/gettext-tools/tests/xgettext-19 @@ -0,0 +1,27 @@ +#!/bin/sh +. "${srcdir=.}/init.sh"; path_prepend_ . ../src + +# Test that msgid and msgid_plural are checked to be compatible as +# format strings. +# + +cat <<\EOF > xg-test19.c + if (filedata->is_separate) + printf (ngettext ("\nIn linked file '%s' the dynamic section at offset %#" PRIx64 " contains %" PRIu64 " entry:\n", + "\nIn linked file '%s' the dynamic section at offset %#" PRIx64 " contains %" PRIu64 " entries:\n", + filedata->dynamic_nent), + filedata->file_name, + filedata->dynamic_addr, + filedata->dynamic_nent); + else + printf (ngettext ("\nDynamic section at offset %#" PRIx64 " contains %" PRId64 " entry:\n", + "\nDynamic section at offset %#" PRIx64 " contains %" PRIu64 " entries:\n", + filedata->dynamic_nent), + filedata->dynamic_addr, + filedata->dynamic_nent); +EOF + +: ${XGETTEXT=xgettext} +LANGUAGE= LC_ALL=C ${XGETTEXT} --omit-header --add-comments -d xg-test19.tmp xg-test19.c 2>xg-test19.err +test $? = 1 || Exit 1 +grep "format specifications in 'msgid_plural' and 'msgid' for argument 2 are not the same" xg-test19.err || Exit 1