From wp-dev
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/wp-dev:php-cleanupThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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.
| 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 |
composer require --dev \
icanhazstring/composer-unused \
maglnet/composer-require-checker \
vimeo/psalm \
php-stubs/wordpress-stubs \
friendsofphp/php-cs-fixer
# Unused dependency detection
composer require --dev icanhazstring/composer-unused
# Missing dependency detection
composer require --dev maglnet/composer-require-checker
# Static analysis + dead code detection
composer require --dev vimeo/psalm
composer require --dev php-stubs/wordpress-stubs
# Unused import cleanup
composer require --dev friendsofphp/php-cs-fixer
Detects Composer packages listed in composer.json that are never referenced anywhere in the codebase.
# Basic scan
composer unused
# JSON output for parsing
composer unused --output-format=json
# Exclude specific packages from the check
composer unused --excludePackage=php-stubs/wordpress-stubs
# Only check runtime dependencies (skip dev)
composer unused --no-dev
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
// Packages used dynamically or via WordPress hooks
->addNamedFilter(NamedFilter::fromString('php-stubs/wordpress-stubs'))
->addNamedFilter(NamedFilter::fromString('wpackagist-plugin/some-plugin'))
// Ignore all packages matching a pattern
->addPatternFilter(PatternFilter::fromString('/^ext-/'));
};
These packages are often flagged but are actually used:
php-stubs/wordpress-stubs — Type stubs, not runtime codephp-stubs/woocommerce-stubs — Same, for WooCommerceext-json, ext-mbstring) — Provided by PHP itselfThe inverse of composer-unused: finds symbols (classes, functions, constants) that the code references but aren't declared in composer.json.
# Basic check
vendor/bin/composer-require-checker check
# Check a specific composer.json
vendor/bin/composer-require-checker check composer.json
# JSON output
vendor/bin/composer-require-checker check --output=json
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.
Psalm's --find-unused-code flag performs deep static analysis to detect genuinely unreferenced code.
| 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 |
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>
<!-- Add if using WooCommerce -->
<!-- <file name="vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php" /> -->
</plugins>
</psalm>
# Full analysis with unused code detection
vendor/bin/psalm --find-unused-code
# Only report unused code (suppress other errors)
vendor/bin/psalm --find-unused-code --show-info=true
# JSON output for parsing
vendor/bin/psalm --find-unused-code --output-format=json
# Target specific directories
vendor/bin/psalm --find-unused-code src/ includes/
# Generate baseline (accept current state, catch new issues)
vendor/bin/psalm --find-unused-code --set-baseline=psalm-baseline.xml
# Run against baseline
vendor/bin/psalm --find-unused-code --use-baseline=psalm-baseline.xml
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:
/** @psalm-suppress PossiblyUnusedMethod */
public function handle_form_submission(): void {
// Registered via: add_action('admin_post_my_form', [$this, 'handle_form_submission'])
}
Or suppress at the class level for classes that are entirely hook-driven:
/** @psalm-suppress UnusedClass */
class Admin_Ajax_Handler {
// All methods are registered via add_action('wp_ajax_*', ...)
}
/** @psalm-suppress UnusedClass */
/** @psalm-suppress PossiblyUnusedMethod */
/** @psalm-suppress UnusedVariable */
/** @psalm-suppress UnusedProperty */
/** @psalm-suppress UnusedParam */
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.
Detects and auto-removes unused use statements at the top of PHP files.
.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);
# Dry run — show what would change
vendor/bin/php-cs-fixer fix --dry-run --diff --rules='{"no_unused_imports": true}'
# Auto-fix unused imports
vendor/bin/php-cs-fixer fix --rules='{"no_unused_imports": true}'
# Using config file
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 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']);
When cross-referencing Psalm's unused code report against actual hook registrations, search for these patterns:
// Object method callbacks
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']);
// Function callbacks
add_action('hook_name', 'function_name');
add_action('hook_name', __NAMESPACE__ . '\\function_name');
// Closure callbacks (these don't produce false positives)
add_action('hook_name', function() { ... });
// Same patterns apply to add_filter()
add_filter('hook_name', [$this, 'method_name']);
add_filter('hook_name', 'function_name');
// Shortcode handlers
add_shortcode('my_shortcode', [$this, 'render_shortcode']);
// REST API route callbacks
register_rest_route('namespace/v1', '/route', [
'callback' => [$this, 'handle_request'],
'permission_callback' => [$this, 'check_permissions'],
]);
// Widget registration
register_widget(My_Widget::class);
// Block registration callbacks
register_block_type('namespace/block', [
'render_callback' => [$this, 'render_block'],
]);
// Activation/deactivation hooks
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
// Cron callbacks
add_action('my_plugin_cron_event', [$this, 'run_scheduled_task']);
To filter false positives from Psalm output:
add_action, add_filter, add_shortcode, register_rest_route, register_block_type, register_widget, register_activation_hook, register_deactivation_hook.# Find all hook-registered callbacks (method references)
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
# Find method-style callbacks: [$this, 'method_name'] or [self::class, 'method_name']
grep -rnoP "\[\s*(\\\$this|self::class|static::class|[A-Z][a-zA-Z_]*::class)\s*,\s*'([a-zA-Z_]+)'" \
--include="*.php" src/ includes/
# Find function-style callbacks: 'function_name'
grep -rnoP "(add_action|add_filter)\s*\(\s*'[^']+'\s*,\s*'([a-zA-Z_\\\\]+)'" \
--include="*.php" src/ includes/
Follow this order to avoid cascading issues:
vendor/bin/composer-require-checker check
# Add any missing packages
composer require <missing-package>
composer unused
# Review each, then remove
composer remove <unused-package>
vendor/bin/php-cs-fixer fix --rules='{"no_unused_imports": true}'
vendor/bin/psalm --find-unused-code --output-format=json
# Cross-reference against hook registrations
# Remove genuinely unused code manually
composer install # Ensure deps are consistent
vendor/bin/psalm # Type check (without --find-unused-code)
vendor/bin/phpunit # Run tests
# If WordPress: activate plugin in test environment and check for fatal errors
wp plugin activate <plugin-slug> --path=/path/to/wp
When presenting results:
class_exists(), function_exists(), and string-based class instantiationProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub iwritec0de/claude-plugin-marketplace --plugin wp-dev