Skip to content

Warn instead of erroring on WordPress functions the plugin declares - #1503

Open
sanketio wants to merge 6 commits into
WordPress:trunkfrom
sanketio:fix/1496-cross-file-polyfills
Open

sanketio wants to merge 6 commits into
WordPress:trunkfrom
sanketio:fix/1496-cross-file-polyfills

Conversation

@sanketio

@sanketio sanketio commented Oct 14, 2026 •

Copy link
Copy Markdown

What?

Closes #1496

wp_functions_compatibility reports a call to a newer WordPress function that the plugin declares itself, such as a polyfill in a separate compat file, as a warning naming that file instead of an error.

Why?

A polyfill is only recognised when it's in the same file as the call, so the common includes/compat.php pattern fails the check.

How?

When a call would be flagged, the check collects the global functions the plugin declares across its files and reports calls to them as wp_function_declared_by_plugin warnings. Methods, enum and namespaced functions, and use function imports with the same name are still errors.

Testing Instructions

QA steps

  1. Copy tests/phpunit/testdata/plugins/test-plugin-wp-functions-compatibility-with-polyfill-file into wp-content/plugins/.
  2. Tools > Plugin Check → "Test Plugin WP Functions Compatibility With Polyfill File", Plugin Repo category → Check it:
    • No wp_function_not_compatible_with_requires_wp errors
    • uses-polyfilled-function.php lines 8 and 9: wp_function_declared_by_plugin warning naming includes/compat.php
    • Line 10: the same warning naming includes/braced-namespaces.php
    • Line 11: the same warning naming includes/commented-declaration.php
    • Line 12: the same warning naming includes/class-compat-loader.php

Developer checks

  1. wp plugin check tests/phpunit/testdata/plugins/test-plugin-wp-functions-compatibility-with-polyfill-file --checks=wp_functions_compatibility
    • The same five warnings, no compatibility errors
  2. wp plugin check tests/phpunit/testdata/plugins/test-plugin-wp-functions-compatibility-with-non-global-declarations --checks=wp_functions_compatibility
    • Errors on lines 8 and 9

AI Usage Disclosure

  • This PR was created without the help of AI tools
  • This PR includes AI-assisted code or content

Claude Code was used to reproduce the bug and draft the fix, tests and this description; I reviewed and tested the changes.

Open WordPress Playground Preview

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: sanketio <sanketparmar@git.wordpress.org>
Co-authored-by: ernilambar <nilambar@git.wordpress.org>
Co-authored-by: davidperezgar <davidperez@git.wordpress.org>
Co-authored-by: dshanske <dshanske@git.wordpress.org>

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

@ernilambar

Copy link
Copy Markdown
Member

Summary

The PR builds a plugin-wide set of global function declarations and skips flagging calls to any function in that set. That fixes the issue: polyfills in a separate compat.php were not recognised. The approach is right and handles the tricky cases correctly, but CI fails on PHPMD and the new method needs to be split up before merge.

✅ What's good

  • Fixes the problem in Accept PolyFills #1496 at its source. Any function the plugin declares counts as available, whether or not a function_exists() guard sits next to the call. Plugin authors don't need a magic comment or config option.
  • The declaration map is built lazily, only once a call would actually be flagged. Plugins with no incompatible calls pay nothing extra.
  • Correctly ignores class, interface, trait and enum methods, namespaced functions, closures, and Foo::class. Correctly includes namespace { } global blocks, function &name(), and functions nested inside other functions or closures (those are global in PHP). I checked these locally with a reflection harness on PHP 8.4.
  • The anonymous-class handling is careful: a closure passed in the constructor arguments (new class( function () {} ) { ... }) is not mistaken for the class body.
  • Includes a negative test, so methods and namespaced functions with a WP function's name are still flagged.
  • PHPCS is clean. PHPUnit and Behat pass across the full matrix.

⚠️ Must-fix before merge

  • includes/Checker/Checks/Plugin_Repo/WP_Functions_Compatibility_Check.php:311: the PHP Code Linting CI job fails on PHPMD. get_declared_global_function_names() has an NPath complexity of 3,780,008 (limit 200) and a cyclomatic complexity of 35 (limit 20). It also pushes the class to an overall complexity of 105 (limit 100). Reproduced locally with vendor/bin/phpmd ... text phpmd.xml. Split the method up: one method that loops over files, one that collects declarations from a single file's token stream, and small helpers for the brace/paren bookkeeping and namespace detection. This repo has fixed the same PHPMD rule before (ed6e29c, "Refactor guard helper to satisfy NPath limit"), so follow that. If the class is still over 100 after the split, move the token-walking helpers into a trait or a small utility class.

