]> git.ipfire.org Git - thirdparty/gettext.git/commitdiff
xgettext: Check msgid and msgid_plural for compatibility as format strings.
authorBruno Haible <bruno@clisp.org>
Thu, 3 Jul 2025 14:56:01 +0000 (16:56 +0200)
committerBruno Haible <bruno@clisp.org>
Thu, 3 Jul 2025 15:41:22 +0000 (17:41 +0200)
* gettext-tools/src/if-error.h: Include <stdarg.h>.
(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.

NEWS
gettext-tools/src/if-error.c
gettext-tools/src/if-error.h
gettext-tools/src/xg-check.c
gettext-tools/src/xg-check.h
gettext-tools/src/xgettext.c
gettext-tools/tests/Makefile.am
gettext-tools/tests/xgettext-19 [new file with mode: 0755]

diff --git a/NEWS b/NEWS
index 4cbcd9ba4f23769c5e0e8ce715f9dd329d65a2f5..3a0be9fd0ccd5bb111166c2c51b45a3ccf20eb4e 100644 (file)
--- 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
index 75c0a5a89ba9d7380abb1cac5befa4fe2ea78e04..de0e52390fe714c27e6540f8f19a1f1f7334f505 100644 (file)
@@ -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
 
 
 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);
+}
index 23cc504852d634b18e8d1d46fc1e1ed162d1a9ad..5b8c893727c37f9c30b40acda39e7ade6e6cef8a 100644 (file)
@@ -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 <stdarg.h>
 #include <stdbool.h>
 #include <stddef.h>
 
@@ -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
index 4dc707832250168d54e4f0b3eea12e557d1fd1c9..079027737ad067651a39b6e00d97639661a4db87 100644 (file)
@@ -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 <https://www.gnu.org/licenses/>.  */
 
-/* Written by Daiki Ueno <ueno@gnu.org>.  */
+/* Written by Daiki Ueno <ueno@gnu.org> and Bruno Haible <bruno@clisp.org>.  */
 
 #ifdef HAVE_CONFIG_H
 # include <config.h>
@@ -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;
index bda684e37834d0e84f434ca023291d707068a629..917613bb8718864d88a78923c1d1c4d602327a23 100644 (file)
@@ -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
index f44e9348136be62c0bbd26919acc2171e9175353..e21813a08fa8fdbaa139b41ee46af592dfa7fe32 100644 (file)
@@ -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.  */
index 6f02cc69c1390391f18ade94972c2aaa9ad793d7..06967ae3c84cc8e3d0ca244220ff54d6909fc3fc 100644 (file)
@@ -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 (executable)
index 0000000..369572b
--- /dev/null
@@ -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.
+# <https://savannah.gnu.org/bugs/?63630>
+
+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