From a3df810e64edde3ae3a771a9eb55d1fdf3b38d14 Mon Sep 17 00:00:00 2001
From: Daiki Ueno <ueno@gnu.org>
Date: Thu, 29 Jan 2015 17:22:14 +0900
Subject: [PATCH] xgettext: Support message syntax checks

With this change, xgettext could report syntactic problems in
msgid/msgid_plural strings.  Currently supported checks are
unicode-ellipsis, no-space-before-ellipsis, and unicode-quote.
Feature suggested by Philip Withnall in:
https://savannah.gnu.org/bugs/?44098
* xgettext.c (long_options): Add --check option.
(main): Handle --check option.
(usage): Document --check option.
* msgl-check.h (enum syntax_check): New enum.
(syntax_check_message_list): New declaration.
* msgl-check.c (check_unicode_ellipsis): New function.
(check_no_space_before_ellipsis): New function.
(check_unicode_quote): New function.
(syntax_check_message): New function.
(syntax_check_message_list): New function.
---
 gettext-tools/doc/xgettext.texi |  12 +++
 gettext-tools/src/ChangeLog     |  19 ++++
 gettext-tools/src/msgl-check.c  | 214 ++++++++++++++++++++++++++++++++++++++++
 gettext-tools/src/msgl-check.h  |  11 ++-
 gettext-tools/src/xgettext.c    |  39 +++++++-
 5 files changed, 293 insertions(+), 2 deletions(-)

