| name | php-cleanup |
| description | This skill should be used when the user asks to "find unused PHP code", "clean up dead code", "find unused Composer dependencies", "find unused imports", "remove dead PHP code", "run composer unused", "check for missing dependencies", "detect unused classes", "detect unused methods", "clean up use statements", "audit PHP dependencies", or mentions "composer-unused", "composer-require-checker", "Psalm unused code", "dead code detection", "unused imports", "unused dependencies", "PHP cleanup", "WordPress dead code". Provides expert guidance on detecting and removing unused code, dependencies, imports, and dead exports in PHP/WordPress projects using composer-unused, composer-require-checker, Psalm, and PHP-CS-Fixer. |
| license | MIT |
| metadata | {"author":"Chris Kelley (hello@iwritecode.io)","version":"1.0.0"} |
PHP/WordPress Dead Code & Dependency Cleanup
A toolchain that replicates what Knip does for JS/TS — detect unused code, unused dependencies, missing dependencies, and dead exports — but for PHP/WordPress plugin and theme development.
Toolchain Overview
| Tool | Purpose | Detects |
|---|
composer-unused | Unused Composer packages | Packages in require/require-dev never referenced in code |
composer-require-checker | Missing Composer packages | Packages used in code but not declared in composer.json |
Psalm --find-unused-code | Dead PHP code | Unused classes, methods, functions, properties, variables |
| PHP-CS-Fixer | Unused use imports | use statements at the top of files that aren't referenced |
| PHP_CodeSniffer | Unused imports (alt) | Same as above, alternative tool |
Installation
All Tools at Once
composer require --dev \
icanhazstring/composer-unused \
maglnet/composer-require-checker \
vimeo/psalm \
php-stubs/wordpress-stubs \
friendsofphp/php-cs-fixer
Individual Installation
composer require --dev icanhazstring/composer-unused
composer require --dev maglnet/composer-require-checker
composer require --dev vimeo/psalm
composer require --dev php-stubs/wordpress-stubs
composer require --dev friendsofphp/php-cs-fixer
Tool 1: composer-unused — Unused Dependencies
Detects Composer packages listed in composer.json that are never referenced anywhere in the codebase.
Usage
composer unused
composer unused --output-format=json
composer unused --excludePackage=php-stubs/wordpress-stubs
composer unused --no-dev
Configuration (composer-unused.php)
<?php
declare(strict_types=1);
use ComposerUnused\ComposerUnused\Configuration\Configuration;
use ComposerUnused\ComposerUnused\Configuration\NamedFilter;
use ComposerUnused\ComposerUnused\Configuration\PatternFilter;
return static function (Configuration $config): Configuration {
return $config
->addNamedFilter(NamedFilter::fromString('php-stubs/wordpress-stubs'))
->addNamedFilter(NamedFilter::fromString('wpackagist-plugin/some-plugin'))
->addPatternFilter(PatternFilter::fromString('/^ext-/'));
};
Common False Positives in WordPress
These packages are often flagged but are actually used:
php-stubs/wordpress-stubs — Type stubs, not runtime code
php-stubs/woocommerce-stubs — Same, for WooCommerce
- Platform extensions (
ext-json, ext-mbstring) — Provided by PHP itself
- WordPress plugins installed via Composer (wpackagist) — May be loaded by WP, not imported directly
Tool 2: composer-require-checker — Missing Dependencies
The inverse of composer-unused: finds symbols (classes, functions, constants) that the code references but aren't declared in composer.json.
Usage
vendor/bin/composer-require-checker check
vendor/bin/composer-require-checker check composer.json
vendor/bin/composer-require-checker check --output=json
Configuration (composer-require-checker.json)
{
"symbol-white-list": [
"null", "true", "false",
"static", "self", "parent",
"array", "string", "int", "float", "bool", "callable", "iterable", "void", "object", "mixed", "never",
"WP_Query", "WP_Post", "WP_User", "WP_Error", "WP_REST_Request", "WP_REST_Response",
"WP_REST_Server", "WP_REST_Controller", "WP_Widget", "WP_Customize_Control",
"wpdb", "WP_Hook", "WP_Screen", "WP_Admin_Bar", "WP_Block", "WP_Block_Type",
"Walker", "Walker_Nav_Menu", "WP_Filesystem_Base",
"ABSPATH", "WPINC", "WP_CONTENT_DIR", "WP_PLUGIN_DIR", "WP_DEBUG",
"add_action", "add_filter", "apply_filters", "do_action",
"register_activation_hook", "register_deactivation_hook",
"wp_enqueue_script", "wp_enqueue_style", "wp_localize_script",
"get_option", "update_option", "delete_option",
"esc_html", "esc_attr", "esc_url", "wp_kses", "wp_kses_post",
"sanitize_text_field", "absint", "wp_unslash",
"wp_nonce_field", "wp_verify_nonce", "check_admin_referer",
"current_user_can", "is_admin", "is_user_logged_in",
"__", "_e", "_x", "_n", "esc_html__", "esc_html_e", "esc_attr__", "esc_attr_e"
]
}
Critical: WordPress core functions, classes, and constants are globally available at runtime but not declared in composer.json. The whitelist above prevents false positives for the most common WordPress symbols. Extend it as needed for your project.
Tool 3: Psalm — Dead Code Detection
Psalm's --find-unused-code flag performs deep static analysis to detect genuinely unreferenced code.
What Psalm Detects
| Category | Example |
|---|
| Unused classes | Classes never instantiated or referenced |
| Unused methods | Methods never called (including private/protected) |
| Unused functions | Standalone functions never invoked |
| Unused properties | Class properties never read or written |
| Unused variables | Variables assigned but never used |
| Unused parameters | Method/function parameters never referenced in the body |
| Dead code paths | Code after return, unreachable else branches |
Configuration (psalm.xml)
<?xml version="1.0"?>
<psalm
errorLevel="4"
findUnusedCode="true"
findUnusedBaselineEntry="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src" />
<directory name="includes" />
<ignoreFiles>
<directory name="vendor" />
<directory name="node_modules" />
<directory name="tests" />
<directory name="build" />
</ignoreFiles>
</projectFiles>
<stubs>
<file name="vendor/php-stubs/wordpress-stubs/wordpress-stubs.php" />
</stubs>
<plugins>
</plugins>
</psalm>
Usage
vendor/bin/psalm --find-unused-code
vendor/bin/psalm --find-unused-code --show-info=true
vendor/bin/psalm --find-unused-code --output-format=json
vendor/bin/psalm --find-unused-code src/ includes/
vendor/bin/psalm --find-unused-code --set-baseline=psalm-baseline.xml
vendor/bin/psalm --find-unused-code --use-baseline=psalm-baseline.xml
Suppressing False Positives
WordPress Hook Callbacks
Functions registered via add_action/add_filter appear unused because they're invoked dynamically by WordPress, not called directly in your code. Suppress per-function:
public function handle_form_submission(): void {
}
Or suppress at the class level for classes that are entirely hook-driven:
class Admin_Ajax_Handler {
}
Inline Suppression Reference
Baseline Approach (Recommended for Existing Projects)
Rather than suppressing hundreds of existing findings, generate a baseline and only enforce on new code:
vendor/bin/psalm --find-unused-code --set-baseline=psalm-baseline.xml
This creates psalm-baseline.xml with all current findings. Future runs will only report NEW unused code.
Tool 4: PHP-CS-Fixer — Unused Import Cleanup
Detects and auto-removes unused use statements at the top of PHP files.
Configuration (.php-cs-fixer.php)
<?php
$finder = PhpCsFixer\Finder::create()
->in([__DIR__ . '/src', __DIR__ . '/includes'])
->exclude(['vendor', 'node_modules', 'build']);
return (new PhpCsFixer\Config())
->setRules([
'no_unused_imports' => true,
'ordered_imports' => [
'sort_algorithm' => 'alpha',
'imports_order' => ['class', 'function', 'const'],
],
])
->setFinder($finder);
Usage
vendor/bin/php-cs-fixer fix --dry-run --diff --rules='{"no_unused_imports": true}'
vendor/bin/php-cs-fixer fix --rules='{"no_unused_imports": true}'
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php
WordPress-Specific: Hook-Aware False Positive Filtering
WordPress plugins register callbacks dynamically. A function like this is NOT unused:
add_action('init', [$this, 'register_post_types']);
add_filter('the_content', 'my_plugin_filter_content');
add_action('wp_ajax_my_action', [$this, 'handle_ajax']);
add_action('wp_ajax_nopriv_my_action', [$this, 'handle_ajax']);
add_filter('plugin_action_links_' . MY_PLUGIN_BASENAME, [$this, 'add_settings_link']);
Hook Registration Patterns to Detect
When cross-referencing Psalm's unused code report against actual hook registrations, search for these patterns:
add_action('hook_name', [$this, 'method_name']);
add_action('hook_name', [$instance, 'method_name']);
add_action('hook_name', [self::class, 'method_name']);
add_action('hook_name', [ClassName::class, 'method_name']);
add_action('hook_name', 'function_name');
add_action('hook_name', __NAMESPACE__ . '\\function_name');
add_action('hook_name', function() { ... });
add_filter('hook_name', [$this, 'method_name']);
add_filter('hook_name', 'function_name');
add_shortcode('my_shortcode', [$this, 'render_shortcode']);
register_rest_route('namespace/v1', '/route', [
'callback' => [$this, 'handle_request'],
'permission_callback' => [$this, 'check_permissions'],
]);
register_widget(My_Widget::class);
register_block_type('namespace/block', [
'render_callback' => [$this, 'render_block'],
]);
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
add_action('my_plugin_cron_event', [$this, 'run_scheduled_task']);
Cross-Reference Strategy
To filter false positives from Psalm output:
- Collect hook registrations — Grep the codebase for
add_action, add_filter, add_shortcode, register_rest_route, register_block_type, register_widget, register_activation_hook, register_deactivation_hook.
- Extract callback names — Parse the second argument to get method/function names.
- Compare against Psalm findings — Any method/function Psalm flags as unused that appears in the hook registration list is a false positive.
- Report separately — Present "genuinely unused" vs "hook-registered (likely valid)" as distinct categories.
Grep Patterns for Hook Extraction
grep -rn "add_action\|add_filter\|add_shortcode\|register_rest_route\|register_block_type\|register_widget\|register_activation_hook\|register_deactivation_hook" \
--include="*.php" src/ includes/ \
| grep -oP "'\K[a-zA-Z_]+(?=')" | sort -u
grep -rnoP "\[\s*(\\\$this|self::class|static::class|[A-Z][a-zA-Z_]*::class)\s*,\s*'([a-zA-Z_]+)'" \
--include="*.php" src/ includes/
grep -rnoP "(add_action|add_filter)\s*\(\s*'[^']+'\s*,\s*'([a-zA-Z_\\\\]+)'" \
--include="*.php" src/ includes/
Safe Removal Workflow
Follow this order to avoid cascading issues:
Step 1: Fix Missing Dependencies First
vendor/bin/composer-require-checker check
composer require <missing-package>
Step 2: Remove Unused Dependencies
composer unused
composer remove <unused-package>
Step 3: Clean Up Unused Imports
vendor/bin/php-cs-fixer fix --rules='{"no_unused_imports": true}'
Step 4: Remove Dead Code (Psalm)
vendor/bin/psalm --find-unused-code --output-format=json
Step 5: Verify Nothing Broke
composer install
vendor/bin/psalm
vendor/bin/phpunit
wp plugin activate <plugin-slug> --path=/path/to/wp
Interpreting Results
When presenting results:
- Group by severity — Missing dependencies are critical; unused imports are low-risk
- Separate hook callbacks — Always cross-reference Psalm's unused findings against hook registrations before recommending removal
- Check dynamic usage — WordPress plugins often use
class_exists(), function_exists(), and string-based class instantiation
- Consider public API — Functions/classes intended for use by other plugins or themes may appear unused within the project itself
- Watch for conditional loading — WordPress plugins sometimes load files conditionally (admin-only, frontend-only) which can create apparent dead code