Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class AjaxReturnValue_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-AJAX-02';
}
public function get_name(): string
{
return 'AJAX Return Value Property Check';
}
public function get_description(): string
{
return "Detects unnecessary access to _ajax_return_value property in AJAX responses";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Check JavaScript files for _ajax_return_value property access
* Ajax.call() already unwraps the response, so accessing _ajax_return_value is unnecessary
* Instead of: data._ajax_return_value.user_id
* Should use: data.user_id
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip Ajax.js itself
if (str_ends_with($file_path, '/Ajax.js')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check files in rsx/ or app/RSpade/
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Check for _ajax_return_value usage
if (str_contains($line, '_ajax_return_value')) {
// Try to extract context for better suggestion
$suggestion = "Ajax.call() automatically unwraps the server response. ";
// Check if we can detect the specific property being accessed
if (preg_match('/(\w+)\._ajax_return_value\.(\w+)/', $line, $matches)) {
$variable = $matches[1];
$property = $matches[2];
$suggestion .= "Instead of '{$variable}._ajax_return_value.{$property}', ";
$suggestion .= "use '{$variable}.{$property}' directly.";
} elseif (preg_match('/(\w+)\._ajax_return_value/', $line, $matches)) {
$variable = $matches[1];
$suggestion .= "Instead of '{$variable}._ajax_return_value', ";
$suggestion .= "use '{$variable}' directly - it already contains the unwrapped response.";
} else {
$suggestion .= "The response from Ajax.call() is already unwrapped, ";
$suggestion .= "so you can access properties directly without the _ajax_return_value wrapper.";
}
$this->add_violation(
$file_path,
$line_number,
"Unnecessary access to '_ajax_return_value' property detected.",
trim($line),
$suggestion,
'medium'
);
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
use App\RSpade\Core\Manifest\Manifest;
/**
* Check decorator usage and enforce whitelisting rules
* - Functions/methods marked with @decorator become whitelisted
* - Global functions can only use @decorator
* - Static and instance methods can only use whitelisted decorators
* - Checks for duplicate global function names
* - Checks for duplicate global const names
* - Checks for conflicts between global function and const names
*/
class DecoratorUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
private array $decorator_whitelist = [];
private array $all_global_functions = [];
private array $all_global_constants = [];
private array $all_global_names = []; // Combined functions and constants for conflict checking
public function get_id(): string
{
return 'JS-DECORATOR-01';
}
public function get_name(): string
{
return 'JavaScript Decorator Usage Check';
}
public function get_description(): string
{
return 'Validates JavaScript decorator usage and whitelisting (static and instance methods)';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
/**
* Run during manifest build for immediate feedback
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only process during manifest-time when we have all files
// This rule needs to scan all files first to build the whitelist
// So we run it once at the end of manifest building
static $already_run = false;
if ($already_run) {
return;
}
// On the first JavaScript file, process all files
if (!empty($metadata) && $metadata['extension'] === 'js') {
$this->process_all_files();
$already_run = true;
}
}
private function process_all_files(): void
{
// Get all files from manifest
$files = Manifest::get_all();
// Step 1: Build decorator whitelist and collect all global functions
foreach ($files as $path => $metadata) {
// Skip non-JavaScript files
if (($metadata['extension'] ?? '') !== 'js') {
continue;
}
// Collect global function names for uniqueness check
if (!empty($metadata['global_function_names'])) {
foreach ($metadata['global_function_names'] as $func_name) {
if (isset($this->all_global_functions[$func_name])) {
// Duplicate function name
$existing_file = $this->all_global_functions[$func_name];
$this->throw_duplicate_global($func_name, 'function', $existing_file, $path);
}
$this->all_global_functions[$func_name] = $path;
// Check for conflict with const names
if (isset($this->all_global_constants[$func_name])) {
$existing_file = $this->all_global_constants[$func_name];
$this->throw_name_conflict($func_name, 'function', $path, 'const', $existing_file);
}
$this->all_global_names[$func_name] = ['type' => 'function', 'file' => $path];
}
}
// Collect global const names for uniqueness check
if (!empty($metadata['global_const_names'])) {
foreach ($metadata['global_const_names'] as $const_name) {
if (isset($this->all_global_constants[$const_name])) {
// Duplicate const name
$existing_file = $this->all_global_constants[$const_name];
$this->throw_duplicate_global($const_name, 'const', $existing_file, $path);
}
$this->all_global_constants[$const_name] = $path;
// Check for conflict with function names
if (isset($this->all_global_functions[$const_name])) {
$existing_file = $this->all_global_functions[$const_name];
$this->throw_name_conflict($const_name, 'const', $path, 'function', $existing_file);
}
$this->all_global_names[$const_name] = ['type' => 'const', 'file' => $path];
}
}
// Check global functions for @decorator
if (!empty($metadata['global_functions_with_decorators'])) {
foreach ($metadata['global_functions_with_decorators'] as $func_name => $func_data) {
$decorators = $func_data['decorators'] ?? [];
$line = $func_data['line'] ?? 0;
foreach ($decorators as $decorator) {
$decorator_name = $decorator['name'] ?? $decorator[0] ?? 'unknown';
if ($decorator_name === 'decorator') {
$this->decorator_whitelist[$func_name] = true;
} else {
// Global function with non-@decorator decorator
$this->throw_global_function_decorator($func_name, $path, $line, $decorator_name);
}
}
}
}
// Check static methods for @decorator
if (!empty($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name === 'decorator') {
$this->decorator_whitelist[$method_name] = true;
}
}
}
}
}
// Check instance methods for @decorator
if (!empty($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name === 'decorator') {
$this->decorator_whitelist[$method_name] = true;
}
}
}
}
}
}
// Step 2: Validate static and instance method decorators against whitelist
foreach ($files as $path => $metadata) {
// Skip non-JavaScript files
if (($metadata['extension'] ?? '') !== 'js') {
continue;
}
$class_name = $metadata['class'] ?? 'Unknown';
// Check static methods
if (!empty($metadata['public_static_methods'])) {
foreach ($metadata['public_static_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name !== 'decorator' && !isset($this->decorator_whitelist[$decorator_name])) {
$this->throw_unwhitelisted_decorator($decorator_name, $class_name, $method_name, $path);
}
}
}
}
}
// Check instance methods
if (!empty($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_data) {
// Check for decorators in compact format
if (!empty($metadata['method_decorators'][$method_name])) {
$compact_decorators = $metadata['method_decorators'][$method_name];
foreach ($compact_decorators as $decorator_data) {
$decorator_name = $decorator_data[0] ?? 'unknown';
if ($decorator_name !== 'decorator' && !isset($this->decorator_whitelist[$decorator_name])) {
$this->throw_unwhitelisted_decorator($decorator_name, $class_name, $method_name, $path);
}
}
}
}
}
}
}
// Note: Duplicate/conflict checking methods removed
// This functionality is now handled by BundleCompiler::_check_js_naming_conflicts()
// which checks only the files being bundled, not all files in the project
private function throw_global_function_decorator(string $func_name, string $path, int $line, string $decorator_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Global function cannot use decorator\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$path}\n";
$error_message .= "Line: {$line}\n";
$error_message .= "Function: {$func_name}\n";
$error_message .= "Decorator: @{$decorator_name}\n\n";
$error_message .= "Global functions may only use the @decorator marker.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The function '{$func_name}' has decorator '@{$decorator_name}'.\n";
$error_message .= "Only the '@decorator' marker is allowed on global functions.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " @memoize\n";
$error_message .= " function myFunction() { ... }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " @decorator\n";
$error_message .= " function myDecorator(target, key, descriptor) { ... }\n\n";
$error_message .= "WHY THIS RESTRICTION:\n";
$error_message .= "- Global functions are processed differently than class methods\n";
$error_message .= "- The @decorator marker identifies decorator implementations\n";
$error_message .= "- Other decorators can only be used on static class methods\n\n";
$error_message .= "FIX OPTIONS:\n";
$error_message .= "1. Remove the '@{$decorator_name}' decorator\n";
$error_message .= "2. Move the function into a class as a static method\n";
$error_message .= "3. If this IS a decorator implementation, use '@decorator' instead\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
private function throw_unwhitelisted_decorator(string $decorator_name, string $class_name, string $method_name, string $path): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Decorator not whitelisted\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$path}\n";
$error_message .= "Class: {$class_name}\n";
$error_message .= "Method: {$method_name}\n";
$error_message .= "Decorator: @{$decorator_name}\n\n";
$error_message .= "The decorator '@{$decorator_name}' is not whitelisted.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "Only decorators marked with @decorator can be used.\n";
$error_message .= "The '{$decorator_name}' function/method needs @decorator marker.\n\n";
$error_message .= "EXAMPLE OF WHITELISTING:\n";
$error_message .= " // Mark the decorator implementation:\n";
$error_message .= " @decorator\n";
$error_message .= " function {$decorator_name}(target, key, descriptor) {\n";
$error_message .= " // Decorator implementation\n";
$error_message .= " return descriptor;\n";
$error_message .= " }\n\n";
$error_message .= " // Or in a class:\n";
$error_message .= " class Decorators {\n";
$error_message .= " @decorator\n";
$error_message .= " static {$decorator_name}(target, key, descriptor) {\n";
$error_message .= " return descriptor;\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "WHY THIS MATTERS:\n";
$error_message .= "- Decorators must be explicitly whitelisted\n";
$error_message .= "- This prevents typos and undefined decorators\n";
$error_message .= "- Ensures decorators are properly implemented\n\n";
$error_message .= "FIX:\n";
$error_message .= "Add @decorator to the '{$decorator_name}' function/method definition.\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DefensiveCoding_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-DEFENSIVE-01';
}
public function get_name(): string
{
return 'JavaScript Defensive Coding Check';
}
public function get_description(): string
{
return 'Prohibits existence checks - code must fail loudly if dependencies are missing';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript file for defensive coding violations (from line 833)
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Pattern 1: typeof variable checks (!== undefined, === undefined, == 'function', etc.)
// Match: typeof SomeVar !== 'undefined' or typeof SomeVar == 'function'
if (preg_match('/typeof\s+(\w+)\s*([!=]=+)\s*[\'"]?(undefined|function)[\'"]?/i', $line, $matches)) {
$variable = $matches[1];
// Skip if it's a property check (contains dot)
if (!str_contains($variable, '.')) {
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Checking if '{$variable}' exists. All classes and variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the existence check. Let the code fail if '{$variable}' is not defined.",
'high'
);
}
}
// Pattern 2: typeof window.variable checks
if (preg_match('/typeof\s+window\.(\w+)\s*([!=]=+)\s*[\'"]?undefined[\'"]?/i', $line, $matches)) {
$variable = 'window.' . $matches[1];
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Checking if '{$variable}' exists. All global variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the existence check. Let the code fail if '{$variable}' is not defined.",
'high'
);
}
// Pattern 3: if (variable) or if (!variable) existence checks (more careful pattern)
// Only match simple variables, not property access
if (preg_match('/if\s*\(\s*(!)?(\w+)\s*\)/', $line, $matches)) {
$variable = $matches[2];
// Skip if it's a property or array access or a boolean-like variable name
if (!str_contains($line, '.' . $variable) &&
!str_contains($line, '[' . $variable) &&
!str_contains($line, $variable . '.') &&
!str_contains($line, $variable . '[') &&
!preg_match('/^(is|has|can|should|will|did|was)[A-Z]/', $variable) && // Skip boolean-named vars
!in_array(strtolower($variable), ['true', 'false', 'null', 'undefined'])) { // Skip literals
// Check if this looks like an existence check by looking at context
if (preg_match('/if\s*\(\s*(!)?typeof\s+' . preg_quote($variable, '/') . '/i', $line) ||
preg_match('/if\s*\(\s*' . preg_quote($variable, '/') . '\s*&&\s*' . preg_quote($variable, '/') . '\./i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Checking if '{$variable}' exists. All classes and variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the existence check. Let the code fail if '{$variable}' is not defined.",
'high'
);
}
}
}
// Pattern 4: Guard clauses like: Rsx && Rsx.method()
if (preg_match('/(\w+)\s*&&\s*\1\.\w+/i', $line, $matches)) {
$variable = $matches[1];
// Skip common boolean variable patterns
if (!preg_match('/^(is|has|can|should|will|did|was)[A-Z]/', $variable)) {
$this->add_violation(
$file_path,
$line_number,
"Defensive coding violation: Guard clause checking if '{$variable}' exists. All classes and variables must be assumed to exist. Code should fail loudly if something is undefined.",
trim($line),
"Remove the guard clause. Use '{$variable}.method()' directly.",
'high'
);
}
}
// Pattern 5: try/catch used for existence checking (simplified detection)
if (preg_match('/try\s*\{.*?(\w+).*?\}\s*catch/i', $line, $matches)) {
// This is a simplified check - in reality you'd need multi-line parsing
// Skip for now as it's complex to detect intent
}
}
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DirectAjaxApi_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-AJAX-01';
}
public function get_name(): string
{
return 'Direct AJAX API Call Check';
}
public function get_description(): string
{
return "Detects direct $.ajax calls to /_ajax/ endpoints instead of using JS controller stubs";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript files for direct $.ajax calls to /_ajax/ endpoints
* Instead of:
* await $.ajax({ url: '/_ajax/Controller/action', ... })
* Should use:
* await Controller.action(params)
* Or:
* await Ajax.call('Controller', 'action', params)
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip Ajax.js itself
if (str_ends_with($file_path, '/Ajax.js')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Only check files in rsx/ or app/RSpade/
if (!str_contains($file_path, '/rsx/') && !str_contains($file_path, '/app/RSpade/')) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if (str_contains($file_path, '/app/RSpade/') && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
// Pattern to match $.ajax({ url: '/_ajax/Controller/action'
// This handles both single-line and multi-line cases
$full_content = implode("\n", $lines);
// Match $.ajax({ with optional whitespace/newlines, then url: with quotes around /_ajax/
// Capture controller and action names for suggestion
$pattern = '/\$\.ajax\s*\(\s*\{[^}]*?url\s*:\s*[\'"](\/_ajax\/([A-Za-z_][A-Za-z0-9_]*)\/([A-Za-z_][A-Za-z0-9_]*))[^\'"]*[\'"]/s';
if (preg_match_all($pattern, $full_content, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $index => $match) {
$matched_text = $match[0];
$offset = $match[1];
$url = $matches[1][$index][0];
$controller = $matches[2][$index][0];
$action = $matches[3][$index][0];
// Find line number
$line_number = substr_count(substr($full_content, 0, $offset), "\n") + 1;
// Get the actual line for display
$line = $lines[$line_number - 1] ?? '';
// Build suggestion
$suggestion = "Instead of direct $.ajax() call to '{$url}', use:\n";
$suggestion .= " 1. Preferred: await {$controller}.{$action}(params)\n";
$suggestion .= " 2. Alternative: await Ajax.call('{$controller}', '{$action}', params)\n";
$suggestion .= "The JS stub handles session expiry, notifications, and response unwrapping.";
$this->add_violation(
$file_path,
$line_number,
"Direct $.ajax() call to internal API endpoint '{$url}' detected. Use JS controller stub instead.",
trim($line),
$suggestion,
'high'
);
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\InitializationSuggestions;
class DocumentReady_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-READY-01';
}
public function get_name(): string
{
return 'JavaScript Document Ready Check';
}
public function get_description(): string
{
return 'Enforces use of ES6 class lifecycle methods instead of window.onload or jQuery ready';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Whether this rule is called during manifest scan
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* document ready patterns prevent the framework's auto-initialization from functioning correctly.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Process file during manifest update to extract document ready violations
*/
public function on_manifest_file_update(string $file_path, string $contents, array $metadata = []): ?array
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return null;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return null;
}
$lines = explode("\n", $contents);
$violations = [];
// Also get sanitized content to skip comments
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments using sanitized version
if (isset($sanitized_lines[$line_num])) {
$sanitized_trimmed = trim($sanitized_lines[$line_num]);
if (empty($sanitized_trimmed)) {
continue; // Skip empty/comment lines
}
}
// Check for window.onload patterns
if (preg_match('/\bwindow\s*\.\s*onload\s*=/', $line)) {
$violations[] = [
'type' => 'window_onload',
'line' => $line_number,
'code' => trim($line)
];
break; // Only need first violation
}
// Check for various jQuery ready patterns and DOMContentLoaded
// Patterns: $().ready, $(document).ready, $("document").ready, $('document').ready, $(function(), DOMContentLoaded
$jquery_ready_patterns = [
'/\$\s*\(\s*\)\s*\.\s*ready\s*\(/', // $().ready(
'/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', // $(document).ready( with spaces
'/\$\s*\(\s*["\']document["\']\s*\)\s*\.\s*ready\s*\(/', // $("document").ready( or $('document').ready(
'/\$\s*\(\s*function\s*\(/', // $(function() - shorthand for $(document).ready
'/jQuery\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', // jQuery(document).ready(
'/jQuery\s*\(\s*["\']document["\']\s*\)\s*\.\s*ready\s*\(/', // jQuery("document").ready( or jQuery('document').ready(
'/jQuery\s*\(\s*function\s*\(/', // jQuery(function() - shorthand
'/document\s*\.\s*addEventListener\s*\(\s*["\']DOMContentLoaded[\"\']/', // document.addEventListener("DOMContentLoaded" or 'DOMContentLoaded'
];
foreach ($jquery_ready_patterns as $pattern) {
if (preg_match($pattern, $line)) {
$violations[] = [
'type' => 'jquery_ready',
'line' => $line_number,
'code' => trim($line)
];
break; // Only report once per line
}
}
// Stop after first violation
if (!empty($violations)) {
break;
}
}
if (!empty($violations)) {
return ['document_ready_violations' => $violations];
}
return null;
}
/**
* Check JavaScript file for document ready violations stored in metadata
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Check for violations in code quality metadata
if (isset($metadata['code_quality_metadata']['JS-READY-01']['document_ready_violations'])) {
$violations = $metadata['code_quality_metadata']['JS-READY-01']['document_ready_violations'];
// Get appropriate suggestion based on code location
$suggestion = InitializationSuggestions::get_suggestion($file_path);
// Throw on first violation
foreach ($violations as $violation) {
$type = $violation['type'];
$line = $violation['line'];
$code = $violation['code'];
if ($type === 'window_onload') {
$error_message = "Code Quality Violation (JS-READY-01) - Prohibited Window Onload Pattern\n\n";
$error_message .= "window.onload is not allowed. Use ES6 class with lifecycle methods instead.\n\n";
} else {
$error_message = "Code Quality Violation (JS-READY-01) - Prohibited jQuery Ready Pattern\n\n";
$error_message .= "jQuery ready/DOMContentLoaded patterns are not allowed. Use ES6 class with lifecycle methods instead.\n\n";
}
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line}\n";
$error_message .= "Code: {$code}\n\n";
$error_message .= $suggestion;
throw new \App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException(
$error_message,
0,
null,
base_path($file_path),
$line
);
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class DomMethod_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-DOM-01';
}
public function get_name(): string
{
return 'JavaScript DOM Method Usage Check';
}
public function get_description(): string
{
return 'Enforces jQuery instead of native DOM methods';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Check for document.getElementById
if (preg_match('/\bdocument\.getElementById\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.getElementById(id)' with '$('#' + id)' or use a jQuery selector directly like $('#myId'). " .
"jQuery provides a more consistent and powerful API for DOM manipulation that works across all browsers.",
'medium'
);
}
// Check for document.createElement
if (preg_match('/\bdocument\.createElement\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.createElement(tagName)' with '$('<' + tagName + '>')' or use jQuery element creation like $('<div>'). " .
"jQuery provides a more fluent API for creating and manipulating DOM elements.",
'medium'
);
}
// Check for document.getElementsByClassName
if (preg_match('/\bdocument\.getElementsByClassName\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.getElementsByClassName(className)' with $('.' + className) or use a jQuery class selector directly like $('.myClass'). " .
"jQuery provides a more consistent API that returns a jQuery object with many useful methods.",
'medium'
);
}
// Check for document.getElementsByTagName
if (preg_match('/\bdocument\.getElementsByTagName\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.getElementsByTagName(tagName)' with $(tagName) or use a jQuery tag selector like $('div'). " .
"jQuery provides a unified API for element selection.",
'medium'
);
}
// Check for document.querySelector
if (preg_match('/\bdocument\.querySelector\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.querySelector(selector)' with $(selector). " .
"jQuery's selector engine is more powerful and consistent across browsers.",
'medium'
);
}
// Check for document.querySelectorAll
if (preg_match('/\bdocument\.querySelectorAll\s*\(/i', $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
"Use jQuery instead of native DOM methods.",
trim($original_line),
"Replace 'document.querySelectorAll(selector)' with $(selector). " .
"jQuery automatically handles collections and provides chainable methods.",
'medium'
);
}
}
}
}

View File

@@ -0,0 +1,171 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\InitializationSuggestions;
class FrameworkInitialization_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-INIT-FW-01';
}
public function get_name(): string
{
return 'Framework Code Initialization Pattern Check';
}
public function get_description(): string
{
return 'Enforces proper initialization patterns for framework JavaScript code in /app/RSpade directory';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check JavaScript file for proper framework initialization patterns
* Framework code in /app/RSpade should use _on_framework_* methods
* User methods (on_modules_*, on_app_*) are forbidden in framework code
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in /app/RSpade directory
if (!str_contains($file_path, '/app/RSpade/')) {
return;
}
// Check if it's in an allowed subdirectory
if (!$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Get original content for pattern detection
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Also get sanitized content to skip comments
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($original_lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments using sanitized version
if (isset($sanitized_lines[$line_num])) {
$sanitized_trimmed = trim($sanitized_lines[$line_num]);
if (empty($sanitized_trimmed)) {
continue; // Skip empty/comment lines
}
}
// Check for user code methods (forbidden in framework code)
$user_methods = [
'on_modules_define',
'on_modules_init',
'on_app_define',
'on_app_init',
'on_app_ready'
];
foreach ($user_methods as $method) {
if (preg_match('/\bstatic\s+(async\s+)?' . preg_quote($method) . '\s*\(\s*\)/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"User initialization method '{$method}' cannot be used in framework code.",
trim($line),
InitializationSuggestions::get_framework_suggestion(),
'critical'
);
}
}
// Check for Rsx.on('ready') pattern
if (preg_match('/\bRsx\s*\.\s*on\s*\(\s*[\'\"]ready[\'\"]/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Rsx.on('ready') is deprecated. Use framework lifecycle methods instead.",
trim($line),
InitializationSuggestions::get_framework_suggestion(),
'high'
);
}
// Check for jQuery ready patterns (should not be in framework code)
if (preg_match('/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', $line) ||
preg_match('/\$\s*\(\s*function\s*\(/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"jQuery ready patterns are not allowed in framework code. Use framework lifecycle methods.",
trim($line),
InitializationSuggestions::get_framework_suggestion(),
'high'
);
}
// Validate correct framework method usage (informational)
$framework_methods = [
'_on_framework_core_define',
'_on_framework_core_init',
'_on_framework_module_define',
'_on_framework_module_init'
];
foreach ($framework_methods as $method) {
if (preg_match('/\bstatic\s+(async\s+)?' . preg_quote($method) . '\s*\(\s*\)/', $line)) {
// This is correct usage - no violation
// Could log this for validation purposes if needed
}
}
}
}
/**
* Check if a file in /app/RSpade/ is in an allowed subdirectory
* Based on scan_directories configuration
*/
private function is_in_allowed_rspade_directory(string $file_path): bool
{
// Get allowed subdirectories from config
$scan_directories = config('rsx.manifest.scan_directories', []);
// Extract allowed RSpade subdirectories
$allowed_subdirs = [];
foreach ($scan_directories as $scan_dir) {
if (str_starts_with($scan_dir, 'app/RSpade/')) {
$subdir = substr($scan_dir, strlen('app/RSpade/'));
if ($subdir) {
$allowed_subdirs[] = $subdir;
}
}
}
// Check if file is in any allowed subdirectory
foreach ($allowed_subdirs as $subdir) {
if (str_contains($file_path, '/app/RSpade/' . $subdir . '/') ||
str_contains($file_path, '/app/RSpade/' . $subdir)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
use App\RSpade\CodeQuality\Support\InitializationSuggestions;
class InitializationPattern_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-INIT-USER-01';
}
public function get_name(): string
{
return 'User Code Initialization Pattern Check';
}
public function get_description(): string
{
return 'Enforces proper initialization patterns for user JavaScript code in /rsx directory';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript file for proper initialization patterns
* User code in /rsx should use on_modules_* or on_app_* methods
* Framework methods (_on_framework_*) are forbidden in user code
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in /rsx directory
if (!str_contains($file_path, '/rsx/')) {
return;
}
// Skip vendor and node_modules
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Get original content for pattern detection
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Also get sanitized content to skip comments
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($original_lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments using sanitized version
if (isset($sanitized_lines[$line_num])) {
$sanitized_trimmed = trim($sanitized_lines[$line_num]);
if (empty($sanitized_trimmed)) {
continue; // Skip empty/comment lines
}
}
// Check for Rsx.on('ready') pattern
if (preg_match('/\bRsx\s*\.\s*on\s*\(\s*[\'"]ready[\'"]/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Rsx.on('ready') is deprecated. Use ES6 class lifecycle methods instead.",
trim($line),
InitializationSuggestions::get_user_suggestion(),
'high'
);
}
// Check for framework methods (forbidden in user code)
$framework_methods = [
'_on_framework_core_define',
'_on_framework_core_init',
'_on_framework_module_define',
'_on_framework_module_init'
];
foreach ($framework_methods as $method) {
if (preg_match('/\bstatic\s+(async\s+)?' . preg_quote($method) . '\s*\(\s*\)/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Framework initialization method '{$method}' cannot be used in user code.",
trim($line),
InitializationSuggestions::get_user_suggestion(),
'critical'
);
}
}
// Check for jQuery ready patterns (handled by DocumentReadyRule but add context here)
if (preg_match('/\$\s*\(\s*document\s*\)\s*\.\s*ready\s*\(/', $line) ||
preg_match('/\$\s*\(\s*function\s*\(/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"jQuery ready patterns are not allowed. Use ES6 class lifecycle methods.",
trim($line),
InitializationSuggestions::get_user_suggestion(),
'high'
);
}
}
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQueryLengthCheck_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-LENGTH-01';
}
public function get_name(): string
{
return 'jQuery .length Existence Check';
}
public function get_description(): string
{
return 'Enforces use of .exists() instead of .length for jQuery existence checks';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
// Patterns to detect:
// if($(selector).length)
// if(!$(selector).length)
// if($variable.length)
// if(!$variable.length)
// Also within compound conditions
// Check if line contains 'if' and '.length'
if (str_contains($sanitized_line, 'if') && str_contains($sanitized_line, '.length')) {
// Multiple patterns to check
$patterns = [
// Direct jQuery selector patterns
'/if\s*\(\s*!\s*\$\s*\([^)]+\)\.length/', // if(!$(selector).length
'/if\s*\(\s*\$\s*\([^)]+\)\.length/', // if($(selector).length
// jQuery variable patterns
'/if\s*\(\s*!\s*\$[a-zA-Z_][a-zA-Z0-9_]*\.length/', // if(!$variable.length
'/if\s*\(\s*\$[a-zA-Z_][a-zA-Z0-9_]*\.length/', // if($variable.length
// Within compound conditions (with && or ||)
'/if\s*\([^)]*[&|]{2}[^)]*\$\s*\([^)]+\)\.length/', // compound with $(selector).length
'/if\s*\([^)]*\$\s*\([^)]+\)\.length[^)]*[&|]{2}/', // compound with $(selector).length
'/if\s*\([^)]*[&|]{2}[^)]*\$[a-zA-Z_][a-zA-Z0-9_]*\.length/', // compound with $variable.length
'/if\s*\([^)]*\$[a-zA-Z_][a-zA-Z0-9_]*\.length[^)]*[&|]{2}/', // compound with $variable.length
];
$found = false;
foreach ($patterns as $pattern) {
if (preg_match($pattern, $sanitized_line)) {
$found = true;
break;
}
}
if ($found) {
$original_line = $original_lines[$line_num] ?? $sanitized_line;
$this->add_violation(
$file_path,
$line_number,
"Use .exists() instead of .length for jQuery existence checks.",
trim($original_line),
"Replace .length with .exists() for checking jQuery element existence. " .
"For example: use '$(selector).exists()' instead of '$(selector).length', " .
"or '\$variable.exists()' instead of '\$variable.length'. " .
"The .exists() method is more semantic and clearly indicates the intent of checking for element presence.",
'medium'
);
}
}
}
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQuerySubmitUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQUERY-SUBMIT-01';
}
public function get_name(): string
{
return 'jQuery .submit() Usage Check';
}
public function get_description(): string
{
return "Detects deprecated jQuery .submit() usage and recommends .trigger('submit') or .requestSubmit()";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'low';
}
/**
* Check JavaScript file for .submit() usage
* Recommends .trigger('submit') or .requestSubmit() instead
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Remove comments and strings to avoid false positives
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized = $sanitized_data['content'];
// Pattern matches:
// $variable.submit()
// $(selector).submit()
// that.$anything.submit()
// this.$anything.submit()
$pattern = '/(\$[a-zA-Z_][a-zA-Z0-9_]*|(?:this|that)\.\$[a-zA-Z0-9_.]+|\$\([^)]+\))\.submit\s*\(/';
preg_match_all($pattern, $sanitized, $matches, PREG_OFFSET_CAPTURE);
if (empty($matches[0])) {
return;
}
$lines = explode("\n", $contents);
foreach ($matches[0] as $match) {
$offset = $match[1];
$matched_text = $match[0];
// Find line number from offset
$line_number = 1;
$current_offset = 0;
foreach ($lines as $index => $line) {
$line_length = strlen($line) + 1; // +1 for newline
if ($current_offset + $line_length > $offset) {
$line_number = $index + 1;
break;
}
$current_offset += $line_length;
}
$code_snippet = trim($lines[$line_number - 1] ?? '');
$this->add_violation(
$file_path,
$line_number,
"Use .trigger('submit') or .requestSubmit() instead of deprecated .submit()",
$code_snippet,
"Replace .submit() with .trigger('submit') for event triggering or .requestSubmit() for actual form submission with validation",
'low'
);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQueryUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQUERY-01';
}
public function get_name(): string
{
return 'JavaScript jQuery Usage Check';
}
public function get_description(): string
{
return "Enforces use of '$' shorthand instead of 'jQuery' for consistency";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'low';
}
/**
* Check JavaScript file for 'jQuery' usage instead of '$' (from line 1307)
* Enforces use of '$' shorthand for consistency
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Check for 'jQuery.' or 'jQuery(' usage
if (preg_match('/\bjQuery\s*[\.\(]/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Use '$' instead of 'jQuery' for consistency and brevity.",
trim($line),
"Replace 'jQuery' with '$'.",
'low'
);
}
}
}
}

View File

@@ -0,0 +1,226 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JQueryVariableNaming_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQUERY-VAR-01';
}
public function get_name(): string
{
return 'jQuery Variable Naming Convention';
}
public function get_description(): string
{
return 'Enforces $ prefix for variables storing jQuery objects';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* jQuery methods that return jQuery objects
*/
private const JQUERY_OBJECT_METHODS = [
'parent', 'parents', 'parentsUntil', 'closest',
'find', 'children', 'contents',
'next', 'nextAll', 'nextUntil',
'prev', 'prevAll', 'prevUntil',
'siblings', 'add', 'addBack', 'andSelf',
'end', 'filter', 'not', 'has',
'eq', 'first', 'last', 'slice',
'map', 'clone', 'wrap', 'wrapAll', 'wrapInner',
'unwrap', 'replaceWith', 'replaceAll',
'prepend', 'append', 'prependTo', 'appendTo',
'before', 'after', 'insertBefore', 'insertAfter',
'detach', 'empty', 'remove'
];
/**
* jQuery methods that return scalar values (not jQuery objects)
*/
private const SCALAR_METHODS = [
'data', 'attr', 'val', 'text', 'html', 'prop', 'css',
'offset', 'position', 'scrollTop', 'scrollLeft',
'width', 'height', 'innerWidth', 'innerHeight',
'outerWidth', 'outerHeight',
'index', 'size', 'length', 'get', 'toArray',
'serialize', 'serializeArray',
'is', 'hasClass', 'is_visible' // Custom RSpade methods
];
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Pattern to match variable assignments
// Captures: 1=var declaration, 2=variable name, 3=right side expression
$pattern = '/(?:^|\s)((?:let\s+|const\s+|var\s+)?)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(.+?)(?:;|$)/';
if (preg_match($pattern, $sanitized_line, $matches)) {
$var_decl = $matches[1];
$var_name = $matches[2];
$right_side = trim($matches[3]);
$has_dollar = str_starts_with($var_name, '$');
// Analyze the right side to determine if it returns jQuery object or scalar
$expected_type = $this->analyze_expression($right_side);
if ($expected_type === 'jquery') {
// Should have $ prefix
if (!$has_dollar) {
$this->add_violation(
$file_path,
$line_number,
"jQuery object must be stored in variable starting with $.",
trim($original_line),
"Rename variable '{$var_name}' to '\${$var_name}'. " .
"The expression returns a jQuery object and must be stored in a variable with $ prefix. " .
"In RSpade, $ prefix indicates jQuery objects only.",
'medium'
);
}
} elseif ($expected_type === 'scalar') {
// Should NOT have $ prefix
if ($has_dollar) {
$this->add_violation(
$file_path,
$line_number,
"Scalar values should not use $ prefix.",
trim($original_line),
"Remove $ prefix from variable '{$var_name}'. Rename to '" . substr($var_name, 1) . "'. " .
"The expression returns a scalar value (string, number, boolean, or DOM element), not a jQuery object. " .
"In RSpade, $ prefix is reserved for jQuery objects only.",
'medium'
);
}
}
// If expected_type is 'unknown', we don't enforce either way
}
}
}
/**
* Analyze an expression to determine if it returns jQuery object or scalar
* @return string 'jquery', 'scalar', or 'unknown'
*/
private function analyze_expression(string $expr): string
{
$expr = trim($expr);
// Direct jQuery selector: $(...)
if (preg_match('/^\$\s*\(/', $expr)) {
// Check if followed by method chain
if (preg_match('/^\$\s*\([^)]*\)(.*)/', $expr, $matches)) {
$chain = trim($matches[1]);
if ($chain === '') {
return 'jquery'; // Just $(...) with no methods
}
return $this->analyze_method_chain($chain);
}
return 'jquery';
}
// Variable starting with $ (assumed to be jQuery)
if (preg_match('/^\$[a-zA-Z_][a-zA-Z0-9_]*(.*)/', $expr, $matches)) {
$chain = trim($matches[1]);
if ($chain === '') {
return 'jquery'; // Just $variable with no methods
}
if (str_starts_with($chain, '[')) {
// Array access like $element[0]
return 'scalar';
}
return $this->analyze_method_chain($chain);
}
// Everything else is unknown or definitely not jQuery
return 'unknown';
}
/**
* Analyze a method chain to determine final return type
* @param string $chain The method chain starting with . or [
* @return string 'jquery', 'scalar', or 'unknown'
*/
private function analyze_method_chain(string $chain): string
{
if (empty($chain)) {
return 'jquery'; // No methods means original jQuery object
}
// Array access [0] or [index] returns DOM element (scalar)
if (preg_match('/^\[[\d]+\]/', $chain)) {
return 'scalar';
}
// Find the last method call in the chain
// Match patterns like .method() or .method(args)
$methods = [];
preg_match_all('/\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)/', $chain, $methods);
if (empty($methods[1])) {
// No method calls found
return 'unknown';
}
// Check the last method to determine return type
$last_method = end($methods[1]);
if (in_array($last_method, self::JQUERY_OBJECT_METHODS, true)) {
return 'jquery';
}
if (in_array($last_method, self::SCALAR_METHODS, true)) {
return 'scalar';
}
// Unknown method - could be custom plugin
return 'unknown';
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JQueryVisibilityCheck_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-VISIBLE-01';
}
public function get_name(): string
{
return 'jQuery .is(\':visible\') Check';
}
public function get_description(): string
{
return 'Enforces use of .is_visible() instead of .is(\':visible\') for jQuery visibility checks';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get original content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Pattern to match .is(':visible') or .is(":visible")
$pattern = '/\.is\s*\(\s*[\'\"]:visible[\'\"]\s*\)/';
foreach ($original_lines as $line_num => $original_line) {
$line_number = $line_num + 1;
// Skip empty lines
if (trim($original_line) === '') {
continue;
}
// Check if line contains .is(':visible') pattern
if (preg_match($pattern, $original_line, $matches, PREG_OFFSET_CAPTURE)) {
$match_position = $matches[0][1];
// Check if there's a // comment before the match
$comment_pos = strpos($original_line, '//');
if ($comment_pos !== false && $comment_pos < $match_position) {
// The match is in a comment, skip it
continue;
}
// Check if the match is inside a /* */ comment block (simplified check)
// This is a basic check - for full accuracy would need to track multi-line comments
$before_match = substr($original_line, 0, $match_position);
if (str_contains($before_match, '/*') && !str_contains($before_match, '*/')) {
// Likely inside a block comment
continue;
}
$this->add_violation(
$file_path,
$line_number,
"Use .is_visible() instead of .is(':visible') for jQuery visibility checks.",
trim($original_line),
"Replace .is(':visible') with .is_visible() for checking jQuery element visibility. " .
"IMPORTANT: .is_visible() is a custom jQuery extension implemented in the RSpade framework " .
"and is available throughout the codebase. It provides better performance and clearer intent " .
"than the :visible selector. Example: use '$(selector).is_visible()' instead of '$(selector).is(':visible')'.",
'medium'
);
}
}
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
use App\RSpade\Core\Cache\RsxCache;
/**
* Check Jqhtml_Component implementations for common AI agent mistakes
* Validates that components follow correct patterns
*/
class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-IMPL-01';
}
public function get_name(): string
{
return 'Jqhtml Component Implementation Check';
}
public function get_description(): string
{
return 'Validates Jqhtml_Component subclasses follow correct patterns';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
/**
* Run during manifest build for immediate feedback
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a JavaScript file
if (!isset($metadata['extension']) || $metadata['extension'] !== 'js') {
return;
}
// Skip if not a Jqhtml_Component subclass
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Jqhtml_Component') {
return;
}
// Check cache to avoid redundant validation
$cache_key = $metadata['hash'] ?? md5($contents);
if (RsxCache::get_persistent($cache_key, false) === true) {
// Already validated
return;
}
$lines = explode("\n", $contents);
// Check for render() method and incorrect lifecycle methods
foreach ($lines as $line_num => $line) {
$trimmed = trim($line);
// Check for render() method
if (preg_match('/^render\s*\(/', $trimmed)) {
$this->throw_render_method_error($file_path, $line_num + 1, $metadata['class'] ?? 'Unknown');
}
// Check for incorrect event method names (create, load, ready without on_ prefix)
if (preg_match('/^(create|load|ready)\s*\(/', $trimmed, $matches)) {
$method = $matches[1];
$this->throw_lifecycle_method_error($file_path, $line_num + 1, $method);
}
}
// Mark as validated in cache
RsxCache::set_persistent($cache_key, true);
}
private function throw_render_method_error(string $file_path, int $line_number, string $class_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Jqhtml component should not have render() method\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line_number}\n";
$error_message .= "Class: {$class_name}\n\n";
$error_message .= "Jqhtml components should not define a render() method.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The render() method is not part of the Jqhtml_Component lifecycle.\n";
$error_message .= "Jqhtml components use template files (.jqhtml) for rendering.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " render() {\n";
$error_message .= " return '<div>...</div>';\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " // Create a template file: my_component.jqhtml\n";
$error_message .= " <div>\n";
$error_message .= " <%= content() %>\n";
$error_message .= " </div>\n\n";
$error_message .= " // JavaScript class handles logic:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " on_ready() {\n";
$error_message .= " // Component logic here\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "WHY THIS MATTERS:\n";
$error_message .= "- Jqhtml separates template from logic\n";
$error_message .= "- Templates are pre-compiled for performance\n";
$error_message .= "- The render() pattern is from React, not Jqhtml\n\n";
$error_message .= "FIX:\n";
$error_message .= "1. Remove the render() method\n";
$error_message .= "2. Create a .jqhtml template file for the component\n";
$error_message .= "3. Use lifecycle methods like on_create(), on_load(), on_ready()\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
private function throw_lifecycle_method_error(string $file_path, int $line_number, string $method_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Jqhtml lifecycle method missing 'on_' prefix\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line_number}\n";
$error_message .= "Method: {$method_name}()\n\n";
$error_message .= "Jqhtml lifecycle methods must use the 'on_' prefix.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The method '{$method_name}()' should be 'on_{$method_name}()'.\n";
$error_message .= "Jqhtml components use specific lifecycle method names.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " create() { ... } // Wrong\n";
$error_message .= " load() { ... } // Wrong\n";
$error_message .= " ready() { ... } // Wrong\n";
$error_message .= " }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " class My_Component extends Jqhtml_Component {\n";
$error_message .= " on_create() { ... } // Correct\n";
$error_message .= " on_load() { ... } // Correct\n";
$error_message .= " on_ready() { ... } // Correct\n";
$error_message .= " }\n\n";
$error_message .= "LIFECYCLE METHODS:\n";
$error_message .= "- on_create(): Called when component is created\n";
$error_message .= "- on_load(): Called to load async data\n";
$error_message .= "- on_ready(): Called when component is ready in DOM\n";
$error_message .= "- on_destroy(): Called when component is destroyed\n\n";
$error_message .= "FIX:\n";
$error_message .= "Rename '{$method_name}()' to 'on_{$method_name}()'\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
}

View File

@@ -0,0 +1,259 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JqhtmlDataInCreate_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-JQHTML-01';
}
public function get_name(): string
{
return 'Jqhtml Component this.data in on_create() Check';
}
public function get_description(): string
{
return 'Ensures this.data is not used in on_create() method of Jqhtml components';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for improper this.data usage in on_create() methods of Jqhtml components
*/
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 violations from AST parser (with caching)
$violations = $this->parse_with_acorn($file_path);
if (empty($violations)) {
return;
}
// Process violations
foreach ($violations as $violation) {
$line_number = $violation['line'];
$class_name = $violation['className'] ?? 'unknown';
$code_snippet = $violation['codeSnippet'] ?? 'this.data';
$this->add_violation(
$file_path,
$line_number,
"Jqhtml Component Error: 'this.data' used in on_create() method of class '{$class_name}'. " .
"The 'this.data' property is only available during on_load() and later lifecycle steps. " .
"It is used to store data fetched from AJAX or other async operations.",
$code_snippet,
"Use 'this.args' instead to access the parameters passed to the component at creation time. " .
"The args contain attributes from the component's invocation in templates or JavaScript. " .
"Example: Change 'this.data.initial_value' to 'this.args.initial_value'.",
'high'
);
}
}
/**
* Parse JavaScript file with acorn AST parser
* Results are cached based on file modification time
*/
protected function parse_with_acorn(string $file_path): array
{
// Create cache directory if needed
$cache_dir = storage_path('rsx-tmp/persistent/code-quality-jqhtml-data');
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0777, true);
}
// Generate cache key based on file path and mtime
$file_mtime = filemtime($file_path);
$file_size = filesize($file_path);
$cache_key = md5($file_path . ':jqhtml-data');
$cache_file = $cache_dir . '/' . $cache_key . '.json';
// Check if cached result exists and is valid
if (file_exists($cache_file)) {
$cache_data = json_decode(file_get_contents($cache_file), true);
if ($cache_data &&
isset($cache_data['mtime']) && $cache_data['mtime'] == $file_mtime &&
isset($cache_data['size']) && $cache_data['size'] == $file_size) {
// Cache is valid
return $cache_data['violations'] ?? [];
}
}
// Create parser script if it doesn't exist
$parser_script = storage_path('rsx-tmp/persistent/parse-jqhtml-data.js');
if (!file_exists($parser_script)) {
$this->create_parser_script($parser_script);
}
// Run parser
$command = sprintf(
'node %s %s 2>&1',
escapeshellarg($parser_script),
escapeshellarg($file_path)
);
$output = shell_exec($command);
if (!$output) {
return [];
}
$result = json_decode($output, true);
if (!$result || !isset($result['violations'])) {
// Parser error - don't cache
return [];
}
// Cache the result
$cache_data = [
'mtime' => $file_mtime,
'size' => $file_size,
'violations' => $result['violations']
];
file_put_contents($cache_file, json_encode($cache_data));
return $result['violations'];
}
/**
* Create the Node.js parser script
*/
protected function create_parser_script(string $script_path): void
{
$dir = dirname($script_path);
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
}
$script_content = <<<'JAVASCRIPT'
#!/usr/bin/env node
const fs = require('fs');
const acorn = require('acorn');
const walk = require('acorn-walk');
// Classes that are Jqhtml components
const JQHTML_COMPONENTS = new Set([
'Jqhtml_Component', '_Base_Jqhtml_Component', 'Component'
]);
function analyzeFile(filePath) {
const code = fs.readFileSync(filePath, 'utf8');
const lines = code.split('\n');
let ast;
try {
ast = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
locations: true
});
} catch (e) {
// Parse error - return empty violations
console.log(JSON.stringify({ violations: [] }));
return;
}
const violations = [];
let currentClass = null;
let inOnCreate = false;
// Helper to check if a class extends Jqhtml_Component
function isJqhtmlComponent(extendsClass) {
if (!extendsClass) return false;
return JQHTML_COMPONENTS.has(extendsClass) ||
extendsClass.includes('Component') ||
extendsClass.includes('Jqhtml');
}
// Walk the AST
walk.simple(ast, {
ClassDeclaration(node) {
currentClass = {
name: node.id.name,
extends: node.superClass?.name,
isJqhtml: isJqhtmlComponent(node.superClass?.name)
};
},
ClassExpression(node) {
currentClass = {
name: node.id?.name || 'anonymous',
extends: node.superClass?.name,
isJqhtml: isJqhtmlComponent(node.superClass?.name)
};
},
MethodDefinition(node) {
// Check if this is on_create method
if (node.key.name === 'on_create' && currentClass?.isJqhtml) {
inOnCreate = true;
// Walk the method body looking for this.data
walk.simple(node.value.body, {
MemberExpression(memberNode) {
// Check for this.data pattern
if (memberNode.object.type === 'ThisExpression' &&
memberNode.property.name === 'data') {
// Found this.data in on_create
const lineContent = lines[memberNode.loc.start.line - 1] || '';
violations.push({
line: memberNode.loc.start.line,
column: memberNode.loc.start.column,
className: currentClass.name,
codeSnippet: lineContent.trim()
});
}
}
});
inOnCreate = false;
}
}
});
console.log(JSON.stringify({ violations }));
}
// Main
if (process.argv.length < 3) {
console.error('Usage: node parse-jqhtml-data.js <file-path>');
process.exit(1);
}
try {
analyzeFile(process.argv[2]);
} catch (e) {
console.error('Error:', e.message);
console.log(JSON.stringify({ violations: [] }));
}
JAVASCRIPT;
file_put_contents($script_path, $script_content);
chmod($script_path, 0755);
}
}