💡 Suggestions (optional)

  • includes/Checker/Checks/Plugin_Repo/WP_Functions_Compatibility_Check.php:384: use function wp_foo; gets recorded as a declaration, because the T_FUNCTION after T_USE is treated like a definition. The result is that a real incompatible call is silently skipped. It's rare, but the fix is one line: skip T_FUNCTION when the previous significant token is T_USE. On PHP 7.4 the same path also records the first segment of use function Foo\bar; (for example foo), because that version tokenizes qualified names as separate T_STRINGs.
  • includes/Checker/Checks/Plugin_Repo/WP_Functions_Compatibility_Check.php:326: every PHP file is now tokenized twice, once in find_wp_function_calls() and again here. Lazy loading keeps this off the common path, so it's fine for now. If it matters later, tokenize each file once and pass the tokens to both passes.
  • tests/phpunit/tests/Checker/Checks/WP_Functions_Compatibility_Check_Tests.php:57: assertArrayNotHasKey( 'uses-polyfilled-function.php', $errors ) would also pass if the error were reported on a different file, such as includes/compat.php. Assert that the whole result has no wp_function_not_compatible_with_requires_wp errors instead.
  • tests/phpunit/testdata/plugins/test-plugin-wp-functions-compatibility-with-non-global-declarations/namespaced.php:8: add a fixture that uses braced namespace syntax: namespace Foo { function wp_x() {} } namespace { function wp_y() {} }. The namespace { reset branch has no test right now.
  • Known limitation, worth noting in the PR description: a function counts as available even if it is declared in a file the plugin never loads, or loaded only after the first call. Static analysis can't do better, and that's an acceptable tradeoff.

Verdict

Request Changes: the logic is correct and fixes #1496, but the PHPMD complexity failure blocks CI and needs a refactor.

@davidperezgar

Copy link
Copy Markdown
Member

Thanks for your contribution. Could you fix the tests before we can review?

@davidperezgar

Copy link
Copy Markdown
Member

I reviewed this PR with Codex. The overall direction makes sense, but I found two issues:

  1. Any matching global function declaration found anywhere in the plugin suppresses the compatibility error, even if that file is never loaded or is loaded after the call. This can hide real undefined-function failures on older WordPress versions.
  2. On PHP 7.4/8.0, T_ENUM is unavailable, so enum methods may be misclassified as global functions.

Additionally, the required PHP CI job currently fails PHPMD because the new method exceeds the configured complexity thresholds. These should be addressed before approval.

@sanketio sanketio changed the title Skip WordPress functions the plugin declares itself Warn instead of erroring on WordPress functions the plugin declares Oct 15, 2026
@sanketio

sanketio commented Oct 15, 2026 •

Copy link
Copy Markdown
Author

@ernilambar Thanks for the thorough review.

Changed in e24845d

  • The scanner is now a Function_Declaration_Utils trait, within the complexity limits.
  • use function imports are no longer counted as declarations.
  • The polyfill test asserts no errors anywhere.
  • A braced-namespace fixture covers the namespace { reset.
  • Calls to declared functions are now a warning naming the declaring file, following davidperezgar's concern.

Not changed

  • Second tokenization: it only runs once a call would be flagged.

@sanketio

sanketio commented Oct 15, 2026 •

Copy link
Copy Markdown
Author

@davidperezgar Agreed that skipping the call could hide a real failure.

Changed in e24845d

  • A call to a function the plugin declares itself is now a warning naming the declaring file, instead of being dropped, so a reviewer can confirm the load order.
  • The complexity issue and the PHP 7.4/8.0 enum case are fixed.

Alternatives not taken

  • Following require/include from the main file: includes built from autoloaders, variables or constants can't be resolved statically, and each miss brings back the false error from Accept PolyFills #1496. It still couldn't tell whether a file loads before or after the call.
  • Counting only function_exists()-guarded declarations: it narrows what counts, but doesn't address unloaded or late-loaded files.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Declaration parsing and scope tracking can incorrectly downgrade compatibility errors or miss valid polyfills.

3 open findings
What changed in this PR

Updates WordPress function compatibility checks to recognize plugin-provided polyfills across files, addressing #1496.

Changes:

  • Collects global function declarations across plugin PHP files.
  • Reports matching compatibility issues as warnings naming the declaring file.
  • Adds fixtures and tests for polyfills and non-global declarations.
File Description
tests/​phpunit/​tests/​Checker/​Checks/​WP_Functions_Compatibility_Check_Tests.php Tests warning and error behavior.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-polyfill-file/​uses-polyfilled-function.php Calls cross-file polyfills.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-polyfill-file/​load.php Loads polyfill fixtures.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-polyfill-file/​includes/​compat.php Declares guarded polyfills.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-polyfill-file/​includes/​braced-namespaces.php Covers braced namespace declarations.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​uses-new-function.php Calls functions without global polyfills.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​namespaced.php Provides a namespaced declaration.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​load.php Loads non-global declaration fixtures.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​imports.php Covers function imports.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​enum.php Covers enum methods.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​class-notices.php Covers class methods.
tests/​phpunit/​testdata/​plugins/​test-plugin-wp-functions-compatibility-with-non-global-declarations/​anonymous-class.php Covers anonymous-class methods.
includes/​Traits/​Function_Declaration_Utils.php Adds global declaration scanning.
includes/​Checker/​Checks/​Plugin_Repo/​WP_Functions_Compatibility_Check.php Integrates declaration-aware warnings.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread includes/Traits/Function_Declaration_Utils.php Outdated
Comment thread includes/Traits/Function_Declaration_Utils.php Outdated
Comment thread includes/Traits/Function_Declaration_Utils.php

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A moderate issue remains in comment handling for namespaced and enum declarations.

1 open issue
3 resolved since last review

Review effort: Lite

Comment on lines +255 to +256
$next_index = $this->get_next_significant_token_index( $tokens, $index );
return null === $next_index ? null : $tokens[ $next_index ];
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.

Accept PolyFills

4 participants