Free tool · MIT licensed

WordPress PHP 8 upgrade scanner

Point it at a WordPress codebase. It reports what in your plugins and themes stops working on a modern PHP, what still runs but warns, and what works today and is worth fixing while you are in there — with a file, a line and a column for every hit. One file, no dependencies, no signup, no network access.

Download wp-scan.mjs

Or fetch and run it in one go:

curl -O https://practicalmodules.com/tools/wp-scan.mjs
node wp-scan.mjs /path/to/wordpress

It also takes a single plugin or theme directory, which is the quickest way to try it:

node wp-scan.mjs /path/to/wp-content/plugins/my-plugin
node wp-scan.mjs /path/to/wordpress --markdown > upgrade-inventory.md
node wp-scan.mjs /path/to/wordpress --json
node wp-scan.mjs --rules

Three severities, because "deprecated" covers two very different problems

A PHP upgrade inventory that lumps everything together is not a work plan. Findings are grouped by what actually happens when you upgrade:

Nine decisions, 53 rules, each citing the version that broke it

Inside each severity, findings are filed under one of nine decisions, so the output reads as a plan rather than a wall of grep hits. Every rule names the PHP or WordPress version that removed or deprecated the thing it matches, so you can check a finding against the upstream changelog instead of taking it on trust. --rules prints the whole table.

DecisionRules
1. Know what is installed and who owns itfrom file headers
2. Remove PHP that no longer exists14
3. Fix what PHP deprecated but still runs9
4. Replace deprecated WordPress APIs9
5. Make database access prepared and portable3
6. Stop bypassing WordPress for HTTP, files, and paths9
7. Escape output and guard direct file access5
8. Decide the jQuery and front-end upgrade4
9. Set the target versions and rehearse the upgradefrom file headers

Decisions 1 and 9 carry no rules on purpose. They are answered from the plugin and theme headers — the Requires PHP and Tested up to lines, or their absence — not from pattern matching, and the report says so rather than quietly dropping them.

It skips what an update replaces anyway

Counting a create_function() inside somebody else's plugin buries the lines that are actually yours, so the scanner does not count it.

What the output looks like

The top of a report on a small site — this one is the synthetic install in the project's own test suite:

WHAT IS HERE
3 components of your own (8 PHP files, 119 lines), 1 distributed
32 signals: 9 that stop working, 10 that raise deprecation notices,
           13 portability or security problems
9 of 9 decisions have evidence behind them
3 core files skipped (wp-admin, wp-includes, wp-*.php)
2 files skipped inside distributed plugins and themes
1 file never opened (credentials): wp-config.php

YOUR CODE
  Acme Legacy Tools  [plugin]  wp-content/plugins/acme-legacy
      v1.4.2, no Requires PHP header
      24 signals in 5 files
  Acme Theme  [theme]  wp-content/themes/acme-theme
      v2.0.1, requires PHP 7.4, tested up to WP 5.9
      6 signals in 2 files

And a finding, with the line it matched printed underneath so you can judge it without opening the file:

THIS STOPS WORKING

  Decision 2. Remove PHP that no longer exists
  ------------------------------------------------------------------

  create_function()  —  1 hit in 1 file
    stops working  ·  PHP 8.0 (deprecated 7.2)
    Removed outright. Each call becomes a closure, and callbacks
    registered this way stop firing.
      wp-content/plugins/acme-legacy/acme-legacy.php:11:21
        add_action( 'init', create_function( '', 'return true;' ) );

THIS WORKS, AND IS A PROBLEM WORTH FIXING NOW

  Decision 5. Make database access prepared and portable
  ------------------------------------------------------------------

  SQL with an interpolated variable and no prepare()  —  1 hit in 1 file
    works, but is a portability or security problem
      ·  always wrong; $wpdb->prepare() exists for it
    A variable dropped straight into the query string. This is the
    classic WordPress SQL injection, and an upgrade is the cheapest
    time to fix it.
      wp-content/plugins/acme-legacy/includes/db.php:7:12
        $rows = $wpdb->get_results( "SELECT * FROM wp_posts
                 WHERE post_status = '$status'" );

Add --markdown for a version you can paste straight into a ticket, or --json for the full structured report.

What it does not do

How it is tested

39 checks against a synthetic WordPress install written for the purpose. Beyond counting the expected findings, the suite asserts the exact set of rules allowed to fire, so a new rule that quietly matches fails the build; gives every regex alternative a sample line and checks it survives the hint prefilter first, which has already caught two alternatives that could never have fired; asserts that known false-positive shapes — wp_mail() against mail(), curl_exec() against exec(), maybe_unserialize() against unserialize() — do not match; and re-reads every reported file to confirm the text at the stated line and column really is what the rule matched.

If you are here for Drupal instead

The Drupal 7 migration inventory scanner is the same idea for a Drupal 7 codebase, and it has a free kickoff checklist and a migration workbook behind it.

There is no equivalent WordPress guide or workbook yet. This page ends at the free tool, and it is free, MIT licensed, and stands on its own.