Add SPA+Route code quality rule, async eval support for rsx:debug
Tighten CLAUDE.md from 44KB to 35KB without information loss 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,21 @@ The Code Quality system is a modular, extensible framework for enforcing coding
|
|||||||
- Suggests placeholder URLs for unimplemented routes
|
- Suggests placeholder URLs for unimplemented routes
|
||||||
- Severity: High
|
- Severity: High
|
||||||
|
|
||||||
|
### Manifest Rules (`Rules/Manifest/`)
|
||||||
|
|
||||||
|
1. **SpaAttributeMisuseRule** (PHP-SPA-01)
|
||||||
|
- Detects #[SPA] combined with #[Route] on same method
|
||||||
|
- #[SPA] is for bootstrap entry points only, not route definitions
|
||||||
|
- Routes in SPA modules are defined in JavaScript actions with @route()
|
||||||
|
- Runs at manifest-time for immediate feedback
|
||||||
|
- Severity: Critical
|
||||||
|
|
||||||
|
2. **InstanceMethodsRule** (MANIFEST-INST-01)
|
||||||
|
- Enforces static-only classes unless marked Instantiatable
|
||||||
|
- Checks both PHP (#[Instantiatable]) and JS (@Instantiatable)
|
||||||
|
- Walks inheritance chain to check ancestors
|
||||||
|
- Severity: Medium
|
||||||
|
|
||||||
### Sanity Check Rules (`Rules/SanityChecks/`)
|
### Sanity Check Rules (`Rules/SanityChecks/`)
|
||||||
|
|
||||||
1. **PhpSanityCheckRule** (PHP-SC-001)
|
1. **PhpSanityCheckRule** (PHP-SC-001)
|
||||||
@@ -470,6 +485,8 @@ By default, code quality rules run only when `php artisan rsx:check` is executed
|
|||||||
Only the following rules are approved for manifest-time execution:
|
Only the following rules are approved for manifest-time execution:
|
||||||
- **BLADE-SCRIPT-01** (InlineScriptRule): Prevents inline JavaScript in Blade files (critical architecture violation)
|
- **BLADE-SCRIPT-01** (InlineScriptRule): Prevents inline JavaScript in Blade files (critical architecture violation)
|
||||||
- **JQHTML-INLINE-01** (JqhtmlInlineScriptRule): Prevents inline scripts/styles in Jqhtml template files (critical architecture violation)
|
- **JQHTML-INLINE-01** (JqhtmlInlineScriptRule): Prevents inline scripts/styles in Jqhtml template files (critical architecture violation)
|
||||||
|
- **PHP-SPA-01** (SpaAttributeMisuseRule): Prevents combining #[SPA] with #[Route] attributes (critical architecture misunderstanding)
|
||||||
|
- **MANIFEST-INST-01** (InstanceMethodsRule): Enforces static-only classes unless Instantiatable (framework convention)
|
||||||
|
|
||||||
All other rules should return `false` from `is_called_during_manifest_scan()`.
|
All other rules should return `false` from `is_called_during_manifest_scan()`.
|
||||||
|
|
||||||
|
|||||||
159
app/RSpade/CodeQuality/Rules/Manifest/SpaAttributeMisuse_CodeQualityRule.php
Executable file
159
app/RSpade/CodeQuality/Rules/Manifest/SpaAttributeMisuse_CodeQualityRule.php
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\RSpade\CodeQuality\Rules\Manifest;
|
||||||
|
|
||||||
|
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpaAttributeMisuseRule - Detects incorrect use of #[SPA] with #[Route] attributes
|
||||||
|
*
|
||||||
|
* The #[SPA] attribute marks a method as an SPA bootstrap entry point. It is NOT
|
||||||
|
* for defining routes - routes in SPA modules are handled entirely by JavaScript
|
||||||
|
* actions using the @route() decorator.
|
||||||
|
*
|
||||||
|
* Common mistake: Adding #[Route('/path')] to an #[SPA] method, thinking it defines
|
||||||
|
* the SPA's URL. This is wrong because:
|
||||||
|
* 1. #[SPA] methods serve as bootstraps, loading the JavaScript SPA shell
|
||||||
|
* 2. All actual routes are defined in JS action classes with @route() decorator
|
||||||
|
* 3. The SPA router matches URLs to actions client-side, not server-side
|
||||||
|
*/
|
||||||
|
class SpaAttributeMisuse_CodeQualityRule extends CodeQualityRule_Abstract
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the unique rule identifier
|
||||||
|
*/
|
||||||
|
public function get_id(): string
|
||||||
|
{
|
||||||
|
return 'PHP-SPA-01';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get human-readable rule name
|
||||||
|
*/
|
||||||
|
public function get_name(): string
|
||||||
|
{
|
||||||
|
return 'SPA Attribute Misuse';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rule description
|
||||||
|
*/
|
||||||
|
public function get_description(): string
|
||||||
|
{
|
||||||
|
return 'Detects incorrect combination of #[SPA] and #[Route] attributes on the same method';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file patterns this rule applies to
|
||||||
|
*/
|
||||||
|
public function get_file_patterns(): array
|
||||||
|
{
|
||||||
|
return ['*.php'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This rule runs during manifest scan for immediate feedback
|
||||||
|
*/
|
||||||
|
public function is_called_during_manifest_scan(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default severity for this rule
|
||||||
|
*/
|
||||||
|
public function get_default_severity(): string
|
||||||
|
{
|
||||||
|
return 'critical';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the manifest for SPA + Route attribute combinations
|
||||||
|
*/
|
||||||
|
public function check(string $file_path, string $contents, array $metadata = []): void
|
||||||
|
{
|
||||||
|
static $already_checked = false;
|
||||||
|
|
||||||
|
// Only check once per manifest build
|
||||||
|
if ($already_checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$already_checked = true;
|
||||||
|
|
||||||
|
// Get all manifest files
|
||||||
|
$files = \App\RSpade\Core\Manifest\Manifest::get_all();
|
||||||
|
if (empty($files)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($files as $file => $file_metadata) {
|
||||||
|
// Skip non-PHP files
|
||||||
|
if (($file_metadata['extension'] ?? '') !== 'php') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no public static methods
|
||||||
|
if (empty($file_metadata['public_static_methods'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each method
|
||||||
|
foreach ($file_metadata['public_static_methods'] as $method_name => $method_info) {
|
||||||
|
$attributes = $method_info['attributes'] ?? [];
|
||||||
|
|
||||||
|
// Check if method has both #[SPA] and #[Route]
|
||||||
|
$has_spa = isset($attributes['SPA']);
|
||||||
|
$has_route = isset($attributes['Route']);
|
||||||
|
|
||||||
|
if ($has_spa && $has_route) {
|
||||||
|
$line = $method_info['line'] ?? 1;
|
||||||
|
$class_name = $file_metadata['class'] ?? 'Unknown';
|
||||||
|
|
||||||
|
$this->add_violation(
|
||||||
|
$file,
|
||||||
|
$line,
|
||||||
|
"Method '{$method_name}' has both #[SPA] and #[Route] attributes. These should not be combined.",
|
||||||
|
"#[SPA]\n#[Route('...')]\npublic static function {$method_name}(...)",
|
||||||
|
$this->build_suggestion($class_name, $method_name),
|
||||||
|
'critical'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build detailed suggestion explaining SPA architecture
|
||||||
|
*/
|
||||||
|
private function build_suggestion(string $class_name, string $method_name): string
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
$lines[] = "The #[SPA] and #[Route] attributes serve different purposes and should not be combined.";
|
||||||
|
$lines[] = "";
|
||||||
|
$lines[] = "WHAT #[SPA] DOES:";
|
||||||
|
$lines[] = " - Marks a method as an SPA bootstrap entry point";
|
||||||
|
$lines[] = " - Returns rsx_view(SPA) which loads the JavaScript SPA shell";
|
||||||
|
$lines[] = " - Acts as a catch-all for client-side routing";
|
||||||
|
$lines[] = " - Typically ONE per feature/bundle (e.g., Frontend_Spa_Controller::index)";
|
||||||
|
$lines[] = "";
|
||||||
|
$lines[] = "HOW SPA ROUTING WORKS:";
|
||||||
|
$lines[] = " - Routes are defined in JavaScript action classes with @route() decorator";
|
||||||
|
$lines[] = " - Example: @route('/contacts') on Contacts_Index_Action.js";
|
||||||
|
$lines[] = " - The SPA router matches URLs to actions CLIENT-SIDE";
|
||||||
|
$lines[] = " - Server only provides the bootstrap shell, not individual pages";
|
||||||
|
$lines[] = "";
|
||||||
|
$lines[] = "TO FIX:";
|
||||||
|
$lines[] = " 1. Remove the #[Route] attribute from the #[SPA] method";
|
||||||
|
$lines[] = " 2. Create JavaScript action classes for each route you need:";
|
||||||
|
$lines[] = "";
|
||||||
|
$lines[] = " // Example: rsx/app/frontend/contacts/Contacts_Index_Action.js";
|
||||||
|
$lines[] = " @route('/contacts')";
|
||||||
|
$lines[] = " @layout('Frontend_Layout')";
|
||||||
|
$lines[] = " @spa('{$class_name}::{$method_name}')";
|
||||||
|
$lines[] = " class Contacts_Index_Action extends Spa_Action { }";
|
||||||
|
$lines[] = "";
|
||||||
|
$lines[] = "See: php artisan rsx:man spa";
|
||||||
|
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -548,6 +548,13 @@ class Route_Debug_Command extends Command
|
|||||||
$this->line(' php artisan rsx:debug /demo --eval="Rsx.is_dev()" --no-body');
|
$this->line(' php artisan rsx:debug /demo --eval="Rsx.is_dev()" --no-body');
|
||||||
$this->line('');
|
$this->line('');
|
||||||
|
|
||||||
|
$this->comment('POST-LOAD INTERACTIONS (click buttons, test modals, etc):');
|
||||||
|
$this->line(' php artisan rsx:debug /page --user=1 --eval="$(\'[data-sid=btn_edit]\').click(); await new Promise(r => setTimeout(r, 2000));"');
|
||||||
|
$this->line(' # Click button, wait 2s for modal');
|
||||||
|
$this->line(' php artisan rsx:debug /form --eval="$(\'#submit\').click(); await new Promise(r => setTimeout(r, 1000));"');
|
||||||
|
$this->line(' # Submit form and capture result');
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
$this->comment('DEBUGGING OUTPUT:');
|
$this->comment('DEBUGGING OUTPUT:');
|
||||||
$this->line(' php artisan rsx:debug / --console # All console output');
|
$this->line(' php artisan rsx:debug / --console # All console output');
|
||||||
$this->line(' php artisan rsx:debug / --console-log # Alias for --console');
|
$this->line(' php artisan rsx:debug / --console-log # Alias for --console');
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ function parse_args() {
|
|||||||
console.log(' --dump-element=<sel> Extract and display HTML of specific element');
|
console.log(' --dump-element=<sel> Extract and display HTML of specific element');
|
||||||
console.log(' --storage Dump localStorage and sessionStorage contents');
|
console.log(' --storage Dump localStorage and sessionStorage contents');
|
||||||
console.log(' --full Enable all display options for maximum info');
|
console.log(' --full Enable all display options for maximum info');
|
||||||
console.log(' --eval=<code> Execute JavaScript code in the page context');
|
console.log(' --eval=<code> Execute async JavaScript after page loads (supports await)');
|
||||||
|
console.log(' Example: --eval="$(\'[data-sid=btn_edit]\').click(); await new Promise(r => setTimeout(r, 2000));"');
|
||||||
console.log(' --timeout=<ms> Navigation timeout in milliseconds (minimum 30000ms, default 30000ms)');
|
console.log(' --timeout=<ms> Navigation timeout in milliseconds (minimum 30000ms, default 30000ms)');
|
||||||
console.log(' --console-debug-filter=<ch> Filter console_debug to specific channel');
|
console.log(' --console-debug-filter=<ch> Filter console_debug to specific channel');
|
||||||
console.log(' --console-debug-benchmark Include benchmark timing in console_debug');
|
console.log(' --console-debug-benchmark Include benchmark timing in console_debug');
|
||||||
@@ -638,52 +639,32 @@ function parse_args() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute eval code if --eval option is passed
|
// Execute eval code if --eval option is passed
|
||||||
|
// This runs AFTER page is fully loaded and ready, supports async/await
|
||||||
if (options.eval_code) {
|
if (options.eval_code) {
|
||||||
try {
|
try {
|
||||||
const evalResult = await page.evaluate((code) => {
|
const evalResult = await page.evaluate(async (code) => {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
// Wrap the eval code in a function
|
// Wrap code in async function to support await
|
||||||
const __eval_func = function() {
|
const asyncFunc = new Function('return (async () => { ' + code + ' })()');
|
||||||
|
const result = await asyncFunc();
|
||||||
|
|
||||||
|
// Convert result to string representation
|
||||||
|
if (result === undefined) {
|
||||||
|
return 'undefined';
|
||||||
|
} else if (result === null) {
|
||||||
|
return 'null';
|
||||||
|
} else if (typeof result === 'object') {
|
||||||
try {
|
try {
|
||||||
// Use eval directly for more complex expressions
|
return JSON.stringify(result, null, 2);
|
||||||
const result = eval(code);
|
} catch (e) {
|
||||||
|
return String(result);
|
||||||
// Convert result to string representation
|
|
||||||
if (result === undefined) {
|
|
||||||
return 'undefined';
|
|
||||||
} else if (result === null) {
|
|
||||||
return 'null';
|
|
||||||
} else if (typeof result === 'object') {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(result, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
return String(result);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return String(result);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return `Error: ${error.message}`;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Wait for RSX framework to be fully initialized
|
|
||||||
if (window.Rsx && window.Rsx.on) {
|
|
||||||
// Use Rsx._debug_ready event which fires after all initialization
|
|
||||||
window.Rsx.on('_debug_ready', function() {
|
|
||||||
resolve(__eval_func());
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback for non-RSX pages
|
return String(result);
|
||||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
||||||
setTimeout(function() { resolve(__eval_func()); }, 500);
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
setTimeout(function() { resolve(__eval_func()); }, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
|
return `Error: ${error.message}`;
|
||||||
|
}
|
||||||
}, options.eval_code);
|
}, options.eval_code);
|
||||||
|
|
||||||
console.log('\nJavaScript Eval Result:');
|
console.log('\nJavaScript Eval Result:');
|
||||||
|
|||||||
Reference in New Issue
Block a user