Plugin Analyzer
Plugin Analyzer Roadmap
The plugin-analyzer extracts WordPress plugin functionality into YAML manifests that can be used by the plugin-generator to scaffold Minnow-native plugins.
Current State: 22 extractors implemented, successfully analyzed Gravity Forms (~100k lines) and ACF Pro (~74k lines) into comprehensive manifests.
Implemented Extractors
| Extractor | Detects | Status |
|---|---|---|
TableExtractor |
Database schema from $wpdb->query(), dbDelta() |
✅ Complete |
EntityExtractor |
Data models from class patterns | ✅ Complete |
RestEndpointExtractor |
REST API routes from register_rest_route() and register_rest_field() |
✅ Complete |
AdminPageExtractor |
Admin menus from add_menu_page(), add_submenu_page() |
✅ Complete |
AjaxExtractor |
AJAX handlers from wp_ajax_* hooks |
✅ Complete |
ShortcodeExtractor |
Shortcodes from add_shortcode() |
✅ Complete |
FieldTypeExtractor |
Custom field types (Gravity Forms, ACF, WPForms) | ✅ Complete |
OptionsExtractor |
Options from get_option(), update_option() |
✅ Complete |
ActionExtractor |
Action hooks from add_action(), do_action() |
✅ Complete |
FilterExtractor |
Filter hooks from add_filter(), apply_filters() |
✅ Complete |
LifecycleExtractor |
Activation/deactivation hooks | ✅ Complete |
MetaboxExtractor |
Meta boxes from add_meta_box() |
✅ Complete |
BlockExtractor |
Gutenberg blocks from register_block_type(), acf_register_block_type(), wc_register_block_type(), block.json |
✅ Complete |
PostTypeExtractor |
Custom post types from register_post_type() |
✅ Complete |
TaxonomyExtractor |
Custom taxonomies from register_taxonomy() |
✅ Complete |
FrontendDependencyExtractor |
Scripts/styles from wp_enqueue_*() |
✅ Complete |
CronExtractor |
wp_schedule_event(), cron hooks |
✅ Complete |
TransientExtractor |
get_transient(), set_transient() |
✅ Complete |
MetaFieldExtractor |
register_meta(), get_post_meta() |
✅ Complete |
CapabilityExtractor |
Capability checks and registrations | ✅ Complete |
TranslationExtractor |
i18n function usage and text domain | ✅ Complete |
NotificationExtractor |
Email notification patterns | ✅ Complete |
ACF Pro Test Results (2026-02-04)
What Was Detected
| Section | Count | Notes |
|---|---|---|
| Post Types | 5 ✅ | All 5 internal CPTs: acf-field-group, acf-field, acf-post-type, acf-taxonomy, acf-ui-options-page |
| Admin Pages | 7 ✅ | Options page, Updates, Tools, Options Preview, Upgrade, etc. |
| AJAX Actions | 20 ✅ | Gallery, repeater, clone, select queries |
| Actions | 222 ✅ | Extensive hook coverage |
| Filters | 148 ✅ | Extensive hook coverage |
| Metaboxes | 5 ✅ | Field group metaboxes |
| Shortcodes | 1 ✅ | [acf] shortcode |
| Cron Tasks | 1 ✅ | acf_update_site_health_data |
| Transients | 7 ✅ | License, updates, notices |
| Meta Fields | 11 ✅ | Basic coverage |
| Options | 15 ✅ | ACF settings |
| Capabilities | 4 ✅ | Core caps detected |
| i18n Strings | 2502 ✅ | Full translation coverage |
| Frontend JS | 15 ✅ | All script handles |
| Frontend CSS | 10 ✅ | All style handles |
What Was Missed (Gaps) — ✅ ALL RESOLVED
| Section | Expected | Before | After (2026-02-05) |
|---|---|---|---|
| REST Fields | acf/v1 field extensions | 0 detected | 1 detected (acf field on dynamic post types) |
| Blocks | ACF block types | 0 via PHP wrappers | Wrapper detection added (ACF Pro uses block.json, not PHP wrappers) |
| Field Types | ~38 types | 0 detected | 38 detected with source: acf, 39 confirmed registered: true |
| Entities | 5 from post types | 0 detected | 5 post-type-derived entities (AcfFieldGroup, AcfField, AcfPostType, AcfTaxonomy, AcfUiOptionsPage) |
Phase 1: ACF-Discovered Improvements ✅ COMPLETE (2026-02-05)
1.1 REST Field Extractor Enhancement ✅ COMPLETE
Gap: RestApiExtractor only detects register_rest_route(). ACF and other plugins use register_rest_field() to extend existing endpoints.
Pattern to detect:
register_rest_field(
$base, // post type, taxonomy, or 'user'
'acf', // field name
[
'get_callback' => [$this, 'load_fields'],
'update_callback' => [$this, 'update_fields'],
]
);
Output format:
api:
rest_fields:
- base: post # or user, term, etc.
field: acf
get_callback: ACF_Rest_Api::load_fields
update_callback: ACF_Rest_Api::update_fields
Files modified:
src/Blueprint/RestField.php- New blueprint classsrc/Blueprint/Blueprint.php- Added$restFields,addRestField(), serialization underapi.rest_fieldssrc/Extractor/RestApiExtractor.php- Addedregister_rest_field()detection with base, field name, callbacks
1.2 Block Extractor Enhancement for Wrapper Functions ✅ COMPLETE
Gap: BlockExtractor only detects register_block_type(). ACF and other plugins use wrapper functions.
Patterns to detect:
register_block_type()- WordPress core ✅ Doneregister_block_type_from_metadata()- WordPress core ✅ Doneacf_register_block_type()- ACFacf_register_block()- ACF aliaswc_register_block_type()- WooCommerce- Custom wrapper functions (heuristic: function name contains 'register' + 'block')
Files modified:
src/Extractor/BlockExtractor.php- Addedacf_register_block_type(),acf_register_block(),wc_register_block_type()detection. Extractsnamefrom settings array, prependsacf/orwc/prefix when needed.
1.3 Field Type Extractor Enhancement for ACF ✅ COMPLETE
Gap: FieldTypeExtractor only detects GF_Field extensions.
| Patterns to detect: | Plugin | Base Class | Type Property | Registration |
|---|---|---|---|---|
| Gravity Forms | GF_Field |
$type property |
GF_Fields::register() |
|
| ACF | acf_field |
$this->name in initialize() |
acf_register_field_type() |
|
| WPForms | WPForms\Fields\Field |
$type property |
— |
ACF field pattern:
class acf_field_text extends acf_field {
function initialize() {
$this->name = 'text';
$this->label = __('Text', 'acf');
}
}
acf_register_field_type('acf_field_text');
Files modified:
src/Extractor/FieldTypeExtractor.php- Addedacf_register_field_type()call detection viagetFunctionName()helper,trackRegistration()method. Field types confirmed via registration getregistered: truein output.src/Blueprint/FieldType.php- Added$registeredproperty and serialization.
1.4 Post Type to Entity Mapping ✅ COMPLETE
Gap: Analyzer detects post types but generator expects entities format.
Enhancement: Generate entity-like structures from internal post types.
Input (postTypes):
postTypes:
- name: acf-field-group
labels: { name: 'Field Groups' }
public: false
hierarchical: true
Additional output (entities derived from postTypes):
entities:
AcfFieldGroup:
source: post_type
post_type: acf-field-group
meta_fields:
- key: _acf_location
- key: _acf_fields
Files modified:
src/Blueprint/Entity.php- Added$sourceand$postTypeproperties with serializationsrc/Converter/PostTypeToEntityConverter.php- New converter that creates entities from CPTs with standard post properties and matching meta fieldssrc/Analyzer.php- CallsPostTypeToEntityConverterafterTableToEntityConverter
Phase 2: Analysis Improvements ⬜ PENDING
Improve accuracy and coverage of existing extractors.
2.1 HTTP API Usage Extractor
Detects:
wp_remote_get()/wp_remote_post()/wp_remote_request()- External API endpoints being called
- Authentication patterns (API keys, OAuth)
WordPress Pattern:
$response = wp_remote_get('https://api.example.com/data', [
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
],
'timeout' => 30,
]);
Output YAML:
external_apis:
- url_pattern: "https://api.example.com/*"
methods: [GET, POST]
auth_type: bearer_token
locations:
- file: includes/api-client.php
line: 45
Files to create:
src/Extractor/HttpExtractor.phpsrc/Blueprint/HttpBlueprint.php
2.2 Rewrite Rules Extractor
Detects:
add_rewrite_rule()/add_rewrite_tag()add_rewrite_endpoint()- Flush rewrite patterns
WordPress Pattern:
add_rewrite_rule(
'^products/([^/]+)/?$',
'index.php?post_type=product&product_slug=$matches[1]',
'top'
);
add_rewrite_tag('%product_slug%', '([^/]+)');
Output YAML:
rewrite:
rules:
- pattern: "^products/([^/]+)/?$"
rewrite: "index.php?post_type=product&product_slug=$matches[1]"
position: top
tags:
- name: "%product_slug%"
regex: "([^/]+)"
Files to create:
src/Extractor/RewriteExtractor.phpsrc/Blueprint/RewriteBlueprint.php
2.3 Widget Extractor
Detects:
- Classes extending
WP_Widget wp_add_dashboard_widget()calls- Widget registration via
register_widget()
WordPress Pattern:
class My_Widget extends WP_Widget {
public function __construct() {
parent::__construct('my_widget', 'My Widget');
}
public function widget($args, $instance) { /* ... */ }
public function form($instance) { /* ... */ }
public function update($new_instance, $old_instance) { /* ... */ }
}
register_widget('My_Widget');
Output YAML:
widgets:
sidebar:
- id: my_widget
class: My_Widget
title: My Widget
dashboard:
- id: my_dashboard_widget
title: Stats Overview
callback: render_stats_widget
Files to create:
src/Extractor/WidgetExtractor.phpsrc/Blueprint/WidgetBlueprint.php
Phase 3: Advanced Analysis ⬜ PENDING
3.1 Dependency Graph
Build a graph of how plugin components depend on each other.
Features:
- Track which functions call which hooks
- Map database tables to entities that use them
- Identify circular dependencies
- Generate visual dependency diagram
Output:
dependencies:
entities:
Entry:
depends_on: [Form, User]
used_by: [EntryMeta, Note]
hooks:
gform_after_submission:
triggers: [send_notifications, create_entry_meta]
depends_on: [Entry, Form]
3.2 Code Complexity Analysis
Identify complex areas that may need manual attention during migration.
Metrics:
- Cyclomatic complexity per function
- Lines of code per class
- Number of WordPress function calls
- Direct database queries (bypassing ORM)
Output:
complexity:
high_complexity_files:
- file: includes/form-processing.php
complexity: 45
reason: "Heavy conditional logic, 12 nested if statements"
direct_db_queries:
- file: includes/export.php
line: 123
query: "SELECT * FROM {$wpdb->prefix}entries WHERE..."
recommendation: "Convert to Entity query"
3.3 Migration Difficulty Score
Calculate an overall difficulty score for migrating each plugin.
Factors:
- Number of WordPress-specific functions used
- Custom database operations
- JavaScript/AJAX complexity
- Third-party integrations
- Code quality/documentation
Output:
migration_score:
overall: 7.2/10 # Higher = more difficult
breakdown:
database_complexity: 8/10
wordpress_coupling: 6/10
frontend_complexity: 7/10
documentation: 5/10
estimated_effort: "40-60 hours"
blockers:
- "Uses wp_cron extensively (15 scheduled tasks)"
- "Direct $wpdb queries in 8 locations"
- "Custom Walker classes for menus"
Phase 4: Output Enhancements ⬜ PENDING
4.1 Migration Guide Generation
Generate a human-readable migration guide alongside the YAML.
Output: MIGRATION.md
# Migration Guide: Gravity Forms → Minnow
## Overview
- **Difficulty:** Medium-High (7.2/10)
- **Estimated Effort:** 40-60 hours
- **WordPress Functions Used:** 234
- **Database Tables:** 6
## Step 1: Database Schema
The following tables need to be created...
## Step 2: Entity Classes
Generate entity classes for...
## Step 3: Manual Attention Required
These areas need manual review:
- Custom email templates (15 files)
- JavaScript form validation
...
4.2 Diff Report
When re-analyzing, show what changed since last analysis.
Output: CHANGES.md
# Changes Since Last Analysis
## New Components
- Added shortcode: [gform_confirmation]
- Added REST endpoint: /forms/{id}/duplicate
## Modified Components
- Entity Form: Added 2 new properties
- Admin page settings: 3 new fields
## Removed Components
- Deprecated function: gform_legacy_export()
4.3 Interactive HTML Report
Generate a browsable HTML report for non-technical stakeholders.
Features:
- Collapsible sections for each component type
- Search/filter functionality
- Visual charts for complexity metrics
- Print-friendly view
Phase 5: CLI Improvements ⬜ PENDING
5.1 Incremental Analysis
Only re-analyze changed files for faster iteration.
# Full analysis
minnow analyze /path/to/plugin
# Incremental (only changed files)
minnow analyze /path/to/plugin --incremental
# Force full re-analysis
minnow analyze /path/to/plugin --force
5.2 Watch Mode
Automatically re-analyze when files change.
minnow analyze /path/to/plugin --watch
5.3 Comparison Mode
Compare two plugins or two versions of the same plugin.
# Compare two plugins
minnow analyze:compare /path/to/plugin-a /path/to/plugin-b
# Compare versions
minnow analyze:compare /path/to/plugin@v1.0 /path/to/plugin@v2.0
Summary
| Phase | Components | Status |
|---|---|---|
| Phase 1 | ACF-discovered improvements (4 items) | ✅ Complete |
| Phase 2 | 3 new extractors (HTTP, Rewrite, Widget) | ⬜ Pending |
| Phase 3 | 3 advanced analysis features | ⬜ Pending |
| Phase 4 | 3 output formats | ⬜ Pending |
| Phase 5 | 3 CLI features | ⬜ Pending |
Implemented extractors: 22 (see table above) Total new extractors remaining: 3
Lessons Learned (from Gravity Forms Implementation)
Issues discovered during manual testing that should inform analyzer improvements:
Issue 1: Route Patterns Not Normalized for Minnow ✅ FIXED (2026-02-05)
Problem: RestEndpointExtractor outputs WordPress-style regex patterns directly:
routes:
- path: '/forms/(?P<id>\d+)'
method: GET
controller: 'FormController::get'
Impact: Minnow's RouteRegistry expects {param} format, not (?P<param>\d+). Routes fail to match because preg_quote() escapes the regex characters.
Fix: Added normalizeRoutePath() static method to RestApiExtractor that converts WordPress regex patterns to Minnow {param} format during extraction. Applied automatically before route storage.
| Pattern Conversion: | WordPress Pattern | Minnow Pattern |
|---|---|---|
(?P<id>\d+) |
{id} |
|
(?P<slug>[^/]+) |
{slug} |
|
(?P<type>post\|page) |
{type} |
Files modified: src/Extractor/RestApiExtractor.php
Issue 2: Database Table Prefix Context ✅ FIXED (2026-02-05)
Problem: Extracted table names don't include information about whether they use WordPress prefix.
Context: Gravity Forms tables are {prefix}gf_form, {prefix}gf_entry, etc. The generator needs to know these use the WordPress table prefix.
Fix: Added uses_prefix tracking throughout the extraction pipeline:
TableandEntityblueprints now carry ausesPrefixpropertySchemaExtractortracks which tables were discovered via$wpdb->prefixpatterns, including$wpdb->prefix . self::TABLE_NAMEclass constant concatenationsTableToEntityConverterpropagates the flag from tables to entities- Generator passes
uses_prefixthrough toEntityTemplate, which uses it instead of hardcodingtrue
Output:
tables:
gf_form:
uses_prefix: true
entities:
Form:
uses_prefix: true
Files modified: Blueprint/Table.php, Blueprint/Entity.php, Extractor/SchemaExtractor.php, Converter/TableToEntityConverter.php
Generator files modified: Generator.php, Template/EntityTemplate.php
Issue 3: Mail Hook Discovery ✅ PARTIALLY RESOLVED (2026-02-06)
Problem: The analyzer did not discover that Gravity SMTP intercepts WordPress's wp_mail() function. The original plugin hooks into wp_mail to route emails through SMTP providers and log them. This hook-based email interception pattern was not extracted, so the generated plugin had no email logging.
Impact: The generated plugin's Email Log page was empty — emails sent via minnow mail:send were never logged because no hook listener was generated to capture mail events.
Resolution: The analyzer already extracts wp_mail, wp_mail_failed, and phpmailer_init hooks in its hooks.actions/hooks.filters output. The generator now maps these to Minnow equivalents and generates hook listener classes automatically via the new HookListenerTemplate. See plugin-generator.md Issues #11/#12.
What was done:
- ✅ Generator reads analyzer's hook output and maps WP hooks → Minnow hooks
- ✅ Generator creates
src/Hook/MailLogger.phpwithonAfterSendandonSendFailedmethods - ✅ Generated callbacks use fully-qualified class names
Still pending (low priority):
- ⬜ Analyzer could flag plugins as "email interceptor" in manifest metadata
- ⬜ Analyzer could annotate mail-specific hooks with semantic meaning
| WordPress → Minnow mail hook mapping (implemented in generator): | WordPress Hook | Minnow Hook | Purpose |
|---|---|---|---|
wp_mail |
minnow.mail.after_send |
Log outgoing mail | |
phpmailer_init |
minnow.mail.after_send |
Log outgoing mail | |
wp_mail_failed |
minnow.mail.send_failed |
Log failures |
Files modified:
tools/plugin-generator/src/Template/HookListenerTemplate.php— New template for hook listener generationtools/plugin-generator/src/Generator.php— AddedextractHookListeners()and generation block
Issue 4: Hook Callback Namespace Resolution ✅ RESOLVED (2026-02-06)
Problem: The analyzer extracts WordPress action/filter callbacks but doesn't normalize them for Minnow's hook registration format. The Minnow PluginLoader::resolveCallback() method only prepends the plugin namespace when the callback class contains no backslash. Callbacks like Hook\MailLogger::onAfterSend (with a sub-namespace) are treated as already fully-qualified and fail to resolve.
Impact: Hooks with sub-namespace callbacks silently fail — class_exists('Hook\MailLogger') returns false because the full class is GravitySMTP\Hook\MailLogger.
Resolution: The generator's new HookListenerTemplate always emits fully-qualified class names (Option 2). For example, GravitySMTP\Hook\MailLogger::onAfterSend — the namespace is derived from the manifest's namespace field. See plugin-generator.md Issue #12.