Skip to content

Menus: Stop a nested loop clobbering the slug it is matching against. - #13058

Open
jigneshbhavani wants to merge 1 commit into
WordPress:trunkfrom
jigneshbhavani:fix/nav-menu-slug-shadowing
Open

Menus: Stop a nested loop clobbering the slug it is matching against.#13058
jigneshbhavani wants to merge 1 commit into
WordPress:trunkfrom
jigneshbhavani:fix/nav-menu-slug-shadowing

Conversation

@jigneshbhavani

Copy link
Copy Markdown

wp_map_nav_menu_locations() uses $slug for two nested loops: the one over the slug group
at nav-menu.php:1276, and the one at :1292 nested three levels inside it. PHP leaves a
foreach variable at its last assigned value, so once any new location descends into the
old-location loops, the comparison at :1282 runs against whatever the inner loop left
behind rather than the outer loop's slug.

A skipped location is only recovered if it happens to match a later slug in the same group.
When it matches exactly one, nothing picks it up and the assignment is lost.

Measured on trunk. New theme locations on the left, previous theme's assignments on the right:

New locations Previous assignments Before After
primary, primary-menu header, mainmenu 1 of 2 mapped 2 of 2
primary, primary-nav header, mainmenu 1 of 2 mapped 2 of 2
footer, footer-menu secondary, bottom 1 of 2 mapped 2 of 2
primary, main navigation-menu, top-menu 2 of 2 2 of 2
header, header-top primary, top-menu 2 of 2 2 of 2

The last two rows are why this has gone unnoticed. primary + main is exactly
test_location_guessing_one_menu_per_location(), and it passes today because main is
itself a slug in the group, so the skipped location is picked up on a later pass.
header-top recovers the same way via top. Only a location matching a single slug is
actually lost.

Reached on every theme switch via _wp_menus_changed() on after_switch_theme
(default-filters.php:376) and from class-wp-customize-nav-menus.php:734.

Present since [41237] (#39692) in 4.9 — the inner loop has carried the outer loop's variable
name since the first version.

Approach

Renamed the inner loop variable to $old_slug. That is the whole change; the matching
logic is untouched. I confirmed the rename is what fixes it by applying it in isolation to
an extracted copy of the function and re-running the table above.

Testing

The new test fails without the nav-menu.php change and is the only failure of the 106 in
the menu group, so it fails on its own assertion rather than a precondition. With the
change, menu is 106/106 and customize is 243/243. phpcs passes on both files.

Trac ticket: https://core.trac.wordpress.org/ticket/65884

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Drafting this description and a first pass at the test. I confirmed the mechanism
myself against the source, produced the before/after table by running the real function,
verified the rename alone is what changes the outcome, checked the test fails without the
patch, ran the menu and customize suites and phpcs, and I take responsibility for the
change.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

`wp_map_nav_menu_locations()` used `$slug` for both the loop over the slug
group and a loop nested three levels inside it. PHP leaves a `foreach`
variable at its last assigned value, so once a new location descended into
the old-location loops, every later new location in that pass was compared
against the wrong slug and skipped.

A skipped location was only recovered if it happened to match a later slug
in the same group, which is why `test_location_guessing_one_menu_per_location()`
passes: `main` is itself a group slug. A location matching exactly one slug,
such as `primary-menu`, had nothing to pick it up and lost its assignment.

See #65884.
@github-actions

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props bejignesh.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant