On Fri, Jul 04, 2025 at 10:18:05PM +0100, Gavin Smith wrote:
> All these nodes (in the Sphinx output) have explicit pointers on the
> node lines, just like the affected part of the libc manual.
> 
> So I expect we will have to take some account of node pointers
> when generating these warnings.  (We could still give 1 or 2 warnings if
> there is a simple way of doing so, but not 197.)

Here's my current version (done in both Perl and C).  Only the first block
of code is new in each file; the second ("check consistency..."") is
just moved from later in the function.

As with the existing warnings, node pointers can be used to get the next
expected node to appear in a menu.

I just need to check through all the changes in the test suite to check
if they are satisfactory and tidy up the patch a bit.  I probably need
one or two more test cases for explicit node pointers.


diff --git a/tta/C/structuring_transfo/structuring.c 
b/tta/C/structuring_transfo/structuring.c
index af8262ab91..f6cc943ae4 100644
--- a/tta/C/structuring_transfo/structuring.c
+++ b/tta/C/structuring_transfo/structuring.c
@@ -1231,11 +1231,169 @@ check_node_tree_menu_structure (DOCUMENT *document)
           }
       }
 
+  /* Go through all the menus and check if they match subordinate
+     nodes. */
+  if (!options
+      || options->CHECK_NORMAL_MENU_STRUCTURE.o.integer > 0)
+    {
+      for (i = 0; i < nodes_list->number; i++)
+        {
+          NODE_RELATIONS *node_relations = nodes_list->list[i];
+
+          const SECTION_RELATIONS *section_relations;
+          section_relations = node_relations->associated_section;
+          if (!section_relations)
+            continue;
+
+          const SECTION_RELATIONS_LIST *section_childs
+            = section_relations->section_childs;
+          if (!section_childs || section_childs->number == 0)
+            continue;
+
+          const SECTION_RELATIONS *first_child = section_childs->list[0];
+          const NODE_RELATIONS *first_child_node_relations
+            = first_child->associated_node;
+          if (!first_child_node_relations)
+            continue;
+
+          /* Set to the first subordinate section, which should appear first in
+             the menu. */
+          const ELEMENT *section_node = first_child_node_relations->element;
+          const NODE_RELATIONS *last_menu_node_relations = NULL;
+
+          const CONST_ELEMENT_LIST *menus = node_relations->menus;
+          if (!menus)
+            continue;
+
+          size_t j;
+          for (j = 0; j < menus->number; j++)
+            {
+              const ELEMENT *menu_node;
+
+              const ELEMENT *menu = menus->list[j];
+              size_t k;
+              /* Loop through each entry in the menu and
+                 check if it is the menu entry we were expecting
+                 to see based on what came before. */
+              for (k = 0; k < menu->e.c->contents.number; k++)
+                {
+                  const ELEMENT *menu_content = menu->e.c->contents.list[k];
+                  if (menu_content->type != ET_menu_entry)
+                    continue;
+                  const ELEMENT *menu_node
+                    = normalized_entry_associated_internal_node
+                        (menu_content, identifiers_target);
+                  if (!menu_node)
+                    continue;
+
+                  int status;
+                  const int menu_node_element_number
+                    = lookup_extra_integer (menu_node, AI_key_node_number, 
&status);
+                  if (status < 0)
+                    continue; /* not defined if @anchor or @namedanchor */
+
+                  if (!section_node)
+                    {
+                      char *menu_node_texi
+                        = target_element_to_texi_label (menu_node);
+                      message_list_command_warn (error_messages,
+                         (options && options->DEBUG.o.integer > 0),
+                              menu_content, 0,
+                              "unexpected node `%s' in menu",
+                              menu_node_texi);
+                      free (menu_node_texi);
+                      node_errors[menu_node_element_number - 1] = 1;
+                    }
+                  else if (menu_node != section_node)
+                    {
+                      const ELEMENT *next_pointer_node = NULL;
+                      if (last_menu_node_relations)
+                        {
+                          /* If there are explicit node pointers, also allow
+                             the "next" node. */
+                          const ELEMENT *last_menu_node
+                            = last_menu_node_relations->element;
+                          const ELEMENT *arguments_line
+                            = last_menu_node->e.c->contents.list[0];
+                          int automatic_directions
+                            = (arguments_line->e.c->contents.number <= 1);
+                          if (!automatic_directions)
+                            {
+                              const ELEMENT * const *node_directions
+                                = last_menu_node_relations->node_directions;
+                              if (node_directions)
+                                next_pointer_node = node_directions[D_next];
+                            }
+                        }
+
+                      if (!next_pointer_node
+                          || next_pointer_node != menu_node)
+                        {
+                          char *menu_node_texi
+                            = target_element_to_texi_label (menu_node);
+                          char *section_node_texi
+                            = target_element_to_texi_label (section_node);
+                          message_list_command_warn (error_messages,
+                             (options && options->DEBUG.o.integer > 0),
+                                  menu_content, 0,
+                                  "node `%s' in menu where `%s' expected",
+                                  menu_node_texi,
+                                  section_node_texi);
+                          free (menu_node_texi);
+                          free (section_node_texi);
+                          node_errors[menu_node_element_number - 1] = 1;
+                        }
+                    }
+
+                  /* Now set section_node for the section that is
+                     expected to follow the current menu node. */
+
+                  last_menu_node_relations
+                    = nodes_list->list[menu_node_element_number - 1];
+                  if (!last_menu_node_relations
+                      || !last_menu_node_relations->associated_section)
+                    continue;
+
+                  const SECTION_RELATIONS *const *menu_section_dirs
+                    = last_menu_node_relations->associated_section
+                        ->section_directions;
+
+                  if (!menu_section_dirs || !menu_section_dirs[D_up]
+                      || !menu_section_dirs[D_up]->associated_node)
+                    continue;
+                  const NODE_RELATIONS *menu_node_up
+                    = menu_section_dirs[D_up]->associated_node;
+                  if (menu_node_up != node_relations)
+                    {
+                      /* Keep the same expected section as the current
+                         menu node is misplaced. */
+                      ;
+                    }
+                  else if (menu_section_dirs[D_next]
+                           && menu_section_dirs[D_next]->associated_node
+                           && 
menu_section_dirs[D_next]->associated_node->element)
+                    {
+                      section_node
+                        = menu_section_dirs[D_next]->associated_node->element;
+                    }
+                   else
+                    {
+                      /* We reached the last subordinate section so no more
+                         menu entries are expected. */
+                      section_node = NULL;
+                    }
+                }
+            }
+        }
+    }
+
   /* check for node up / menu up mismatch */
   if ((!options)
       || options->CHECK_MISSING_MENU_ENTRY.o.integer > 0)
     for (i = 0; i < nodes_list->number; i++)
       {
+        if (node_errors[i])
+          continue;
         NODE_RELATIONS *node_relations = nodes_list->list[i];
         const ELEMENT *node = node_relations->element;
 
@@ -1307,6 +1465,10 @@ check_node_tree_menu_structure (DOCUMENT *document)
                            the child node is to a different node. */
                         const ELEMENT * const *node_directions
                                          = node_relations->node_directions;
+
+
+
+
                         if (!node_directions
                             || node_directions[D_up] == up_node)
                           {
@@ -1333,6 +1495,73 @@ check_node_tree_menu_structure (DOCUMENT *document)
           }
       }
 
+  /* check consistency between explicit node pointer and node
+     entries menu order */
+  if (!options
+      || options->CHECK_NORMAL_MENU_STRUCTURE.o.integer > 0)
+    {
+      for (i = 0; i < nodes_list->number; i++)
+        {
+          if (node_errors[i])
+            continue;
+          NODE_RELATIONS *node_relations = nodes_list->list[i];
+          const ELEMENT *node = node_relations->element;
+          const char *normalized = lookup_extra_string (node, 
AI_key_normalized);
+          if (!strcmp (normalized, "Top"))
+            continue;
+
+          const ELEMENT * const *node_directions
+                           = node_relations->node_directions;
+
+          const ELEMENT *arguments_line = node->e.c->contents.list[0];
+          int automatic_directions = (arguments_line->e.c->contents.number <= 
1);
+          const ELEMENT * const *menu_directions = 
node_relations->menu_directions;
+          if (!automatic_directions)
+            if (node_directions && menu_directions)
+              {
+                size_t d;
+                for (d = 0; d < directions_length; d++)
+                  {
+                    if (node_directions[d]
+                        && menu_directions[d]
+                        && node_directions[d]
+                             != menu_directions[d])
+                      {
+                        const ELEMENT *menu_direction
+                         = menu_directions[d];
+                        const ELEMENT *menu_dir_manual_content
+                         = lookup_extra_container (menu_direction,
+                                                   AI_key_manual_content);
+                        const ELEMENT *node_dir_manual_content
+                         = lookup_extra_container (node_directions[d],
+                                                   AI_key_manual_content);
+                        if (!menu_dir_manual_content && 
!node_dir_manual_content)
+                          {
+                            char *node_texi = target_element_to_texi_label 
(node);
+                            char *dir_texi = target_element_to_texi_label
+                                              (node_directions[d]);
+                            char *menu_dir_texi
+                               = target_element_to_texi_label (menu_direction);
+                            message_list_command_warn (error_messages,
+                               (options && options->DEBUG.o.integer > 0),
+                                             node, 0,
+                      "node %s pointer for `%s' is `%s' but %s is `%s' in 
menu",
+                                             direction_names[d], node_texi,
+                                             dir_texi, direction_names[d],
+                                             menu_dir_texi);
+                            free (node_texi);
+                            free (dir_texi);
+                            free (menu_dir_texi);
+                          }
+                      }
+                  }
+              }
+        }
+    }
+
+  free (node_errors);
+  return;
+
   /* Node-by-node structure checking. */
   if (!options
       || options->CHECK_NORMAL_MENU_STRUCTURE.o.integer > 0)