View File

@@ -0,0 +1,199 @@
<?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;
class JqhtmlOnLoadData_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-LOAD-02';
}
public function get_name(): string
{
return 'JQHTML on_load Data Assignment Check';
}
public function get_description(): string
{
return 'on_load() method should only set this.data properties, not other this. properties';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check that on_load methods only set this.data properties
*/
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 sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$content = $sanitized_data['content'];
// 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, try to extract from source
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 (handles indirect inheritance)
if (!Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component') &&
!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// Now find WHERE this class is in the source for extraction
// @META-INHERIT-01-EXCEPTION - Finding class position with PREG_OFFSET_CAPTURE to extract method bodies
if (!preg_match('/class\s+' . preg_quote($class_name, '/') . '\s+extends\s+\w+\s*\{/i', $content, $class_match, PREG_OFFSET_CAPTURE)) {
continue;
}
$class_start = $class_match[0][1];
// Find the class content
$brace_count = 0;
$in_class = false;
$class_content = '';
$pos = $class_start;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$brace_count++;
$in_class = true;
} elseif ($char === '}') {
$brace_count--;
if ($brace_count === 0 && $in_class) {
$class_content = substr($content, $class_start, $pos - $class_start + 1);
break;
}
}
$pos++;
}
if (empty($class_content)) {
continue;
}
// Look for on_load method
if (!preg_match('/(?:async\s+)?on_load\s*\([^)]*\)\s*\{/i', $class_content, $method_match, PREG_OFFSET_CAPTURE)) {
continue; // No on_load method
}
$method_start = $method_match[0][1];
// Extract the on_load method body
$method_pos = $class_start + $method_start;
$method_brace_count = 0;
$in_method = false;
$method_content = '';
$pos = $method_pos;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$method_brace_count++;
$in_method = true;
} elseif ($char === '}') {
$method_brace_count--;
if ($method_brace_count === 0 && $in_method) {
$method_content = substr($content, $method_pos, $pos - $method_pos + 1);
break;
}
}
$pos++;
}
if (empty($method_content)) {
continue;
}
// Check for this. property assignments
$lines = explode("\n", $method_content);
$line_offset = substr_count(substr($content, 0, $method_pos), "\n");
foreach ($lines as $line_num => $line) {
$actual_line_number = $line_offset + $line_num + 1;
$trimmed = trim($line);
// Skip comments
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for this.property = value patterns
// Match: this.something =
// But not: this.data.something =
if (preg_match('/\bthis\.(\w+)\s*=/', $line, $matches)) {
$property_name = $matches[1];
// Allow this.data assignments
if ($property_name === 'data') {
continue;
}
// Check if it's a sub-property of data (this.data.something)
if (preg_match('/\bthis\.data\.\w+/', $line)) {
continue;
}
// Check for destructuring into this.data
if (preg_match('/\bthis\.data\s*=\s*\{.*' . preg_quote($property_name, '/') . '/', $line)) {
continue;
}
$this->add_violation(
$file_path,
$actual_line_number,
"Setting 'this.{$property_name}' in on_load() method of class '{$class_name}'. The on_load() method should only update this.data properties.",
trim($line),
"Change to 'this.data.{$property_name} = ...' or move to on_create() if it's component state.",
'high'
);
}
// Also check for Object.assign or similar patterns that set this properties
if (preg_match('/Object\.assign\s*\(\s*this\s*(?!\.data)/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Using Object.assign on 'this' in on_load() method of class '{$class_name}'. The on_load() method should only update this.data.",
trim($line),
"Use 'Object.assign(this.data, ...)' instead, or move to on_create() for component state.",
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,234 @@
<?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;
class JqhtmlOnLoadDom_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-LOAD-01';
}
public function get_name(): string
{
return 'JQHTML on_load DOM Access Check';
}
public function get_description(): string
{
return 'on_load() method must not access DOM or call render() - only update this.data';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for DOM access in on_load methods of Jqhtml_Component subclasses
*/
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 sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$content = $sanitized_data['content'];
// 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 (handles indirect inheritance)
if (!Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component') &&
!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// Now find WHERE this class is in the source for extraction
// @META-INHERIT-01-EXCEPTION - Finding class position with PREG_OFFSET_CAPTURE to extract method bodies
// Not checking inheritance - need PREG_OFFSET_CAPTURE to extract method bodies
if (!preg_match('/class\s+' . preg_quote($class_name, '/') . '\s+extends\s+\w+\s*\{/i', $content, $class_match, PREG_OFFSET_CAPTURE)) {
continue;
}
$class_start = $class_match[0][1];
// Find the class content
$brace_count = 0;
$in_class = false;
$class_content = '';
$pos = $class_start;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$brace_count++;
$in_class = true;
} elseif ($char === '}') {
$brace_count--;
if ($brace_count === 0 && $in_class) {
$class_content = substr($content, $class_start, $pos - $class_start + 1);
break;
}
}
$pos++;
}
if (empty($class_content)) {
continue;
}
// Look for on_load method
if (!preg_match('/(?:async\s+)?on_load\s*\([^)]*\)\s*\{/i', $class_content, $method_match, PREG_OFFSET_CAPTURE)) {
continue; // No on_load method
}
$method_start = $method_match[0][1];
// Extract the on_load method body
$method_pos = $class_start + $method_start;
$method_brace_count = 0;
$in_method = false;
$method_content = '';
$pos = $method_pos;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$method_brace_count++;
$in_method = true;
} elseif ($char === '}') {
$method_brace_count--;
if ($method_brace_count === 0 && $in_method) {
$method_content = substr($content, $method_pos, $pos - $method_pos + 1);
break;
}
}
$pos++;
}
if (empty($method_content)) {
continue;
}
// Check for DOM access patterns
$lines = explode("\n", $method_content);
$line_offset = substr_count(substr($content, 0, $method_pos), "\n");
foreach ($lines as $line_num => $line) {
$actual_line_number = $line_offset + $line_num + 1;
$trimmed = trim($line);
// Skip comments
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for this.$ (jQuery element access)
// Match this.$ followed by anything except .ajax/.get/.post/.getJSON
if (preg_match('/\bthis\.\$/', $line)) {
// Check if it's this.$.something (DOM manipulation) vs $.ajax usage
if (preg_match('/\bthis\.\$\./', $line)) {
// this.$. pattern - this is DOM manipulation
$this->add_violation(
$file_path,
$actual_line_number,
"DOM access 'this.\$' detected in on_load() method of class '{$class_name}'. The on_load() phase runs in parallel and must not access DOM.",
trim($line),
"To handle loading states, you have two options:\n" .
"1. Use conditional rendering in your .jqhtml template:\n" .
" <% if (this.data === null): %>\n" .
" <div class=\"loading\">Loading...</div>\n" .
" <% else: %>\n" .
" <div class=\"content\">...loaded content...</div>\n" .
" <% endif; %>\n" .
"2. Set loading state in on_create() (before load) and clear it in on_ready() (after load).\n" .
' on_load() should only update this.data properties.',
'high'
);
}
}
// Check for this.$id() calls
if (preg_match('/\bthis\.\$id\s*\(/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"DOM access 'this.\$id()' detected in on_load() method of class '{$class_name}'. The on_load() phase runs in parallel and must not access DOM.",
trim($line),
"To handle loading states, you have two options:\n" .
"1. Use conditional rendering in your .jqhtml template:\n" .
" <% if (this.data === null): %>\n" .
" <div class=\"loading\">Loading...</div>\n" .
" <% else: %>\n" .
" <div class=\"content\">...loaded content...</div>\n" .
" <% endif; %>\n" .
"2. Set loading state in on_create() (before load) and clear it in on_ready() (after load).\n" .
' on_load() should only update this.data properties.',
'high'
);
}
// Check for jQuery selector usage (but allow $.ajax)
if (preg_match('/\$\s*\([\'"`]/', $line) && !preg_match('/\$\.(ajax|get|post|getJSON)/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"jQuery selector '\$()' detected in on_load() method of class '{$class_name}'. The on_load() phase runs in parallel and must not access DOM.",
trim($line),
"To handle loading states, you have two options:\n" .
"1. Use conditional rendering in your .jqhtml template:\n" .
" <% if (this.data === null): %>\n" .
" <div class=\"loading\">Loading...</div>\n" .
" <% else: %>\n" .
" <div class=\"content\">...loaded content...</div>\n" .
" <% endif; %>\n" .
"2. Set loading state in on_create() (before load) and clear it in on_ready() (after load).\n" .
' on_load() should only update this.data properties. Use $.ajax() for data fetching.',
'high'
);
}
// Check for this.render() calls
if (preg_match('/\bthis\.render\s*\(/', $line)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Calling 'this.render()' in on_load() method of class '{$class_name}' is not allowed. Re-render happens automatically after on_load() if this.data changed.",
trim($line),
'Remove this.render() call. The framework automatically re-renders after on_load() if this.data was modified.',
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?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;
class JqhtmlRenderOverride_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JQHTML-RENDER-01';
}
public function get_name(): string
{
return 'JQHTML render() Method Override Check';
}
public function get_description(): string
{
return 'Components must not override render() method - use on_render(), on_create(), or on_ready() instead';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check for render() method override in Jqhtml_Component subclasses
*/
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 sanitized content
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$content = $sanitized_data['content'];
// 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 (handles indirect inheritance)
if (!Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component') &&
!Manifest::js_is_subclass_of($class_name, 'Component')) {
continue;
}
// Now find WHERE this class is in the source for extraction
// @META-INHERIT-01-EXCEPTION - Finding class position with PREG_OFFSET_CAPTURE to extract method bodies
// Not checking inheritance - need PREG_OFFSET_CAPTURE to extract method bodies
if (!preg_match('/class\s+' . preg_quote($class_name, '/') . '\s+extends\s+\w+\s*\{/i', $content, $class_match, PREG_OFFSET_CAPTURE)) {
continue;
}
$class_start = $class_match[0][1];
// Find the class content
$brace_count = 0;
$in_class = false;
$class_content = '';
$pos = $class_start;
while ($pos < strlen($content)) {
$char = $content[$pos];
if ($char === '{') {
$brace_count++;
$in_class = true;
} elseif ($char === '}') {
$brace_count--;
if ($brace_count === 0 && $in_class) {
$class_content = substr($content, $class_start, $pos - $class_start + 1);
break;
}
}
$pos++;
}
if (empty($class_content)) {
continue;
}
// Check for render() method definition
$lines = explode("\n", $class_content);
$line_offset = substr_count(substr($content, 0, $class_start), "\n");
foreach ($lines as $line_num => $line) {
$actual_line_number = $line_offset + $line_num + 1;
$trimmed = trim($line);
// Skip comments
if (str_starts_with($trimmed, '//') || str_starts_with($trimmed, '*')) {
continue;
}
// Check for render() method definition
// Match: render() { or async render() {
// But not: on_render() or other_render() or renderSomething()
if (preg_match('/^(?:async\s+)?render\s*\([^)]*\)\s*\{/', $trimmed)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Component class '{$class_name}' overrides render() method. This is not allowed in JQHTML components.",
trim($line),
'Use lifecycle methods instead: on_render() for initial render setup, on_create() for post-render initialization, or on_ready() for final setup. The render() method is reserved for the framework to render templates.',
'high'
);
}
// Also check for static render method (which would also be wrong)
if (preg_match('/^static\s+(?:async\s+)?render\s*\([^)]*\)\s*\{/', $trimmed)) {
$this->add_violation(
$file_path,
$actual_line_number,
"Component class '{$class_name}' defines a static render() method. This is not allowed in JQHTML components.",
trim($line),
'Use lifecycle methods instead: on_render() for initial render setup, on_create() for post-render initialization, or on_ready() for final setup.',
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class JsFallbackLegacy_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-FALLBACK-01';
}
public function get_name(): string
{
return 'JavaScript Fallback/Legacy Code Check';
}
public function get_description(): string
{
return 'Enforces fail-loud principle - no fallback implementations allowed';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check JavaScript file for fallback/legacy code in comments and function calls (from line 1415)
* Enforces fail-loud principle - no fallback implementations allowed
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Use original content to check comments before sanitization
$original_content = file_get_contents($file_path);
$lines = explode("\n", $original_content);
// Also get sanitized content to check for function calls
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Check for fallback or legacy in comments (case insensitive, with word boundaries)
// But allow fallback* or legacy* as marked exceptions
if (preg_match('/\/\/.*\b(fallback|legacy)\b(?!\*)/i', $line) ||
preg_match('/\/\*.*\b(fallback|legacy)\b(?!\*)/i', $line) ||
preg_match('/\*.*\b(fallback|legacy)\b(?!\*)/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"CRITICAL: Fallback/legacy code detected in comment. This violates the fail-loud principle - there must be ONE way to do things.",
trim($line),
"REMOVE the fallback/legacy code immediately. If primary code fails, it MUST throw an exception, NOT execute alternative code. Fallbacks create non-deterministic behavior and debugging nightmares. Only ONE implementation should exist. If you're an AI/LLM agent: DO NOT decide to keep fallback code - you MUST ask the developer for explicit approval. To mark legitimate uses (rare), add '*' after the word. Remember: Predictable failures are better than unpredictable 'working' code.",
'critical'
);
}
// Check for dual implementation patterns in single-line comments
// These patterns suggest old/new dual implementations which violate the no-fallback rule
if (preg_match('/\/\/.*\bold\W+.*\bnew\b/i', $line) || // "old" ... "new"
preg_match('/\/\/.*\bprevious\W+.*\bnew\b/i', $line) || // "previous" ... "new"
preg_match('/\/\/.*\bbackwards?\s+compatability/i', $line)) { // "backward(s) compatibility" (common misspelling)
$this->add_violation(
$file_path,
$line_number,
"🚨 CRITICAL DUAL IMPLEMENTATION VIOLATION: Comment suggests old/new or backwards compatibility patterns\n\nThis violates the fundamental architecture principle: There is NO 'old' and 'new' way - there is ONLY THE WAY.\nDual implementations cause:\n1. CONFUSION about which code path executes\n2. DUPLICATE CODE that diverges over time\n3. DEBUGGING NIGHTMARES when behavior differs\n4. MAINTENANCE BURDEN keeping both paths in sync",
trim($line),
"IMMEDIATE ACTION REQUIRED:\n1. IDENTIFY what 'old' vs 'new' refers to\n2. REMOVE the old implementation entirely\n3. UPDATE all references to use the single implementation\n4. DO NOT maintain backwards compatibility\n\nFor AI/LLM agents: This is a CRITICAL violation. You MUST:\n- Search for BOTH implementations referenced in the comment\n- Report findings to user before making changes\n- Remove the OLD implementation completely\n- Update all code to use the single 'new' (current) way\n- NEVER maintain dual implementations for compatibility\n\nRemember: There is only ONE way to do things. What's currently called 'new' IS the way.",
'critical'
);
}
// Check for function calls starting or ending with 'fallback'
// This catches: fallback(), fallback_loader(), document_loader_fallback(), etc.
// But NOT: document_fallback_loader() (fallback in middle)
// Use sanitized line to check if there's actual code, but check pattern on original line
if (isset($sanitized_lines[$line_num]) && trim($sanitized_lines[$line_num]) !== '') {
// There's non-comment code on this line
// Pattern matches functions that start with "fallback" OR end with "fallback"
if (preg_match('/\b(fallback\w*|\w+fallback)\s*\(/i', $line)) {
$this->add_violation(
$file_path,
$line_number,
"CRITICAL: Fallback function call detected. This violates the fail-loud principle - there must be ONE way to do things.",
trim($line),
"REMOVE the fallback function immediately or RENAME it if it's legitimate required program flow (not an alternative implementation). If primary code fails, it MUST throw an exception, NOT execute alternative code. Fallbacks create non-deterministic behavior and debugging nightmares. Only ONE implementation should exist.",
'critical'
);
}
}
}
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class JsLegacyFunction_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-LEGACY-FUNC-01';
}
public function get_name(): string
{
return 'JavaScript Legacy Function Comment Check';
}
public function get_description(): string
{
return 'Prohibits functions with "legacy" in block comments - enforces no backwards compatibility principle';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'critical';
}
/**
* Check for block comments containing "legacy" directly before function definitions
* Enforces RSX principle of no backwards compatibility functions
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip node_modules directories
if (str_contains($file_path, '/node_modules/')) {
return;
}
// Skip vendor directories
if (str_contains($file_path, '/vendor/')) {
return;
}
$lines = explode("\n", $contents);
$in_block_comment = false;
$block_comment_content = '';
$block_comment_start_line = 0;
for ($i = 0; $i < count($lines); $i++) {
$line = $lines[$i];
$line_number = $i + 1;
$trimmed_line = trim($line);
// Track block comment state
if (str_contains($trimmed_line, '/*')) {
$in_block_comment = true;
$block_comment_start_line = $line_number;
$block_comment_content = $line;
// Handle single-line block comments
if (str_contains($trimmed_line, '*/')) {
$in_block_comment = false;
$this->check_block_comment_for_legacy($file_path, $block_comment_content, $block_comment_start_line, $lines, $i);
$block_comment_content = '';
}
continue;
}
if ($in_block_comment) {
$block_comment_content .= "\n" . $line;
if (str_contains($trimmed_line, '*/')) {
$in_block_comment = false;
$this->check_block_comment_for_legacy($file_path, $block_comment_content, $block_comment_start_line, $lines, $i);
$block_comment_content = '';
}
continue;
}
}
}
/**
* Check if a block comment contains "legacy" and is followed by a function
*/
private function check_block_comment_for_legacy(string $file_path, string $comment_content, int $comment_start_line, array $lines, int $comment_end_index): void
{
// Check if comment contains "legacy" (case insensitive)
if (!preg_match('/\blegacy\b/i', $comment_content)) {
return;
}
// Look for function definition in the next few lines after comment
for ($j = $comment_end_index + 1; $j < min($comment_end_index + 5, count($lines)); $j++) {
$next_line = trim($lines[$j]);
// Skip empty lines and single-line comments
if (empty($next_line) || str_starts_with($next_line, '//')) {
continue;
}
// Check if this line contains a JavaScript function definition
if (preg_match('/^(static\s+)?function\s+\w+\s*\(/i', $next_line) || // function name()
preg_match('/^\w+\s*:\s*function\s*\(/i', $next_line) || // name: function()
preg_match('/^(static\s+)?\w+\s*\(/i', $next_line) || // static name() or name() (ES6 class method)
preg_match('/^(const|let|var)\s+\w+\s*=\s*(async\s+)?function/i', $next_line) || // const name = function
preg_match('/^(const|let|var)\s+\w+\s*=\s*(async\s+)?\(/i', $next_line) || // const name = () =>
preg_match('/^\w+\s*=\s*(async\s+)?function/i', $next_line) || // name = function
preg_match('/^\w+\s*=\s*(async\s+)?\(/i', $next_line)) { // name = () =>
$this->add_violation(
$file_path,
$comment_start_line,
"🚨 CRITICAL BACKWARDS COMPATIBILITY VIOLATION: JavaScript function with 'legacy' comment detected
RSX Framework Principle: NO BACKWARDS COMPATIBILITY FUNCTIONS ALLOWED
The presence of 'legacy' in function comments indicates backwards compatibility code, which violates the core RSX principle of 'one way to do things'. Legacy functions create:
1. MULTIPLE CODE PATHS for the same functionality
2. MAINTENANCE BURDEN keeping old interfaces working
3. CONFUSION about which method to use
4. TECHNICAL DEBT that accumulates over time
5. DEBUGGING NIGHTMARES when behavior differs between paths
RSX enforces FAIL LOUD architecture - when things change, they should break clearly, not fall back to old implementations.",
trim($comment_content),
"IMMEDIATE ACTION REQUIRED:
1. REMOVE the legacy function entirely
2. UPDATE all callers to use the current implementation
3. DO NOT maintain backwards compatibility
4. If migration is needed, create a one-time migration script that:
- Identifies all usage of the old function
- Updates code to use new function
- Removes old function after migration is complete
For AI/LLM agents: This is a CRITICAL violation. You MUST:
- Search for ALL callers of this legacy function
- Report findings to user before making changes
- Remove the legacy function completely
- Update all code to use the current implementation
- NEVER maintain dual implementations for compatibility
Remember: There is only ONE way to do things in RSX. Legacy functions violate this fundamental principle.",
'critical'
);
return;
}
// If we hit a non-function code line, stop looking
break;
}
}
}

