Skip to content

Commit 059ffee

Browse files
author
razvan
committed
feat: integrate Oxygen and WooCommerce analyzers into WordPress pipeline
- Add OxygenInfo and WooCommerceInfo fields to WordPressInfo struct - Add oxygen.Analyzer and woocommerce.Analyzer to WordPress Analyzer - Call Oxygen analyzer in analyzeWordPress() for OxyEl element detection - Call WooCommerce analyzer for hook classification by area (cart, checkout, product, etc.) - Convert Oxygen elements/templates and WC hooks/API calls to CodeChunks - Break import cycle: woocommerce package no longer imports wordpress - Define WPHookInput in woocommerce as local mirror of WPHook - Reimplement AST helpers locally in woocommerce package - Add integration tests: - TestAnalyzer_OxygenElementDetection (end-to-end OxyEl detection) - TestAnalyzer_WooCommerceHookClassification (end-to-end WC area classification) - TestConvertToChunks_OxygenAndWooCommerce (nil safety)
1 parent 1cf637b commit 059ffee

6 files changed

Lines changed: 424 additions & 49 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Thumbs.db
6060
# Temporary files
6161
tmp/
6262
temp/
63+
docs/plans/*.md
6364

6465
# Scripts
6566
scripts/

pkg/parser/php/wordpress/analyzer.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515

1616
pkgParser "github.com/doITmagic/rag-code-mcp/pkg/parser"
1717
"github.com/doITmagic/rag-code-mcp/pkg/parser/php"
18+
"github.com/doITmagic/rag-code-mcp/pkg/parser/php/wordpress/oxygen"
19+
"github.com/doITmagic/rag-code-mcp/pkg/parser/php/wordpress/woocommerce"
1820
)
1921

2022
// Analyzer is the main WordPress framework analyzer that coordinates all WordPress-specific analyzers
@@ -26,6 +28,8 @@ type Analyzer struct {
2628
widgetAnalyzer *WidgetAnalyzer
2729
adminAnalyzer *AdminAnalyzer
2830
pluginHeaderAnalyzer *PluginHeaderAnalyzer
31+
oxygenAnalyzer *oxygen.Analyzer
32+
woocommerceAnalyzer *woocommerce.Analyzer
2933
phpAnalyzer *php.CodeAnalyzer
3034
}
3135

@@ -39,6 +43,8 @@ func NewAnalyzer() *Analyzer {
3943
widgetAnalyzer: NewWidgetAnalyzer(),
4044
adminAnalyzer: NewAdminAnalyzer(),
4145
pluginHeaderAnalyzer: NewPluginHeaderAnalyzer(),
46+
oxygenAnalyzer: oxygen.NewAnalyzer(),
47+
woocommerceAnalyzer: woocommerce.NewAnalyzer(),
4248
phpAnalyzer: php.NewCodeAnalyzer(),
4349
}
4450
}
@@ -147,6 +153,31 @@ func (a *Analyzer) analyzeWordPress(packages []*php.PackageInfo, paths []string)
147153
})
148154
}
149155

156+
// Oxygen Builder analysis (reuses already-parsed packages)
157+
oxyInfo := a.oxygenAnalyzer.AnalyzeFromPackages(packages)
158+
if oxyInfo != nil && (len(oxyInfo.Elements) > 0 || len(oxyInfo.Templates) > 0) {
159+
info.OxygenInfo = oxyInfo
160+
}
161+
162+
// WooCommerce analysis: classify WP hooks that have woocommerce_ prefix
163+
var wcInputHooks []woocommerce.WPHookInput
164+
for _, h := range info.Hooks {
165+
wcInputHooks = append(wcInputHooks, woocommerce.WPHookInput{
166+
Type: string(h.Type),
167+
Name: h.Name,
168+
Callback: h.Callback,
169+
Priority: h.Priority,
170+
FilePath: h.FilePath,
171+
StartLine: h.StartLine,
172+
EndLine: h.EndLine,
173+
})
174+
}
175+
wcHooks := a.woocommerceAnalyzer.AnalyzeHooksFromWP(wcInputHooks)
176+
if len(wcHooks) > 0 {
177+
wcInfo := &woocommerce.WooCommerceInfo{Hooks: wcHooks}
178+
info.WooCommerceInfo = wcInfo
179+
}
180+
150181
return info
151182
}
152183

@@ -344,6 +375,92 @@ func (a *Analyzer) convertToChunks(info *WordPressInfo) []php.CodeChunk {
344375
})
345376
}
346377

378+
// Convert Oxygen elements
379+
if info.OxygenInfo != nil {
380+
if oxyInfo, ok := info.OxygenInfo.(*oxygen.OxygenInfo); ok {
381+
for _, elem := range oxyInfo.Elements {
382+
chunks = append(chunks, php.CodeChunk{
383+
Name: elem.ClassName,
384+
Type: "oxy_element",
385+
Language: "php",
386+
FilePath: elem.FilePath,
387+
StartLine: elem.StartLine,
388+
EndLine: elem.EndLine,
389+
Signature: fmt.Sprintf("class %s extends OxyEl", elem.ClassName),
390+
Docstring: fmt.Sprintf("Oxygen Builder Element: %s (methods: %s)", elem.ClassName, strings.Join(elem.Methods, ", ")),
391+
Metadata: map[string]any{
392+
"framework": "wordpress",
393+
"wp_type": "oxygen_element",
394+
"namespace": elem.Namespace,
395+
"has_slug": elem.SlugMethod,
396+
"methods": elem.Methods,
397+
},
398+
})
399+
}
400+
401+
for _, tmpl := range oxyInfo.Templates {
402+
chunks = append(chunks, php.CodeChunk{
403+
Name: tmpl.PostType,
404+
Type: "oxy_template",
405+
Language: "php",
406+
FilePath: tmpl.FilePath,
407+
StartLine: tmpl.Line,
408+
EndLine: tmpl.Line,
409+
Signature: fmt.Sprintf("register_post_type('%s', ...)", tmpl.PostType),
410+
Docstring: fmt.Sprintf("Oxygen Template: %s", tmpl.PostType),
411+
Metadata: map[string]any{
412+
"framework": "wordpress",
413+
"wp_type": "oxygen_template",
414+
},
415+
})
416+
}
417+
}
418+
}
419+
420+
// Convert WooCommerce hooks
421+
if info.WooCommerceInfo != nil {
422+
if wcInfo, ok := info.WooCommerceInfo.(*woocommerce.WooCommerceInfo); ok {
423+
for _, wcHook := range wcInfo.Hooks {
424+
chunks = append(chunks, php.CodeChunk{
425+
Name: wcHook.HookName,
426+
Type: "wc_hook",
427+
Language: "php",
428+
FilePath: wcHook.FilePath,
429+
StartLine: wcHook.StartLine,
430+
EndLine: wcHook.EndLine,
431+
Signature: fmt.Sprintf("%s('%s', '%s')", wcHook.HookType, wcHook.HookName, wcHook.Callback),
432+
Docstring: fmt.Sprintf("WooCommerce %s hook (%s area): %s", wcHook.HookType, wcHook.Area, wcHook.HookName),
433+
Metadata: map[string]any{
434+
"framework": "wordpress",
435+
"wp_type": "wc_hook",
436+
"wc_area": string(wcHook.Area),
437+
"hook_type": wcHook.HookType,
438+
"callback": wcHook.Callback,
439+
"priority": wcHook.Priority,
440+
},
441+
})
442+
}
443+
444+
for _, apiCall := range wcInfo.APICalls {
445+
chunks = append(chunks, php.CodeChunk{
446+
Name: apiCall.Function,
447+
Type: "wc_api_call",
448+
Language: "php",
449+
FilePath: apiCall.FilePath,
450+
StartLine: apiCall.StartLine,
451+
EndLine: apiCall.EndLine,
452+
Signature: fmt.Sprintf("%s(...)", apiCall.Function),
453+
Docstring: fmt.Sprintf("WooCommerce API call: %s (category: %s)", apiCall.Function, apiCall.Category),
454+
Metadata: map[string]any{
455+
"framework": "wordpress",
456+
"wp_type": "wc_api_call",
457+
"wc_category": apiCall.Category,
458+
},
459+
})
460+
}
461+
}
462+
}
463+
347464
return chunks
348465
}
349466

pkg/parser/php/wordpress/analyzer_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,3 +385,138 @@ func TestMergePostTypes_Deduplication(t *testing.T) {
385385
t.Errorf("expected 2 post types after merge, got %d", len(result))
386386
}
387387
}
388+
389+
func TestAnalyzer_OxygenElementDetection(t *testing.T) {
390+
tmpDir := t.TempDir()
391+
392+
// Plugin header to detect as WordPress
393+
err := os.WriteFile(filepath.Join(tmpDir, "plugin.php"), []byte(`<?php
394+
/**
395+
* Plugin Name: Oxygen Custom Elements
396+
*/
397+
`), 0644)
398+
if err != nil {
399+
t.Fatal(err)
400+
}
401+
402+
// Oxygen element extending OxyEl
403+
oxyFile := filepath.Join(tmpDir, "elements.php")
404+
err = os.WriteFile(oxyFile, []byte(`<?php
405+
class MyCustomHeader extends OxyEl {
406+
public function slug() {
407+
return 'my-custom-header';
408+
}
409+
public function render($options, $defaults, $content) {
410+
echo '<h1>Custom</h1>';
411+
}
412+
}
413+
`), 0644)
414+
if err != nil {
415+
t.Fatal(err)
416+
}
417+
418+
analyzer := NewAnalyzer()
419+
chunks, err := analyzer.AnalyzePaths([]string{tmpDir})
420+
if err != nil {
421+
t.Fatalf("AnalyzePaths failed: %v", err)
422+
}
423+
424+
// Find oxy_element chunks
425+
var oxyChunks []string
426+
for _, c := range chunks {
427+
if c.Type == "oxy_element" {
428+
oxyChunks = append(oxyChunks, c.Name)
429+
if c.Metadata["framework"] != "wordpress" {
430+
t.Errorf("Oxygen chunk %s: expected framework=wordpress", c.Name)
431+
}
432+
if c.Metadata["wp_type"] != "oxygen_element" {
433+
t.Errorf("Oxygen chunk %s: expected wp_type=oxygen_element, got %v", c.Name, c.Metadata["wp_type"])
434+
}
435+
}
436+
}
437+
438+
if len(oxyChunks) == 0 {
439+
t.Error("expected at least 1 oxy_element chunk, got 0")
440+
} else {
441+
t.Logf("Found Oxygen elements: %v", oxyChunks)
442+
}
443+
}
444+
445+
func TestAnalyzer_WooCommerceHookClassification(t *testing.T) {
446+
tmpDir := t.TempDir()
447+
448+
// Plugin header
449+
err := os.WriteFile(filepath.Join(tmpDir, "plugin.php"), []byte(`<?php
450+
/**
451+
* Plugin Name: WooCommerce Extension
452+
*/
453+
`), 0644)
454+
if err != nil {
455+
t.Fatal(err)
456+
}
457+
458+
// WC hooks
459+
wcFile := filepath.Join(tmpDir, "wc-hooks.php")
460+
err = os.WriteFile(wcFile, []byte(`<?php
461+
add_action('woocommerce_before_cart', 'custom_cart_notice');
462+
add_filter('woocommerce_product_get_price', 'custom_price', 10, 2);
463+
add_action('woocommerce_checkout_process', 'validate_checkout');
464+
`), 0644)
465+
if err != nil {
466+
t.Fatal(err)
467+
}
468+
469+
analyzer := NewAnalyzer()
470+
chunks, err := analyzer.AnalyzePaths([]string{tmpDir})
471+
if err != nil {
472+
t.Fatalf("AnalyzePaths failed: %v", err)
473+
}
474+
475+
// Find wc_hook chunks
476+
wcAreas := make(map[string]bool)
477+
for _, c := range chunks {
478+
if c.Type == "wc_hook" {
479+
area, _ := c.Metadata["wc_area"].(string)
480+
wcAreas[area] = true
481+
if c.Metadata["framework"] != "wordpress" {
482+
t.Errorf("WC chunk %s: expected framework=wordpress", c.Name)
483+
}
484+
}
485+
}
486+
487+
if len(wcAreas) == 0 {
488+
t.Error("expected WC hook chunks, got 0")
489+
}
490+
if !wcAreas["cart"] {
491+
t.Error("expected wc_area=cart for woocommerce_before_cart")
492+
}
493+
if !wcAreas["product"] {
494+
t.Error("expected wc_area=product for woocommerce_product_get_price")
495+
}
496+
if !wcAreas["checkout"] {
497+
t.Error("expected wc_area=checkout for woocommerce_checkout_process")
498+
}
499+
500+
t.Logf("Found WC areas: %v", wcAreas)
501+
}
502+
503+
func TestConvertToChunks_OxygenAndWooCommerce(t *testing.T) {
504+
analyzer := NewAnalyzer()
505+
506+
// Use concrete types for OxygenInfo and WooCommerceInfo
507+
info := &WordPressInfo{
508+
Hooks: []WPHook{
509+
{Type: HookAction, Name: "init", Callback: "my_init", FilePath: "test.php", StartLine: 1, EndLine: 1},
510+
},
511+
}
512+
513+
// Test that convertToChunks handles nil OxygenInfo/WooCommerceInfo gracefully
514+
chunks := analyzer.convertToChunks(info)
515+
if len(chunks) != 1 {
516+
t.Fatalf("expected 1 chunk (hook), got %d", len(chunks))
517+
}
518+
if chunks[0].Type != "wp_hook" {
519+
t.Errorf("expected wp_hook, got %s", chunks[0].Type)
520+
}
521+
}
522+

pkg/parser/php/wordpress/types.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ package wordpress
22

33
// WordPressInfo contains WordPress-specific framework information extracted from a project
44
type WordPressInfo struct {
5-
Hooks []WPHook `json:"hooks,omitempty"`
6-
PostTypes []PostType `json:"post_types,omitempty"`
7-
Taxonomies []Taxonomy `json:"taxonomies,omitempty"`
8-
Shortcodes []Shortcode `json:"shortcodes,omitempty"`
9-
Blocks []Block `json:"blocks,omitempty"`
10-
BlockPatterns []BlockPattern `json:"block_patterns,omitempty"`
11-
Widgets []Widget `json:"widgets,omitempty"`
12-
AdminPages []AdminPage `json:"admin_pages,omitempty"`
13-
Settings []Setting `json:"settings,omitempty"`
14-
PluginHeader *PluginHeader `json:"plugin_header,omitempty"`
5+
Hooks []WPHook `json:"hooks,omitempty"`
6+
PostTypes []PostType `json:"post_types,omitempty"`
7+
Taxonomies []Taxonomy `json:"taxonomies,omitempty"`
8+
Shortcodes []Shortcode `json:"shortcodes,omitempty"`
9+
Blocks []Block `json:"blocks,omitempty"`
10+
BlockPatterns []BlockPattern `json:"block_patterns,omitempty"`
11+
Widgets []Widget `json:"widgets,omitempty"`
12+
AdminPages []AdminPage `json:"admin_pages,omitempty"`
13+
Settings []Setting `json:"settings,omitempty"`
14+
PluginHeader *PluginHeader `json:"plugin_header,omitempty"`
15+
OxygenInfo any `json:"oxygen,omitempty"` // *oxygen.OxygenInfo (avoid import cycle)
16+
WooCommerceInfo any `json:"woocommerce,omitempty"` // *woocommerce.WooCommerceInfo (avoid import cycle)
1517
}
1618

1719
// HookType represents the type of a WordPress hook

0 commit comments

Comments
 (0)