diff --git a/tta/perl/Texinfo/Structuring.pm b/tta/perl/Texinfo/Structuring.pm
index 5f79484786..5345b28026 100644
--- a/tta/perl/Texinfo/Structuring.pm
+++ b/tta/perl/Texinfo/Structuring.pm
@@ -844,12 +844,133 @@ sub check_node_tree_menu_structure($)
     }
   }
 
+  # Go through all the menus and check if they match subordinate
+  # nodes.
+  if ($customization_information->get_conf('CHECK_NORMAL_MENU_STRUCTURE')) {
+    foreach my $node_relations (@{$nodes_list}) {
+      if ($node_relations->{'menus'}) {
+        next if !$node_relations->{'associated_section'};
+        my $section_childs = $node_relations->{'associated_section'}
+                                            ->{'section_childs'};
+        next if !defined($section_childs) or !@{$section_childs};
+
+        my $first_child_node_relations = 
$section_childs->[0]->{'associated_node'};
+        next if !defined($first_child_node_relations);
+
+        # Set to the first subordinate section, which should appear first in 
the
+        # menu.
+        my ($section_node, $last_menu_node_relations);
+        $section_node = $first_child_node_relations->{'element'};
+        next if !defined($section_node);
+
+        foreach my $menu (@{$node_relations->{'menus'}}) {
+          # Loop through each each entry in the menu and
+          # check if it is the menu entry we were expecting
+          # to see based on what came before.
+          MENU_CONTENT:
+          foreach my $menu_content (@{$menu->{'contents'}}) {
+            next if !defined($menu_content->{'type'})
+              or $menu_content->{'type'} ne 'menu_entry';
+
+            my $menu_node;
+            foreach my $content (@{$menu_content->{'contents'}}) {
+              next if $content->{'type'} ne 'menu_entry_node';
+              if ($content->{'extra'}) {
+                if (!$content->{'extra'}->{'manual_content'}) {
+                  if (defined($content->{'extra'}->{'normalized'})) {
+                    my $menu_node_name = $content->{'extra'}->{'normalized'};
+                    $menu_node = $identifier_target->{$menu_node_name};
+                  }
+                }
+              }
+              last; # menu_entry_node found
+            }
+            next MENU_CONTENT if !defined($menu_node)
+              or !defined($menu_node->{'extra'})
+              or !defined($menu_node->{'extra'}->{'node_number'});
+
+            my $menu_node_element_number = 
$menu_node->{'extra'}->{'node_number'};
+
+            if (!defined($section_node)) {
+              $registrar->line_warn(
+                sprintf(__("unexpected node `%s' in menu"),
+                  target_element_to_texi_label($menu_node)),
+                $menu_content->{'source_info'}, 0,
+                $customization_information->get_conf('DEBUG'));
+              $node_errors{$menu_node_element_number} = 1;
+            } elsif ($menu_node ne $section_node) {
+              my $next_pointer_node;
+              if ($last_menu_node_relations) {
+                # If there are explicit node pointers, also allow
+                # the "next" node.
+                my $last_menu_node = $last_menu_node_relations->{'element'};
+                my $arguments_line = $last_menu_node->{'contents'}->[0];
+                my $automatic_directions
+                  = (not (scalar(@{$arguments_line->{'contents'}}) > 1));
+                if (!$automatic_directions) {
+                  my $last_menu_node_directions = 
$last_menu_node_relations->{'node_directions'};
+                  $next_pointer_node = $last_menu_node_directions->{'next'};
+                }
+              }
+
+              if (!defined($next_pointer_node)
+                  or $next_pointer_node ne $menu_node) {
+                $registrar->line_warn(
+                  sprintf(__("node `%s' in menu where `%s' expected"),
+                    target_element_to_texi_label($menu_node),
+                    target_element_to_texi_label($section_node)),
+                  $menu_content->{'source_info'}, 0,
+                  $customization_information->get_conf('DEBUG'));
+                $node_errors{$menu_node_element_number} = 1;
+              }
+            }
+
+            # Now set section_node for the section that is
+            # expected to follow the current menu node.
+
+            $last_menu_node_relations
+              = $nodes_list->[$menu_node_element_number - 1];
+            next MENU_CONTENT if
+              !$last_menu_node_relations
+                or !defined($last_menu_node_relations->{'associated_section'});
+
+            my $menu_section_dirs
+              = $last_menu_node_relations->{'associated_section'}
+                  ->{'section_directions'};
+
+            next MENU_CONTENT
+              if !defined($menu_section_dirs)
+                 or !defined($menu_section_dirs->{'up'})
+                 or !defined($menu_section_dirs->{'up'}->{'associated_node'});
+
+            my $menu_node_up = $menu_section_dirs->{'up'}->{'associated_node'};
+            if ($menu_node_up ne $node_relations) {
+              # Keep the same expected section as the current menu node
+              # is misplaced.
+            } elsif (defined($menu_section_dirs->{'next'})
+                and defined($menu_section_dirs->{'next'}->{'associated_node'})
+                and defined($menu_section_dirs->{'next'}->{'associated_node'}
+                              ->{'element'})) {
+              $section_node = $menu_section_dirs->{'next'}->{'associated_node'}
+                                ->{'element'};
+            } else {
+              # We reached the last subordinate section so no more menu
+              # entries are expected.
+              undef $section_node;
+            }
+          }
+        }
+      }
+    }
+  }
+
   my %cached_menu_nodes;
 
   # check for node up / menu up mismatch
   if ($customization_information->get_conf('CHECK_MISSING_MENU_ENTRY')) {
     foreach my $node_relations (@{$nodes_list}) {
       my $node = $node_relations->{'element'};
+      next if $node_errors{$node->{'extra'}->{'node_number'}};
 
       my $section_relations = $node_relations->{'associated_section'};
       next if !defined($section_relations);
@@ -906,6 +1027,54 @@ sub check_node_tree_menu_structure($)
     }
   }
 
