Add JQHTML-EVENT-01 rule, document custom component events, update jqhtml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-12 03:42:24 +00:00
parent c4ba2b743f
commit 5d5bd85e42
19 changed files with 364 additions and 79 deletions

View File

@@ -0,0 +1,197 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\Core\Manifest\Manifest;
/**
* Detects use of jQuery .trigger() for custom events in jqhtml components.
*
* Custom events should use the jqhtml event bus (this.trigger()) instead of
* jQuery's trigger() because the jqhtml bus supports firing events that
* occurred before handlers were registered - critical for component lifecycle.
*/
class JqhtmlCustomEvent_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Standard jQuery/DOM events that are legitimate to trigger via jQuery
*/
private const ALLOWED_EVENTS = [
// Mouse events
'click',
'dblclick',
'mousedown',
'mouseup',
'mouseover',
'mouseout',
'mouseenter',
'mouseleave',
'mousemove',
'contextmenu',
// Keyboard events
'keydown',
'keyup',
'keypress',
// Form events
'submit',
'change',
'input',
'focus',
'blur',
'focusin',
'focusout',
'select',
'reset',
// Touch events
'touchstart',
'touchend',
'touchmove',
'touchcancel',
// Drag events
'drag',
'dragstart',
'dragend',
'dragover',
'dragenter',
'dragleave',
'drop',
// Scroll/resize
'scroll',
'resize',
// Load events
'load',
'unload',
'error',
// Clipboard events
'copy',
'cut',
'paste',
];
public function get_id(): string
{
return 'JQHTML-EVENT-01';
}
public function get_name(): string
{
return 'JQHTML Custom Event Check';
}
public function get_description(): string
{
return 'Custom events in jqhtml components should use this.trigger() instead of jQuery .trigger()';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get JavaScript class from manifest metadata
$js_classes = [];
if (isset($metadata['class']) && isset($metadata['extension']) && $metadata['extension'] === 'js') {
$js_classes = [$metadata['class']];
}
// If no classes in metadata, nothing to check
if (empty($js_classes)) {
return;
}
// Check each class to see if it's a JQHTML component
foreach ($js_classes as $class_name) {
// Use Manifest to check inheritance
if (!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// The $contents passed to check() is already sanitized (strings removed).
// We need the original file content to extract event names from string literals.
$original_contents = file_get_contents($file_path);
$original_lines = explode("\n", $original_contents);
// Sanitized lines are used to skip comments
$sanitized_lines = explode("\n", $contents);
// Look for .trigger() calls on this.$ or that.$
foreach ($original_lines as $line_number => $line) {
$actual_line_number = $line_number + 1;
// Use sanitized line to check if this is a comment
$sanitized_line = $sanitized_lines[$line_number] ?? '';
$trimmed = trim($sanitized_line);
// Skip comments (check sanitized version which preserves comment markers)
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for exception comment on this line or previous line (use original)
if (str_contains($line, '@' . $this->get_id() . '-EXCEPTION')) {
continue;
}
if ($line_number > 0 && str_contains($original_lines[$line_number - 1], '@' . $this->get_id() . '-EXCEPTION')) {
continue;
}
// Match patterns like: this.$.trigger( or that.$.trigger(
// Also match: this.$sid('x').trigger( or that.$sid('x').trigger(
if (preg_match('/(this|that)\.\$(?:sid\s*\([^)]+\))?\s*\.\s*trigger\s*\(\s*[\'"]([^\'"]+)[\'"]/', $line, $matches)) {
$event_name = $matches[2];
// Check if it's an allowed standard event
if (in_array(strtolower($event_name), self::ALLOWED_EVENTS, true)) {
continue;
}
$this->add_violation(
$file_path,
$actual_line_number,
"Custom event '{$event_name}' triggered via jQuery .trigger() in Component class '{$class_name}'. " .
'Use the jqhtml event bus instead.',
trim($line),
"Use jqhtml event system for custom events:\n\n" .
"FIRING EVENTS:\n" .
" Instead of: this.\$.trigger('{$event_name}', data)\n" .
" Use: this.trigger('{$event_name}', data)\n\n" .
"LISTENING FROM PARENT (using \$sid):\n" .
" Instead of: this.\$sid('child').on('{$event_name}', handler)\n" .
" Use: this.sid('child').on('{$event_name}', handler)\n\n" .
"LISTENING FROM EXTERNAL CODE:\n" .
" Instead of: \$(element).on('{$event_name}', handler)\n" .
" Use: \$(element).component().on('{$event_name}', handler)\n\n" .
"WHY: The jqhtml event bus fires callbacks for events that occurred before the handler " .
"was registered. This is critical for component lifecycle - parent components may " .
"register handlers after child components have already fired their events during initialization.",
'high'
);
}
}
}
}
}

View File

@@ -865,7 +865,7 @@ LIFECYCLE EVENT CALLBACKS
(useful for re-renders)
- Multiple callbacks: Can register multiple for same event
- Chaining: Returns this so you can chain .on() calls
- Only lifecycle events allowed: Other event names log error
- Custom events also supported (see CUSTOM COMPONENT EVENTS)
Example - Wait for component initialization:
// Initialize nested components after parent ready
@@ -881,6 +881,72 @@ LIFECYCLE EVENT CALLBACKS
Available in JQHTML v2.2.81+
CUSTOM COMPONENT EVENTS
Components can fire and listen to custom events using the jqhtml event
bus. Unlike jQuery's .trigger()/.on(), the jqhtml event bus guarantees
callback execution even if the event fired before the handler was
registered. This is critical for component lifecycle coordination.
Firing Events:
// Fire event from within a component
this.trigger('my_event');
this.trigger('my_event', { key: 'value' });
Listening to Events:
From parent component (using $sid):
// In parent's on_ready()
this.sid('child_component').on('my_event', (component, data) => {
console.log('Event from:', component);
console.log('Event data:', data);
});
From external code:
$('#element').component().on('my_event', (component, data) => {
// Handle event
});
Callback Signature:
.on('event_name', (component, data) => { ... })
- component: The component instance that fired the event
- data: Optional data passed as second argument to trigger()
Key Difference from jQuery Events:
jQuery events are lost if fired before handler registration:
// jQuery - BROKEN: Event fires before handler exists
child.$.trigger('initialized'); // Lost!
parent.$sid('child').on('initialized', handler); // Never called
JQHTML events work regardless of timing:
// JQHTML - WORKS: Event queued, fires when handler registers
child.trigger('initialized'); // Queued
parent.sid('child').on('initialized', handler); // Called immediately!
This enables reliable parent-child communication during component
initialization, when event timing is unpredictable.
Example - Child notifies parent of state change:
// Child component (Tab_Bar.js)
set_active_tab(key) {
this.state.active_tab = key;
this.render();
this.trigger('tab:change', { key: key });
}
// Parent component (Settings_Page.js)
on_ready() {
this.sid('tabs').on('tab:change', (component, data) => {
this.show_panel(data.key);
});
}
IMPORTANT: Do not use jQuery's .trigger() for custom events in jqhtml
components. The code quality rule JQHTML-EVENT-01 enforces this.
$REDRAWABLE ATTRIBUTE - LIGHTWEIGHT COMPONENTS
Convert any HTML element into a re-renderable component using the
$redrawable attribute. This parser-level transformation enables