View File

@@ -0,0 +1,134 @@
<?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;
class JsRspadeInternalMethod_CodeQualityRule extends CodeQualityRule_Abstract
{
protected static $rspade_classes = null;
public function get_id(): string
{
return 'JS-INTERNAL-01';
}
public function get_name(): string
{
return 'JavaScript RSpade Internal Method Usage Check';
}
public function get_description(): string
{
return 'Prohibits calling internal RSpade methods (starting with _) from application code';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Get list of RSpade JavaScript classes from manifest
*/
protected function get_rspade_classes(): array
{
if (static::$rspade_classes === null) {
static::$rspade_classes = [];
// Get all files from manifest
$all_files = Manifest::get_all();
foreach ($all_files as $file_path => $metadata) {
// Check if it's a JavaScript file with a class
$extension = $metadata['extension'] ?? '';
if (in_array($extension, ['*.js']) && isset($metadata['class'])) {
// Handle Windows backslash issue - normalize to forward slashes
$normalized_path = str_replace('\\', '/', $file_path);
// Check if the file is in app/RSpade directory
if (str_contains($normalized_path, 'app/RSpade/')) {
static::$rspade_classes[] = $metadata['class'];
}
}
}
}
return static::$rspade_classes;
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get RSpade classes
$rspade_classes = $this->get_rspade_classes();
if (empty($rspade_classes)) {
return; // No RSpade JS classes found
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
// Check for each RSpade class
foreach ($rspade_classes as $class_name) {
// Pattern to match ClassName._method
// Must be preceded by non-alphanumeric or beginning of line
$pattern = '/(?:^|[^a-zA-Z0-9_])(' . preg_quote($class_name, '/') . ')\._[a-zA-Z0-9_]+\s*\(/';
if (preg_match($pattern, $sanitized_line, $matches)) {
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Extract the method call for better error message
preg_match('/' . preg_quote($class_name, '/') . '\._[a-zA-Z0-9_]+/', $sanitized_line, $method_match);
$method_call = $method_match[0] ?? $class_name . '._method';
$this->add_violation(
$file_path,
$line_number,
"Calling internal RSpade method. Methods starting with _ are for framework internal use only.",
trim($original_line),
"Do not call methods starting with underscore on RSpade framework classes. " .
"The method '{$method_call}()' is internal to the framework and may change without notice in updates. " .
"Use only public API methods (those not starting with underscore).",
'high'
);
}
}
}
}
}

View File

@@ -0,0 +1,219 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* LifecycleMethodsStaticRule - Ensures RSX lifecycle methods are static
*
* This rule checks JavaScript ES6 classes for RSX framework lifecycle methods
* and ensures they are declared as static. These methods are called by the
* framework at specific initialization phases and must be static to work correctly.
*/
class LifecycleMethodsStatic_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* RSX lifecycle methods that must be static
* These are called by the framework during initialization phases
*/
private const LIFECYCLE_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready',
'on_jqhtml_ready',
];
public function get_id(): string
{
return 'JS-LIFECYCLE-01';
}
public function get_name(): string
{
return 'RSX Lifecycle Methods Must Be Static';
}
public function get_description(): string
{
return 'Ensures RSX framework lifecycle methods (on_app_ready, etc.) are declared as static in ES6 classes';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Whether this rule is called during manifest scan
*
* IMPORTANT: This method should ALWAYS return false unless explicitly requested
* by the framework developer. Manifest-time checks are reserved for critical
* framework convention violations that need immediate developer attention.
*
* Rules executed during manifest scan will run on every file change in development,
* potentially impacting performance. Only enable this for rules that:
* - Enforce critical framework conventions that would break the application
* - Need to provide immediate feedback before code execution
* - Have been specifically requested to run at manifest-time by framework maintainers
*
* DEFAULT: Always return false unless you have explicit permission to do otherwise.
*
* EXCEPTION: This rule has been explicitly approved to run at manifest-time because
* lifecycle methods must be static for the framework's auto-initialization to function correctly.
*/
public function is_called_during_manifest_scan(): bool
{
return true; // Explicitly approved for manifest-time checking
}
/**
* Check JavaScript file for lifecycle methods that aren't static
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip if not a JavaScript file with a class
if (!isset($metadata['class'])) {
return;
}
// Get class name
$class_name = $metadata['class'];
// Only check classes that extend Jqhtml_Component
if (!\App\RSpade\Core\Manifest\Manifest::js_is_subclass_of($class_name, 'Jqhtml_Component')) {
return;
}
// Check regular (non-static) methods for lifecycle methods that should be static
if (isset($metadata['public_instance_methods']) && is_array($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_info) {
if (in_array($method_name, self::LIFECYCLE_METHODS, true)) {
// This lifecycle method is not static - violation!
// Find the line number by searching for the method definition
$line_number = $this->find_method_line($contents, $method_name);
$code_line = $this->extract_code_line($contents, $line_number);
$this->add_violation(
$file_path,
$line_number,
"RSX lifecycle method '{$method_name}' must be declared as static",
$code_line,
$this->get_remediation($method_name, $class_name, $code_line),
'high'
);
}
}
}
}
/**
* Find the line number where a method is defined
*/
private function find_method_line(string $contents, string $method_name): int
{
$lines = explode("\n", $contents);
foreach ($lines as $line_num => $line) {
// Look for method definition (with or without async)
if (preg_match('/\b(async\s+)?' . preg_quote($method_name, '/') . '\s*\(/', $line)) {
return $line_num + 1;
}
}
return 1; // Default to line 1 if not found
}
/**
* Extract code line for a given line number
*/
private function extract_code_line(string $contents, int $line_number): string
{
$lines = explode("\n", $contents);
if (isset($lines[$line_number - 1])) {
return trim($lines[$line_number - 1]);
}
return '';
}
/**
* Get remediation instructions for non-static lifecycle method
*/
private function get_remediation(string $method, ?string $class_name, string $current_line): string
{
$is_async = strpos($current_line, 'async') !== false;
$static_version = $is_async ? "static async {$method}()" : "static {$method}()";
$class_info = $class_name ? " in class {$class_name}" : "";
$phase_description = $this->get_phase_description($method);
return "FRAMEWORK CONVENTION: RSX lifecycle method '{$method}'{$class_info} must be static.
PROBLEM:
The method is currently defined as an instance method, but the RSX framework
calls these methods statically during application initialization.
SOLUTION:
Change the method declaration to: {$static_version}
CURRENT (INCORRECT):
{$current_line}
CORRECTED:
{$static_version} {
// Your initialization code here
}
WHY THIS MATTERS:
- The RSX framework calls lifecycle methods statically on classes
- Instance methods cannot be called without instantiating the class
- The framework does not instantiate classes during initialization
- Using instance methods will cause the initialization to fail silently
LIFECYCLE PHASE:
This method runs during: {$phase_description}
ALL RSX LIFECYCLE METHODS (must be static):
- _on_framework_core_define() - Framework core modules define phase
- _on_framework_modules_define() - Framework extension modules define
- _on_framework_core_init() - Framework core initialization
- on_app_modules_define() - Application modules define phase
- on_app_define() - Application-wide define phase
- _on_framework_modules_init() - Framework modules initialization
- on_app_modules_init() - Application modules initialization
- on_app_init() - Application initialization
- on_app_ready() - Application ready (DOM loaded, components initialized)
Methods prefixed with underscore (_) are internal framework methods.
Application code should typically only use the non-underscore methods.";
}
/**
* Get description of when this lifecycle phase runs
*/
private function get_phase_description(string $method): string
{
$descriptions = [
'_on_framework_core_define' => 'Framework core module definition (internal)',
'_on_framework_modules_define' => 'Framework extension module definition (internal)',
'_on_framework_core_init' => 'Framework core initialization (internal)',
'on_app_modules_define' => 'Application module definition phase',
'on_app_define' => 'Application-wide definition phase',
'_on_framework_modules_init' => 'Framework module initialization (internal)',
'on_app_modules_init' => 'Application module initialization',
'on_app_init' => 'Application initialization',
'on_app_ready' => 'Application ready - DOM loaded, components initialized',
];
return $descriptions[$method] ?? 'Unknown phase';
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class NativeFunction_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-NATIVE-01';
}
public function get_name(): string
{
return 'JavaScript Native Function Usage Check';
}
public function get_description(): string
{
return 'Enforces RSpade functions instead of native JavaScript functions';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Native function patterns and their replacements
$function_patterns = [
// Array.isArray()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])Array\.isArray\s*\(/i',
'native' => 'Array.isArray()',
'replacement' => 'is_array()',
'message' => 'Use is_array() instead of Array.isArray().'
],
// parseFloat()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])parseFloat\s*\(/i',
'native' => 'parseFloat()',
'replacement' => 'float()',
'message' => 'Use float() instead of parseFloat().'
],
// parseInt()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])parseInt\s*\(/i',
'native' => 'parseInt()',
'replacement' => 'int()',
'message' => 'Use int() instead of parseInt().'
],
// String() constructor
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])String\s*\(/i',
'native' => 'String()',
'replacement' => 'str()',
'message' => 'Use str() instead of String().'
],
// encodeURIComponent()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])encodeURIComponent\s*\(/i',
'native' => 'encodeURIComponent()',
'replacement' => 'urlencode()',
'message' => 'Use urlencode() instead of encodeURIComponent().'
],
// decodeURIComponent()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])decodeURIComponent\s*\(/i',
'native' => 'decodeURIComponent()',
'replacement' => 'urldecode()',
'message' => 'Use urldecode() instead of decodeURIComponent().'
],
// JSON.stringify()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])JSON\.stringify\s*\(/i',
'native' => 'JSON.stringify()',
'replacement' => 'json_encode()',
'message' => 'Use json_encode() instead of JSON.stringify().'
],
// JSON.parse()
[
'pattern' => '/(?:^|[^a-zA-Z0-9_])JSON\.parse\s*\(/i',
'native' => 'JSON.parse()',
'replacement' => 'json_decode()',
'message' => 'Use json_decode() instead of JSON.parse().'
],
];
foreach ($function_patterns as $check) {
if (preg_match($check['pattern'], $sanitized_line)) {
$this->add_violation(
$file_path,
$line_number,
$check['message'],
trim($original_line),
"Replace '{$check['native']}' with '{$check['replacement']}'. " .
"RSpade provides PHP-like functions that should be used instead of native JavaScript functions. " .
"This provides consistency across PHP and JavaScript code and ensures predictable behavior.",
'medium'
);
break; // Only report first match per line
}
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class NoControllerSuffix_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-CONTROLLER-01';
}
public function get_name(): string
{
return 'No Controller Suffix in JavaScript';
}
public function get_description(): string
{
return 'JavaScript classes cannot use Controller suffix - reserved for PHP controllers';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check that JavaScript classes don't use Controller suffix
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Check if metadata has a class name (from Manifest)
if (!isset($metadata['class'])) {
return;
}
$class_name = $metadata['class'];
// Check if class name ends with Controller or _controller (case sensitive)
if (str_ends_with($class_name, 'Controller') || str_ends_with($class_name, '_controller')) {
// Find the line where the class is declared
$lines = explode("\n", $contents);
$line_number = 0;
$code_snippet = '';
foreach ($lines as $i => $line) {
// Look for ES6 class declaration
if (preg_match('/\bclass\s+' . preg_quote($class_name) . '\b/', $line)) {
$line_number = $i + 1;
$code_snippet = $this->get_code_snippet($lines, $i);
break;
}
}
$resolution = "JavaScript class '{$class_name}' uses the reserved 'Controller' suffix.\n\n";
$resolution .= "WHY THIS IS PROHIBITED:\n";
$resolution .= "The 'Controller' suffix is reserved exclusively for PHP controller classes because:\n";
$resolution .= "1. It maintains clear separation between frontend and backend code\n";
$resolution .= "2. It prevents confusion when making Ajax_Endpoint_Controller calls from JavaScript\n";
$resolution .= "3. It ensures consistent naming conventions across the codebase\n";
$resolution .= "4. Controllers handle HTTP requests and must be server-side PHP classes\n\n";
$resolution .= "HOW TO FIX:\n";
$resolution .= "Rename the JavaScript class to use a different suffix that describes its purpose:\n";
// Generate suggestions based on common patterns
$base_name = preg_replace('/(Controller|_controller)$/', '', $class_name);
$suggestions = [
$base_name . 'Manager' => 'For managing state or coordinating components',
$base_name . 'Handler' => 'For handling events or user interactions',
$base_name . 'Component' => 'For UI components',
$base_name . 'Service' => 'For service/API interaction logic',
$base_name . 'View' => 'For view-related logic',
$base_name . 'Widget' => 'For reusable UI widgets',
$base_name . 'Helper' => 'For utility/helper functions',
];
$resolution .= "\nSUGGESTED ALTERNATIVES:\n";
foreach ($suggestions as $suggestion => $description) {
$resolution .= "- {$suggestion}: {$description}\n";
}
$resolution .= "\nREMEMBER:\n";
$resolution .= "- JavaScript handles frontend logic and UI interactions\n";
$resolution .= "- PHP Controllers handle HTTP requests and backend logic\n";
$resolution .= "- When JavaScript needs to call a controller, use Ajax.internal() or fetch() to the controller's API endpoint\n\n";
$resolution .= "If this naming is absolutely required (extremely rare), add '@JS-CONTROLLER-01-EXCEPTION' comment.";
$this->add_violation(
$file_path,
$line_number,
"JavaScript class '{$class_name}' uses reserved 'Controller' suffix",
$code_snippet,
$resolution,
'high'
);
}
}
}