+  if ($customization_information->get_conf('CHECK_NORMAL_MENU_STRUCTURE')) {
+    foreach my $node_relations (@{$nodes_list}) {
+      my $node = $node_relations->{'element'};
+      next if $node_errors{$node->{'extra'}->{'node_number'}};
+      next if $node->{'extra'}->{'normalized'} eq 'Top';
+
+      my $node_directions = $node_relations->{'node_directions'};
+
+      my $arguments_line = $node->{'contents'}->[0];
+      my $automatic_directions
+        = (not (scalar(@{$arguments_line->{'contents'}}) > 1));
+
+      my $menu_directions = $node_relations->{'menu_directions'};
+      # check consistency between explicit node pointer and
+      # node entries menu order
+      if (!$automatic_directions) {
+        if ($node_directions and $menu_directions) {
+          foreach my $direction (@node_directions_names) {
+            if ($node_directions->{$direction}
+                and not $node_directions->{$direction}
+                      ->{'extra'}->{'manual_content'}
+                and $menu_directions->{$direction}
+                and $menu_directions->{$direction}
+                  ne $node_directions->{$direction}
+                and not $menu_directions->{$direction}
+                              ->{'extra'}->{'manual_content'}) {
+              $registrar->line_warn(
+             sprintf(__("node %s pointer for `%s' is `%s' but %s is `%s' in 
menu"),
+                    $direction,
+                    target_element_to_texi_label($node),
+                    target_element_to_texi_label(
+                        $node_directions->{$direction}),
+                    $direction,
+                    target_element_to_texi_label(
+                        $menu_directions->{$direction})),
+                                    $node->{'source_info'}, 0,
+                             $customization_information->get_conf('DEBUG'));
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return;
+
+  ###################################################################
+
   # Any problems with 'up' direction should have been found by previous code.
   #my @checked_node_directions_names = ('next', 'prev', 'up');
   my @checked_node_directions_names = ('next', 'prev');



Reply via email to