Minnow

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 class
  • src/Blueprint/Blueprint.php - Added $restFields, addRestField(), serialization under api.rest_fields
  • src/Extractor/RestApiExtractor.php - Added register_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 ✅ Done
  • register_block_type_from_metadata() - WordPress core ✅ Done
  • acf_register_block_type() - ACF
  • acf_register_block() - ACF alias
  • wc_register_block_type() - WooCommerce
  • Custom wrapper functions (heuristic: function name contains 'register' + 'block')

Files modified:

  • src/Extractor/BlockExtractor.php - Added acf_register_block_type(), acf_register_block(), wc_register_block_type() detection. Extracts name from settings array, prepends acf/ or wc/ 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 - Added acf_register_field_type() call detection via getFunctionName() helper, trackRegistration() method. Field types confirmed via registration get registered: true in output.
  • src/Blueprint/FieldType.php - Added $registered property 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 $source and $postType properties with serialization
  • src/Converter/PostTypeToEntityConverter.php - New converter that creates entities from CPTs with standard post properties and matching meta fields
  • src/Analyzer.php - Calls PostTypeToEntityConverter after TableToEntityConverter

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.php
  • src/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.php
  • src/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.php
  • src/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:

  • Table and Entity blueprints now carry a usesPrefix property
  • SchemaExtractor tracks which tables were discovered via $wpdb->prefix patterns, including $wpdb->prefix . self::TABLE_NAME class constant concatenations
  • TableToEntityConverter propagates the flag from tables to entities
  • Generator passes uses_prefix through to EntityTemplate, which uses it instead of hardcoding true

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.php with onAfterSend and onSendFailed methods
  • ✅ 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 generation
  • tools/plugin-generator/src/Generator.php — Added extractHookListeners() 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.