View File

@@ -0,0 +1,387 @@
<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* NoPageLoadAnimationRule - Detect animations that occur on page load
*
* This rule ensures all page elements appear immediately on initial page load.
* Animations are only allowed in specific scenarios:
* - After loading data via AJAX (discouraged but allowed)
* - In response to user interaction (click, checkbox, etc)
* - For position:absolute overlays like modals
*/
class NoPageLoadAnimation_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Animation methods that we want to detect
* Note: show() and hide() without duration are allowed as they're instant
*/
private const ANIMATION_METHODS = [
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideIn',
'slideOut',
'slideDown',
'slideUp',
'slideToggle'
];
/**
* Init methods where animations are not allowed (unless in anonymous function)
*/
private const INIT_METHODS = [
'_on_framework_core_define',
'_on_framework_modules_define',
'_on_framework_core_init',
'on_app_modules_define',
'on_app_define',
'_on_framework_modules_init',
'on_app_modules_init',
'on_app_init',
'on_app_ready'
];
/**
* Event binding methods that indicate user interaction
*/
private const EVENT_METHODS = [
'on',
'click',
'change',
'submit',
'keydown',
'keyup',
'keypress',
'mouseenter',
'mouseleave',
'hover',
'focus',
'blur',
'addEventListener'
];
/**
* AJAX methods that indicate data loading
*/
private const AJAX_METHODS = [
'ajax',
'get',
'post',
'getJSON',
'load',
'done',
'success',
'complete',
'then',
'fetch'
];
/**
* Get the unique identifier for this rule
*/
public function get_id(): string
{
return 'JS-ANIMATION-01';
}
/**
* Get the default severity level
*/
public function get_default_severity(): string
{
return 'critical';
}
/**
* Get the file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*.js', '*.jqhtml'];
}
/**
* Get the display name for this rule
*/
public function get_name(): string
{
return 'No Page Load Animation';
}
/**
* Get the description of what this rule checks
*/
public function get_description(): string
{
return 'Detects animations on initial page load - all elements must appear immediately';
}
/**
* Check the file contents for violations
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip minified files
if (str_contains($file_path, '.min.js')) {
return;
}
$lines = explode("\n", $contents);
$in_init_method = false;
$init_method_name = '';
$brace_depth = 0;
$init_method_depth = 0;
foreach ($lines as $line_num => $line) {
// Check if we're entering an init method
if ($this->_is_entering_init_method($line)) {
$in_init_method = true;
$init_method_name = $this->_extract_method_name($line);
$init_method_depth = $brace_depth;
}
// Track brace depth
$brace_depth += substr_count($line, '{');
$brace_depth -= substr_count($line, '}');
// Check if we're leaving the init method
if ($in_init_method && $brace_depth <= $init_method_depth) {
$in_init_method = false;
$init_method_name = '';
}
// If we're in an init method, check for animations
if ($in_init_method) {
// Check if this line is inside an anonymous function (including arrow functions)
if ($this->_is_in_anonymous_function($lines, $line_num) ||
$this->_is_in_event_handler($lines, $line_num) ||
$this->_is_in_ajax_callback($lines, $line_num)) {
continue; // Allowed context
}
// Check for animation calls
foreach (self::ANIMATION_METHODS as $method) {
// Pattern for jQuery style: .animate( or .fadeIn(
if (preg_match('/\.\s*' . preg_quote($method, '/') . '\s*\(/i', $line)) {
// Check for specific exceptions
if ($this->_is_allowed_animation($line, $lines, $line_num)) {
continue;
}
$this->add_violation(
$line_num + 1,
strpos($line, $method),
"Animation on page load detected: .{$method}()",
trim($line),
"Remove animation from {$init_method_name}(). Elements must appear immediately on page load.\n" .
"If you need to show/hide elements at page load, use .show() or .hide() instead of fade/slide effects.\n" .
"Animations are only allowed:\n" .
"- In response to user interaction (click, change, etc)\n" .
"- After AJAX data loading (discouraged)\n" .
"- For position:absolute overlays (modals)"
);
}
}
// Also check for direct opacity manipulation during init
if (preg_match('/\.css\s*\(\s*[\'"]opacity[\'"]/', $line) &&
(str_contains($line, 'setTimeout') || str_contains($line, 'setInterval'))) {
$this->add_violation(
$line_num + 1,
strpos($line, 'opacity'),
"Delayed opacity change on page load detected",
trim($line),
"Remove opacity animation from {$init_method_name}(). Use CSS for initial styling."
);
}
}
}
}
/**
* Check if we're entering an init method
*/
private function _is_entering_init_method(string $line): bool
{
foreach (self::INIT_METHODS as $method) {
// Match: static method_name() or function method_name()
if (preg_match('/(?:static\s+|function\s+)?' . preg_quote($method, '/') . '\s*\(/i', $line)) {
return true;
}
// Match: method_name: function()
if (preg_match('/' . preg_quote($method, '/') . '\s*:\s*function\s*\(/i', $line)) {
return true;
}
}
return false;
}
/**
* Extract the method name from a line
*/
private function _extract_method_name(string $line): string
{
foreach (self::INIT_METHODS as $method) {
if (str_contains($line, $method)) {
return $method;
}
}
return 'initialization';
}
/**
* Check if current context is inside an anonymous function (including arrow functions)
*/
private function _is_in_anonymous_function(array $lines, int $current_line): bool
{
// Count function depth by looking backwards
$function_depth = 0;
$paren_depth = 0;
$brace_depth = 0;
// Look backwards from current line to find function declarations
for ($i = $current_line; $i >= 0; $i--) {
$line = $lines[$i];
// Count braces to track scope
$brace_depth += substr_count($line, '}');
$brace_depth -= substr_count($line, '{');
// If we've exited all scopes, stop looking
if ($brace_depth > 0) {
break;
}
// Check for anonymous function patterns
// Regular function: function() { or function(args) {
if (preg_match('/function\s*\([^)]*\)\s*{/', $line)) {
return true;
}
// Arrow function: () => { or (args) => {
if (preg_match('/\([^)]*\)\s*=>\s*{/', $line)) {
return true;
}
// Single arg arrow function: arg => {
if (preg_match('/\w+\s*=>\s*{/', $line)) {
return true;
}
// Common callback patterns: setTimeout, setInterval, forEach, map, filter, etc.
if (preg_match('/(setTimeout|setInterval|forEach|map|filter|reduce|some|every|find)\s*\(\s*(function|\([^)]*\)\s*=>|\w+\s*=>)/', $line)) {
return true;
}
// jQuery each pattern: .each(function() or .each((i, el) =>
if (preg_match('/\.each\s*\(\s*(function|\([^)]*\)\s*=>)/', $line)) {
return true;
}
}
return false;
}
/**
* Check if current context is inside an event handler
*/
private function _is_in_event_handler(array $lines, int $current_line): bool
{
// Look backwards for event binding within 10 lines
$start = max(0, $current_line - 10);
for ($i = $current_line; $i >= $start; $i--) {
$line = $lines[$i];
foreach (self::EVENT_METHODS as $event) {
// Check for .on('click', or .click( patterns
if (preg_match('/\.\s*' . preg_quote($event, '/') . '\s*\(/i', $line)) {
return true;
}
// Check for addEventListener
if (str_contains($line, 'addEventListener')) {
return true;
}
}
}
return false;
}
/**
* Check if current context is inside an AJAX callback
*/
private function _is_in_ajax_callback(array $lines, int $current_line): bool
{
// Look backwards for AJAX methods within 10 lines
$start = max(0, $current_line - 10);
for ($i = $current_line; $i >= $start; $i--) {
$line = $lines[$i];
foreach (self::AJAX_METHODS as $ajax) {
if (preg_match('/\.\s*' . preg_quote($ajax, '/') . '\s*\(/i', $line) ||
preg_match('/\$\s*\.\s*' . preg_quote($ajax, '/') . '\s*\(/i', $line)) {
return true;
}
}
// Check for promise patterns
if (str_contains($line, '.then(') || str_contains($line, 'async ') || str_contains($line, 'await ')) {
return true;
}
}
return false;
}
/**
* Check if this is an allowed animation exception
*/
private function _is_allowed_animation(string $line, array $lines, int $line_num): bool
{
// Check for modal or overlay keywords
$allowed_selectors = [
'modal',
'overlay',
'popup',
'dialog',
'tooltip',
'dropdown-menu',
'position-absolute',
'position-fixed'
];
foreach ($allowed_selectors as $selector) {
if (str_contains(strtolower($line), $selector)) {
return true;
}
}
// Check if the element being animated has position:absolute in a nearby style
// This is harder to detect statically, so we'll be conservative
// Check for comments indicating AJAX loading
if ($line_num > 0) {
$prev_line = $lines[$line_num - 1];
if (str_contains(strtolower($prev_line), 'ajax') ||
str_contains(strtolower($prev_line), 'load') ||
str_contains(strtolower($prev_line), 'fetch')) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* JavaScript 'this' Usage Rule
*
* PHILOSOPHY: Remove ambiguity about what 'this' refers to in all contexts.
*
* RULES:
* 1. Anonymous functions: Can use 'const $var = $(this)' as first line (jQuery pattern)
* 2. Instance methods: Must use 'const that = this' as first line (constructors exempt)
* 3. Static methods: Use Class_Name OR 'const CurrentClass = this' for polymorphism
* 4. Arrow functions: Ignored (they inherit 'this' context)
* 5. Constructors: Direct 'this' usage allowed for property assignment
*
* PATTERNS:
* - jQuery: const $element = $(this) // Variable must start with $
* - Instance: const that = this // Standard instance aliasing
* - Static (exact): Use Class_Name // When you need exact class
* - Static (polymorphic): const CurrentClass = this // When inherited classes need different behavior
*
* This rule does NOT try to detect all jQuery callbacks - it offers the jQuery
* pattern as an option when 'this' violations are found in anonymous functions.
*/
class ThisUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-THIS-01';
}
public function get_name(): string
{
return "JavaScript 'this' Usage Check";
}
public function get_description(): string
{
return "Enforces clear 'this' patterns: jQuery callbacks use '\$element = \$(this)', instance methods use 'that = this'";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript file for improper 'this' usage
*/
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;
}
// Only check JavaScript files that contain ES6 classes
if (!isset($metadata['class'])) {
return; // Not a class file
}
// Get violations from AST parser
$violations = $this->parse_with_acorn($file_path);
if (empty($violations)) {
return;
}
// Process violations
foreach ($violations as $violation) {
$this->add_violation(
$file_path,
$violation['line'],
$violation['message'],
$violation['codeSnippet'],
$violation['remediation'],
$this->get_default_severity()
);
}
}
/**
* Use Node.js with acorn to parse JavaScript and find violations
* Uses external parser script stored in resources directory
*/
private function parse_with_acorn(string $file_path): array
{
// Setup cache directory
$cache_dir = storage_path('rsx-tmp/cache/code-quality/js-this');
if (!is_dir($cache_dir)) {
mkdir($cache_dir, 0755, true);
}
// Cache based on file modification time
$cache_key = md5($file_path) . '-' . filemtime($file_path);
$cache_file = $cache_dir . '/' . $cache_key . '.json';
// Check cache first
if (file_exists($cache_file)) {
$cached = json_decode(file_get_contents($cache_file), true);
if ($cached !== null) {
return $cached;
}
}
// Clean old cache files for this source file
$pattern = $cache_dir . '/' . md5($file_path) . '-*.json';
foreach (glob($pattern) as $old_cache) {
if ($old_cache !== $cache_file) {
unlink($old_cache);
}
}
// Path to the parser script
$parser_script = __DIR__ . '/resource/this-usage-parser.js';
if (!file_exists($parser_script)) {
// Parser script missing - fatal error
throw new \RuntimeException("JS-THIS parser script missing: {$parser_script}");
}
// Run Node.js parser with the external script
$output = shell_exec("cd /tmp && node " . escapeshellarg($parser_script) . " " . escapeshellarg($file_path) . " 2>&1");
if (!$output) {
return [];
}
$result = json_decode($output, true);
if (!$result) {
return [];
}
// Check for errors from the parser
if (isset($result['error'])) {
// Parser encountered an error but it's not fatal for the rule
return [];
}
$violations = $result['violations'] ?? [];
// Cache the result
file_put_contents($cache_file, json_encode($violations));
return $violations;
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class TypeofCheck_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-TYPEOF-01';
}
public function get_name(): string
{
return 'JavaScript typeof Usage Check';
}
public function get_description(): string
{
return 'Enforces RSpade type checking functions instead of typeof';
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx/ directory
if (!str_contains($file_path, '/rsx/') && !str_starts_with($file_path, 'rsx/')) {
return;
}
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
// Get both original and sanitized content
$original_content = file_get_contents($file_path);
$original_lines = explode("\n", $original_content);
// Get sanitized content with comments removed
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$sanitized_lines = $sanitized_data['lines'];
foreach ($sanitized_lines as $line_num => $sanitized_line) {
$line_number = $line_num + 1;
// Skip if the line is empty in sanitized version
if (trim($sanitized_line) === '') {
continue;
}
$original_line = $original_lines[$line_num] ?? $sanitized_line;
// Check for typeof patterns and their replacements
$typeof_patterns = [
// typeof something == 'string' or === 'string' (single or double quotes)
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[!=]=+\s*[\'"]string[\'"]/i',
'replacement' => 'is_string($1)',
'message' => 'Use is_string() instead of typeof for string type checking.'
],
// typeof something == 'object' or === 'object'
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[!=]=+\s*[\'"]object[\'"]/i',
'replacement' => 'is_object($1)',
'message' => 'Use is_object() instead of typeof for object type checking.'
],
// typeof value != 'undefined' or !== 'undefined'
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*!==?\s*[\'"]undefined[\'"]/i',
'replacement' => 'isset($1)',
'message' => 'Use isset() instead of typeof for undefined checking.'
],
// typeof value == 'undefined' or === 'undefined'
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*===?\s*[\'"]undefined[\'"]/i',
'replacement' => '!isset($1)',
'message' => 'Use !isset() instead of typeof for undefined checking.'
],
// typeof something == 'number' or === 'number'
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[!=]=+\s*[\'"]number[\'"]/i',
'replacement' => 'is_numeric($1)',
'message' => 'Use is_numeric() instead of typeof for number type checking.'
],
// typeof something == 'boolean' or === 'boolean'
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[!=]=+\s*[\'"]boolean[\'"]/i',
'replacement' => 'is_bool($1)',
'message' => 'Use is_bool() instead of typeof for boolean type checking.'
],
// typeof something == 'function' or === 'function'
[
'pattern' => '/typeof\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[!=]=+\s*[\'"]function[\'"]/i',
'replacement' => 'is_callable($1)',
'message' => 'Use is_callable() instead of typeof for function type checking.'
],
];
foreach ($typeof_patterns as $check) {
if (preg_match($check['pattern'], $sanitized_line, $matches)) {
$this->add_violation(
$file_path,
$line_number,
$check['message'],
trim($original_line),
"Replace with '{$check['replacement']}'. " .
"RSpade provides PHP-like type checking functions that should be used instead of native JavaScript typeof. " .
"This provides consistency across PHP and JavaScript code.",
'medium'
);
break; // Only report first match per line
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class VarUsage_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-VAR-01';
}
public function get_name(): string
{
return 'JavaScript var Keyword Check';
}
public function get_description(): string
{
return "Enforces use of 'let' and 'const' instead of 'var'";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'medium';
}
/**
* Check JavaScript file for 'var' keyword usage (from line 1269)
* Enforces use of 'let' and 'const' instead of 'var'
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Check for 'var' keyword at the beginning of a line or after ( or ;
// This catches var declarations in for loops and inline declarations
if (preg_match('/(?:^\s*|[\(;]\s*)var\s+/', $line)) {
$this->add_violation(
$file_path,
$line_number,
"Use of 'var' keyword is not allowed. Use 'let' or 'const' instead for better scoping and immutability.",
trim($line),
"Replace 'var' with 'let' for mutable variables or 'const' for immutable values.",
'medium'
);
}
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\Support\FileSanitizer;
class WindowAssignment_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'JS-WINDOW-01';
}
public function get_name(): string
{
return 'Window Assignment Check';
}
public function get_description(): string
{
return "Prohibits window.ClassName = ClassName assignments in ALL code (framework and application). RSX uses simple concatenation, not modules - all classes are automatically global when files are concatenated together, just like JavaScript worked in 1998 before module systems existed.";
}
public function get_file_patterns(): array
{
return ['*.js'];
}
public function get_default_severity(): string
{
return 'high';
}
/**
* Check JavaScript files for window.ClassName = ClassName pattern
* RSX uses concatenation, not modules - all classes become global automatically
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip vendor and node_modules directories
if (str_contains($file_path, '/vendor/') || str_contains($file_path, '/node_modules/')) {
return;
}
// Skip CodeQuality directory
if (str_contains($file_path, '/CodeQuality/')) {
return;
}
$sanitized_data = FileSanitizer::sanitize_javascript($file_path);
$lines = $sanitized_data['lines'];
foreach ($lines as $line_num => $line) {
$line_number = $line_num + 1;
// Skip comments
$trimmed_line = trim($line);
if (str_starts_with($trimmed_line, '//') || str_starts_with($trimmed_line, '*')) {
continue;
}
// Check for window.ClassName = ClassName pattern
// This matches window.Anything = Anything
if (preg_match('/window\.(\w+)\s*=\s*\1\s*;?/', $line, $matches)) {
$class_name = $matches[1];
$this->add_violation(
$file_path,
$line_number,
"Manual window assignment detected for class '{$class_name}'. RSX framework automatically makes classes global.",
trim($line),
"Remove the line 'window.{$class_name} = {$class_name}' - the class is already global via the RSX manifest system.",
'high'
);
}
// Also check for generic window assignments that might be problematic
// This catches window.SomeName = OtherName patterns that may also be incorrect
if (preg_match('/^\s*window\.([A-Z]\w+)\s*=\s*([A-Z]\w+)\s*;?/', $line, $matches)) {
// Only flag if it's not the same-name pattern (already handled above)
if ($matches[1] !== $matches[2]) {
$this->add_violation(
$file_path,
$line_number,
"Manual window assignment detected. Consider if this is necessary - RSX classes are automatically global.",
trim($line),
"Verify if this manual window assignment is needed. RSX framework classes don't need window assignment.",
'medium'
);
}
}
}
}
}

View File

@@ -0,0 +1,375 @@
#!/usr/bin/env node
/**
* JavaScript 'this' usage parser for code quality checks
*
* PURPOSE: Parse JavaScript files to find 'this' usage violations
* according to RSpade coding standards.
*
* USAGE: node this-usage-parser.js <filepath>
* OUTPUT: JSON with violations array
*
* RULES:
* - Anonymous functions can use: const $var = $(this) as first line
* - Instance methods must use: const that = this as first line
* - Static methods should never use 'this', use ClassName instead
* - Arrow functions are ignored (they inherit 'this')
*
* @FILENAME-CONVENTION-EXCEPTION - Node.js utility script
*/
const fs = require('fs');
const acorn = require('acorn');
const walk = require('acorn-walk');
// Known jQuery callback methods - used for better remediation messages
const JQUERY_CALLBACKS = new Set([
'click', 'dblclick', 'mouseenter', 'mouseleave', 'mousedown', 'mouseup',
'mousemove', 'mouseover', 'mouseout', 'change', 'submit', 'focus', 'blur',
'keydown', 'keyup', 'keypress', 'resize', 'scroll', 'select', 'load',
'on', 'off', 'one', 'each', 'map', 'filter',
'fadeIn', 'fadeOut', 'slideDown', 'slideUp', 'animate',
'done', 'fail', 'always', 'then', 'ready', 'hover'
]);
function analyzeFile(filePath) {
try {
const code = fs.readFileSync(filePath, 'utf8');
const lines = code.split('\n');
let ast;
try {
ast = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
locations: true
});
} catch (e) {
// Parse error - return empty violations
return { violations: [], error: `Parse error: ${e.message}` };
}
const violations = [];
const classInfo = new Map(); // Track class info
// First pass: identify all classes and their types
walk.simple(ast, {
ClassDeclaration(node) {
const hasStaticInit = node.body.body.some(member =>
member.static && member.key?.name === 'init'
);
classInfo.set(node.id.name, {
isStatic: hasStaticInit
});
}
});
// Helper to check if first line of function has valid pattern
function checkFirstLinePattern(funcNode) {
if (!funcNode.body || !funcNode.body.body || funcNode.body.body.length === 0) {
return null;
}
let checkIndex = 0;
const firstStmt = funcNode.body.body[0];
// Check if first statement is e.preventDefault() or similar
if (firstStmt.type === 'ExpressionStatement' &&
firstStmt.expression?.type === 'CallExpression' &&
firstStmt.expression?.callee?.type === 'MemberExpression' &&
firstStmt.expression?.callee?.property?.name === 'preventDefault') {
// First line is preventDefault, check second line for pattern
checkIndex = 1;
if (funcNode.body.body.length <= 1) {
return null; // No second statement
}
}
const targetStmt = funcNode.body.body[checkIndex];
if (targetStmt.type !== 'VariableDeclaration') {
return null;
}
const firstDecl = targetStmt.declarations[0];
if (!firstDecl || !firstDecl.init) {
return null;
}
const varKind = targetStmt.kind; // 'const', 'let', or 'var'
// Check for 'that = this' pattern
if (firstDecl.id.name === 'that' &&
firstDecl.init.type === 'ThisExpression') {
if (varKind !== 'const') {
return 'that-pattern-wrong-kind';
}
return 'that-pattern';
}
// Check for 'CurrentClass = this' pattern (for static polymorphism)
if (firstDecl.id.name === 'CurrentClass' &&
firstDecl.init.type === 'ThisExpression') {
if (varKind !== 'const') {
return 'currentclass-pattern-wrong-kind';
}
return 'currentclass-pattern';
}
// Check for '$var = $(this)' pattern
if (firstDecl.id.name.startsWith('$') &&
firstDecl.init.type === 'CallExpression' &&
firstDecl.init.callee.name === '$' &&
firstDecl.init.arguments.length === 1 &&
firstDecl.init.arguments[0].type === 'ThisExpression') {
if (varKind !== 'const') {
return 'jquery-pattern-wrong-kind';
}
return 'jquery-pattern';
}
return null;
}
// Helper to detect if we're in a jQuery callback (best effort)
function isLikelyJQueryCallback(ancestors) {
for (let i = ancestors.length - 1; i >= 0; i--) {
const node = ancestors[i];
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
const methodName = node.callee.property.name;
if (JQUERY_CALLBACKS.has(methodName)) {
return true;
}
}
}
return false;
}
// Walk the AST looking for 'this' usage
walk.ancestor(ast, {
ThisExpression(node, ancestors) {
// Skip arrow functions - they inherit 'this'
for (const ancestor of ancestors) {
if (ancestor.type === 'ArrowFunctionExpression') {
return; // Skip - arrow functions inherit context
}
}
// Find containing function and class
let containingFunc = null;
let containingClass = null;
let isAnonymousFunc = false;
let isStaticMethod = false;
let isConstructor = false;
for (let i = ancestors.length - 1; i >= 0; i--) {
const ancestor = ancestors[i];
if (!containingFunc && (
ancestor.type === 'FunctionExpression' ||
ancestor.type === 'FunctionDeclaration'
)) {
containingFunc = ancestor;
isAnonymousFunc = ancestor.type === 'FunctionExpression';
}
if (!containingClass && (
ancestor.type === 'ClassDeclaration' ||
ancestor.type === 'ClassExpression'
)) {
containingClass = ancestor;
}
if (ancestor.type === 'MethodDefinition') {
isStaticMethod = ancestor.static;
isConstructor = ancestor.kind === 'constructor';
}
}
if (!containingFunc) {
return; // Not in a function
}
// Skip constructors - 'this' is allowed for property assignment
if (isConstructor) {
return;
}
// Check if this is part of the allowed first-line pattern with const
const parent = ancestors[ancestors.length - 2];
const firstStmt = containingFunc.body?.body?.[0];
let checkIndex = 0;
// Check if first statement is preventDefault
const hasPreventDefault = firstStmt?.type === 'ExpressionStatement' &&
firstStmt?.expression?.type === 'CallExpression' &&
firstStmt?.expression?.callee?.type === 'MemberExpression' &&
firstStmt?.expression?.callee?.property?.name === 'preventDefault';
if (hasPreventDefault) {
checkIndex = 1;
}
const targetStmt = containingFunc.body?.body?.[checkIndex];
const isTargetConst = targetStmt?.type === 'VariableDeclaration' && targetStmt?.kind === 'const';
// Check if this 'this' is inside $(this) on the first or second line
// For jQuery pattern: const $var = $(this)
if (parent && parent.type === 'CallExpression' && parent.callee?.name === '$') {
// This is $(this) - check if it's in the right position with const
if (isTargetConst &&
targetStmt?.declarations?.[0]?.init === parent &&
targetStmt?.declarations?.[0]?.id?.name?.startsWith('$')) {
return; // This is const $var = $(this) in correct position
}
}
// Check if this 'this' is the 'const that = this' in correct position
if (parent && parent.type === 'VariableDeclarator' &&
parent.id?.name === 'that' &&
isTargetConst &&
targetStmt?.declarations?.[0]?.id?.name === 'that') {
return; // This is 'const that = this' in correct position
}
// Check if this 'this' is the 'const CurrentClass = this' in correct position
if (parent && parent.type === 'VariableDeclarator' &&
parent.id?.name === 'CurrentClass' &&
isTargetConst &&
targetStmt?.declarations?.[0]?.id?.name === 'CurrentClass') {
return; // This is 'const CurrentClass = this' in correct position
}
// Check what pattern is used
const pattern = checkFirstLinePattern(containingFunc);
// Determine the violation and remediation
let message = '';
let remediation = '';
const lineNum = node.loc.start.line;
const codeSnippet = lines[lineNum - 1].trim();
const className = containingClass?.id?.name || 'unknown';
const isJQueryContext = isLikelyJQueryCallback(ancestors);
// Anonymous functions take precedence - even if inside a static method
if (isAnonymousFunc) {
if (!pattern) {
// Check if there's a preventDefault on the first line
const firstStmt = containingFunc.body?.body?.[0];
const hasPreventDefault = firstStmt?.type === 'ExpressionStatement' &&
firstStmt?.expression?.type === 'CallExpression' &&
firstStmt?.expression?.callee?.type === 'MemberExpression' &&
firstStmt?.expression?.callee?.property?.name === 'preventDefault';
if (isJQueryContext) {
message = `'this' in jQuery callback should be aliased for clarity.`;
if (hasPreventDefault) {
remediation = `Add 'const $element = $(this);' as the second line (after preventDefault), then use $element instead of 'this'.`;
} else {
remediation = `Add 'const $element = $(this);' as the first line of this function, then use $element instead of 'this'.`;
}
} else {
message = `Ambiguous 'this' usage in anonymous function.`;
if (hasPreventDefault) {
remediation = `If this is a jQuery callback: Add 'const $element = $(this);' as the second line (after preventDefault).\n` +
`If this is an instance context: Add 'const that = this;' as the second line.\n` +
`Then use the aliased variable instead of 'this'.`;
} else {
remediation = `If this is a jQuery callback: Add 'const $element = $(this);' as first line.\n` +
`If this is an instance context: Add 'const that = this;' as first line.\n` +
`Then use the aliased variable instead of 'this'.`;
}
}
} else if (pattern === 'that-pattern' || pattern === 'jquery-pattern') {
message = `'this' used after aliasing. Use the aliased variable instead.`;
// Find the variable declaration (might be first or second statement)
let varDeclIndex = 0;
const firstStmt = containingFunc.body?.body?.[0];
if (firstStmt?.type === 'ExpressionStatement' &&
firstStmt?.expression?.callee?.property?.name === 'preventDefault') {
varDeclIndex = 1;
}
const varName = containingFunc.body?.body?.[varDeclIndex]?.declarations?.[0]?.id?.name;
remediation = pattern === 'jquery-pattern'
? `You already have 'const ${varName} = $(this)'. Use that variable instead of 'this'.`
: `You already have 'const that = this'. Use 'that' instead of 'this'.`;
} else if (pattern === 'that-pattern-wrong-kind') {
message = `Instance alias must use 'const', not 'let' or 'var'.`;
remediation = `Change to 'const that = this;' - the instance reference should never be reassigned.`;
} else if (pattern === 'jquery-pattern-wrong-kind') {
message = `jQuery element alias must use 'const', not 'let' or 'var'.`;
remediation = `Change to 'const $element = $(this);' - jQuery element references should never be reassigned.`;
}
} else if (isStaticMethod) {
if (!pattern) {
message = `Static method in '${className}' should not use naked 'this'.`;
remediation = `Static methods have two options:\n` +
`1. If you need the exact class (no polymorphism): Replace 'this' with '${className}'\n` +
`2. If you need polymorphism (inherited classes): Add 'const CurrentClass = this;' as first line\n` +
` Then use CurrentClass.name or CurrentClass.property for polymorphic access.\n` +
`Consider: Does this.name need to work for child classes? If yes, use CurrentClass pattern.`;
} else if (pattern === 'currentclass-pattern') {
message = `'this' used after aliasing to CurrentClass. Use 'CurrentClass' instead.`;
remediation = `You already have 'const CurrentClass = this;'. Use 'CurrentClass' for all access, not naked 'this'.`;
} else if (pattern === 'currentclass-pattern-wrong-kind') {
message = `CurrentClass pattern must use 'const', not 'let' or 'var'.`;
remediation = `Change to 'const CurrentClass = this;' - the CurrentClass reference should never be reassigned.`;
} else if (pattern === 'jquery-pattern') {
// jQuery pattern in static method's anonymous function is OK
return;
} else if (pattern === 'jquery-pattern-wrong-kind') {
message = `jQuery element alias must use 'const', not 'let' or 'var'.`;
remediation = `Change to 'const $element = $(this);' - jQuery element references should never be reassigned.`;
}
if (isAnonymousFunc && !pattern) {
remediation += `\nException: If this is a jQuery callback, add 'const $element = $(this);' as the first line.`;
}
} else {
// Instance method
if (!pattern) {
message = `Instance method in '${className}' must alias 'this' for clarity.`;
remediation = `Add 'const that = this;' as the first line of this method, then use 'that' instead of 'this'.\n` +
`This applies even to ORM models and similar classes where direct property access is common.\n` +
`Note: Constructors are exempt - 'this' is allowed directly in constructors for property assignment.\n` +
`Example: Instead of 'return this.name;' use 'const that = this; return that.name;'`;
} else if (pattern === 'that-pattern') {
message = `'this' used after aliasing to 'that'. Use 'that' instead.`;
remediation = `You already have 'const that = this'. Use 'that' consistently throughout the method.\n` +
`All property access should use 'that.property' not 'this.property'.`;
} else if (pattern === 'that-pattern-wrong-kind') {
message = `Instance alias must use 'const', not 'let' or 'var'.`;
remediation = `Change to 'const that = this;' - the instance reference should never be reassigned.`;
}
}
if (message) {
violations.push({
line: lineNum,
message: message,
codeSnippet: codeSnippet,
remediation: remediation
});
}
}
});
return { violations: violations };
} catch (error) {
return { violations: [], error: error.message };
}
}
// Main execution
const filePath = process.argv[2];
if (!filePath) {
console.log(JSON.stringify({ violations: [], error: 'No file path provided' }));
process.exit(1);
}
if (!fs.existsSync(filePath)) {
console.log(JSON.stringify({ violations: [], error: `File not found: ${filePath}` }));
process.exit(1);
}
const result = analyzeFile(filePath);
console.log(JSON.stringify(result));