diff --git a/gettext-tools/doc/xgettext.texi b/gettext-tools/doc/xgettext.texi
index 451e25f..c45e0c4 100644
--- a/gettext-tools/doc/xgettext.texi
+++ b/gettext-tools/doc/xgettext.texi
@@ -144,6 +144,18 @@ gettext (
 The second comment line will not be extracted, because there is one
 blank line between the comment line and the keyword.
 
+@item -W[@var{CHECK}]
+@itemx --check[=@var{CHECK}]
+@opindex -W@r{, @code{xgettext} option}
+@opindex --check@r{, @code{xgettext} option}
+@cindex supported syntax checks, @code{xgettext}
+Perform a syntax check on msgid and msgid_plural before writing a POT
+file.  The supported checks are @code{unicode-ellipsis} which prefers
+Unicode ellipsis character over ASCII @code{...},
+@code{no-space-before-ellipsis} which forbids whitespace before an
+ellipsis character, and @code{unicode-quote} which prefers Unicode
+quotation marks over ASCII @code{"'`}.
+
 @end table
 
 @subsection Language specific options
diff --git a/gettext-tools/src/ChangeLog b/gettext-tools/src/ChangeLog
index 6327a1d..cd9feb2 100644
--- a/gettext-tools/src/ChangeLog
+++ b/gettext-tools/src/ChangeLog
@@ -1,5 +1,24 @@
 2015-01-29  Daiki Ueno  <ueno@gnu.org>
 
+	xgettext: Support message syntax checks
+	With this change, xgettext could report syntactic problems in
+	msgid/msgid_plural strings.  Currently supported checks are
+	unicode-ellipsis, no-space-before-ellipsis, and unicode-quote.
+	Feature suggested by Philip Withnall in:
+	https://savannah.gnu.org/bugs/?44098
+	* xgettext.c (long_options): Add --check option.
+	(main): Handle --check option.
+	(usage): Document --check option.
+	* msgl-check.h (enum syntax_check): New enum.
+	(syntax_check_message_list): New declaration.
+	* msgl-check.c (check_unicode_ellipsis): New function.
+	(check_no_space_before_ellipsis): New function.
+	(check_unicode_quote): New function.
+	(syntax_check_message): New function.
+	(syntax_check_message_list): New function.
+
+2015-01-29  Daiki Ueno  <ueno@gnu.org>
+
 	msgexec: Add --newline option
 	See the commit 96dde0b for the rationale.
 	* msgexec.c (newline): New variable.
diff --git a/gettext-tools/src/msgl-check.c b/gettext-tools/src/msgl-check.c
index d6f4a3d..eb55a4f 100644
--- a/gettext-tools/src/msgl-check.c
+++ b/gettext-tools/src/msgl-check.c
@@ -912,3 +912,217 @@ check_message_list (message_list_ty *mlp,
 
   return seen_errors;
 }
+
+
+static int
+check_unicode_ellipsis (const message_ty *mp,
+                        const char *msgid,
+                        const lex_pos_ty *msgid_pos,
+                        int checks)
+{
+  const char *cp = msgid;
+  int seen_errors = 0;
+
+  do
+    {
+      cp = strchrnul (cp, '\n');
+      if (cp > msgid + 3 && strncmp (cp - 3, "...", 3) == 0)
+        {
+          po_xerror (PO_SEVERITY_ERROR,
+                     mp, msgid_pos->file_name, msgid_pos->line_number, 0,
+                     false, _("\
+Unicode ellipsis (U+2026) should be used instead of ASCII ellipsis ('...')"));
+          seen_errors++;
+        }
+    }
+  while (*cp++ != '\0');
+
+  return seen_errors;
+}
+
+
+static int
+check_no_space_before_ellipsis (const message_ty *mp,
+                                const char *msgid,
+                                const lex_pos_ty *msgid_pos,
+                                int checks)
+{
+  /* Coincidentally the lengths of bytes are same for UTF-8 and ASCII
+     ellipsis.  */
+  const char *ellipsis
+    = (checks & CHECK_UNICODE_ELLIPSIS) ? "\xE2\x80\xA6" : "...";
+  const char *cp = msgid;
+  int seen_errors = 0;
+
+  do
+    {
+      cp = strchrnul (cp, '\n');
+      if (cp > msgid + 4 && strncmp (cp - 3, ellipsis, 3) == 0
+          && c_isspace (*(cp - 4)))
+        {
+          po_xerror (PO_SEVERITY_ERROR,
+                     mp, msgid_pos->file_name, msgid_pos->line_number, 0,
+                     false, _("\
+space before ellipsis found in user visible strings"));
+          seen_errors++;
+        }
+    }
+  while (*cp++ != '\0');
+
+  return seen_errors;
+}
+
+
+static int
+check_unicode_quote (const message_ty *mp,
+                     const char *msgid,
+                     const lex_pos_ty *msgid_pos,
+                     int checks)
+{
+  const char *cp, *start = msgid, *end = msgid + strlen (msgid) - 1;
+  /* True if we have seen a character which could be an opening
+     quotation mark.  Note that we can't determine if it is really an
+     opening quotation mark until we see a closing quotation mark.  */
+  bool state = false;
+  int seen_errors = 0;
+
+  for (cp = start; cp <= end; cp++)
+    {
+      switch (*cp)
+        {
+        case '"':
+          if (state)
+            {
+              if (*start == '"')
+                {
+                  if (cp > start + 1)
+                    {
+                      po_xerror (PO_SEVERITY_ERROR,
+                                 mp, msgid_pos->file_name, msgid_pos->line_number, 0,
+                                 false, _("\
+Unicode double quote should be used instead of ASCII"));
+                      seen_errors++;
+                    }
+                  start = cp + 1;
+                  state = false;
+                }
+            }
+          else
+            {
+              start = cp;
+              state = true;
+            }
+          break;
+
+        case '`':
+          if (state)
+            {
+              if (*start == '`')
+                start = cp;
+            }
+          else
+            {
+              start = cp;
+              state = true;
+            }
+          break;
+
+        case '\'':
+          if (state)
+            {
+              if (/* `...' */
+                  *start == '`'
+                  /* '...', where:
+                     - The left quote is preceded by a space, and the
+                       right quote is followed by a space.
+                     - The left quote is preceded by a space, and the
+                       right quote is at the end of line.
+                     - The left quote is at the beginning of the line, and
+                       the right quote is followed by a space.
+                   */
+                  || (*start == '\''
+                      && (((start > msgid && *(start - 1) == ' ')
+                           && (cp == end || *(cp + 1) == '\n'
+                               || *(cp + 1) == ' '))
+                          || ((start == msgid || *(start - 1) == '\n')
+                              && cp < end && *(cp + 1) == ' '))))
+                {
+                  po_xerror (PO_SEVERITY_ERROR,
+                             mp, msgid_pos->file_name, msgid_pos->line_number, 0,
+                             false, _("\
+Unicode single quote should be used instead of ASCII"));
+                  seen_errors++;
+                  start = cp + 1;
+                }
+              else
+                start = cp;
+              state = false;
+            }
+          else if (cp == msgid || *(cp - 1) == '\n' || *(cp - 1) == ' ')
+            {
+              start = cp;
+              state = true;
+            }
+          break;
+        }
+    }
+
+  return seen_errors;
+}
+
+
+/* Perform all syntax checks on a non-obsolete message.
+   Return the number of errors that were seen.  */
+static int
+syntax_check_message (const message_ty *mp,
+                      const lex_pos_ty *msgid_pos,
+                      int checks)
+{
+  int seen_errors = 0;
+
+  if (checks & CHECK_UNICODE_ELLIPSIS)
+    {
+      seen_errors += check_unicode_ellipsis (mp, mp->msgid, msgid_pos, checks);
+      if (mp->msgid_plural)
+        seen_errors += check_unicode_ellipsis (mp, mp->msgid_plural, msgid_pos,
+                                               checks);
+    }
+  if (checks & CHECK_NO_SPACE_BEFORE_ELLIPSIS)
+    {
+      seen_errors += check_no_space_before_ellipsis (mp, mp->msgid, msgid_pos,
+                                                     checks);
+      if (mp->msgid_plural)
+        seen_errors += check_no_space_before_ellipsis (mp, mp->msgid_plural,
+                                                       msgid_pos, checks);
+    }
+  if (checks & CHECK_UNICODE_QUOTE)
+    {
+      seen_errors += check_unicode_quote (mp, mp->msgid, msgid_pos, checks);
+      if (mp->msgid_plural)
+        seen_errors += check_unicode_quote (mp, mp->msgid_plural, msgid_pos,
+                                            checks);
+    }
+
+  return seen_errors;
+}
+
+
+/* Perform all syntax checks on a message list.
+   Return the number of errors that were seen.  */
+int
+syntax_check_message_list (message_list_ty *mlp,
+                           int checks)
+{
+  int seen_errors = 0;
+  size_t j;
+
+  for (j = 0; j < mlp->nitems; j++)
+    {
+      message_ty *mp = mlp->item[j];
+
+      if (!is_header (mp))
+        seen_errors += syntax_check_message (mp, &mp->pos, checks);
+    }
+
+  return seen_errors;
+}
diff --git a/gettext-tools/src/msgl-check.h b/gettext-tools/src/msgl-check.h
index f03300c..d96411e 100644
--- a/gettext-tools/src/msgl-check.h
+++ b/gettext-tools/src/msgl-check.h
@@ -28,7 +28,6 @@
 extern "C" {
 #endif
 
-
 /* Check the values returned by plural_eval.
    Signals the errors through po_xerror.
    Return the number of errors that were seen.
@@ -60,6 +59,16 @@ extern int check_message_list (message_list_ty *mlp,
                                int check_compatibility,
                                int check_accelerators, char accelerator_char);
 
+enum syntax_check
+{
+  CHECK_UNICODE_ELLIPSIS = 1 << 0,
+  CHECK_NO_SPACE_BEFORE_ELLIPSIS = 1 << 1,
+  CHECK_UNICODE_QUOTE = 1 << 2
+};
+
+/* Perform all syntax checks on a message list.
+   Return the number of errors that were seen.  */
+extern int syntax_check_message_list (message_list_ty *mlp, int checks);
 
 #ifdef __cplusplus
 }
diff --git a/gettext-tools/src/xgettext.c b/gettext-tools/src/xgettext.c
index f9156eb..4b48ab1 100644
--- a/gettext-tools/src/xgettext.c
+++ b/gettext-tools/src/xgettext.c
@@ -58,6 +58,8 @@
 #include "po-charset.h"
 #include "msgl-iconv.h"
 #include "msgl-ascii.h"
+#include "msgl-check.h"
+#include "po-xerror.h"
 #include "po-time.h"
 #include "write-catalog.h"
 #include "write-po.h"
@@ -204,6 +206,7 @@ static const struct option long_options[] =
   { "add-location", optional_argument, NULL, 'n' },
   { "boost", no_argument, NULL, CHAR_MAX + 11 },
   { "c++", no_argument, NULL, 'C' },
+  { "check", required_argument, NULL, 'W' },
   { "color", optional_argument, NULL, CHAR_MAX + 14 },
   { "copyright-holder", required_argument, NULL, CHAR_MAX + 1 },
   { "debug", no_argument, &do_debug, 1 },
@@ -304,6 +307,7 @@ main (int argc, char *argv[])
   extractor_ty extractor = { NULL, NULL, NULL, NULL };
   int cnt;
   size_t i;
+  int syntax_checks = 0;
 
   /* Set program name for messages.  */
   set_program_name (argv[0]);
@@ -346,7 +350,7 @@ main (int argc, char *argv[])
   init_flag_table_vala ();
 
   while ((optchar = getopt_long (argc, argv,
-                                 "ac::Cd:D:eEf:Fhijk::l:L:m::M::no:p:sTVw:x:",
+                                 "ac::Cd:D:eEf:Fhijk::l:L:m::M::no:p:sTVw:W:x:",
                                  long_options, NULL)) != EOF)
     switch (optchar)
       {
@@ -525,6 +529,17 @@ main (int argc, char *argv[])
         }
         break;
 
+      case 'W':
+        if (strcmp (optarg, "unicode-ellipsis") == 0)
+          syntax_checks |= CHECK_UNICODE_ELLIPSIS;
+        else if (strcmp (optarg, "no-space-before-ellipsis") == 0)
+          syntax_checks |= CHECK_NO_SPACE_BEFORE_ELLIPSIS;
+        else if (strcmp (optarg, "unicode-quote") == 0)
+          syntax_checks |= CHECK_UNICODE_QUOTE;
+        else
+          error (EXIT_FAILURE, 0, _("syntax check '%s' unknown"), optarg);
+        break;
+
       case 'x':
         read_exclusion_file (optarg);
         break;
@@ -836,6 +851,24 @@ warning: file '%s' extension '%s' is unknown; will try C"), filename, extension)
   else if (sort_by_msgid)
     msgdomain_list_sort_by_msgid (mdlp);
 
+  /* Check syntax 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, syntax_checks);
+      }
+
+    /* Exit with status 1 on any error.  */
+    if (nerrors > 0)
+      error (EXIT_FAILURE, 0,
+             ngettext ("found %d fatal error", "found %d fatal errors",
+                       nerrors),
+             nerrors);
+  }
+
   /* Write the PO file.  */
   msgdomain_list_print (mdlp, file_name, output_syntax, force_po, do_debug);
 
@@ -921,6 +954,10 @@ Operation mode:\n"));
                                 preceding keyword lines in output file\n\
   -c, --add-comments          place all comment blocks preceding keyword lines\n\
                                 in output file\n"));
+      printf (_("\
+  -W, --check=NAME            perform syntax check on messages\n\
+                                (unicode-ellipsis, no-space-before-ellipsis,\n\
+                                 unicode-quote)\n"));
       printf ("\n");
       printf (_("\
 Language specific options:\n"));
-- 
2.1.0

