diff --git a/app/Constants.php b/app/Constants.php index accb22ed9..25d0c09b9 100755 --- a/app/Constants.php +++ b/app/Constants.php @@ -111,6 +111,73 @@ class Constants 'DATETIME' => 'M j, Y g:i A' ]; + /** + * Generic operation/action verbs used in CRUD patterns + * + * These are common verbs developers use for actions performed ON modules/features. + * Used by code quality rules to detect when renaming files would create + * proliferation of identical generic filenames (e.g., many "edit_action.js" files). + */ + public const GENERIC_OPERATIONS = [ + // Core CRUD + 'create', 'read', 'update', 'delete', + 'add', 'view', 'edit', 'remove', + 'new', 'show', 'modify', 'destroy', + + // List/Browse + 'list', 'index', 'browse', 'search', 'find', 'filter', 'query', + + // Data retrieval + 'get', 'fetch', 'load', 'retrieve', 'pull', 'lookup', + + // Data persistence + 'save', 'store', 'persist', 'write', 'put', 'insert', + 'post', 'submit', 'send', 'push', + + // File operations + 'upload', 'download', 'import', 'export', 'attach', + + // Sync/Refresh + 'sync', 'refresh', 'reload', 'reset', + + // Detail views + 'details', 'detail', 'info', 'summary', 'overview', 'single', 'item', + + // Form operations + 'form', 'input', 'enter', 'wizard', 'step', + + // Batch operations + 'bulk', 'batch', 'mass', 'multi', 'all', + + // State changes + 'archive', 'restore', 'recover', 'trash', + 'activate', 'deactivate', 'enable', 'disable', + 'publish', 'unpublish', 'draft', + 'approve', 'reject', 'review', 'pending', + 'lock', 'unlock', 'freeze', + 'open', 'close', 'complete', 'finish', + 'start', 'stop', 'pause', 'resume', 'cancel', + + // Auth actions + 'login', 'logout', 'signin', 'signout', 'auth', + 'register', 'signup', 'join', + 'invite', 'accept', 'confirm', 'verify', + + // Processing + 'process', 'handle', 'execute', 'run', + 'convert', 'transform', 'parse', 'generate', + 'check', 'test', 'validate', + + // Communication actions + 'notify', 'alert', 'email', 'message', + + // Selection/Movement + 'select', 'pick', 'choose', 'assign', 'move', 'copy', 'clone', + + // Output + 'preview', 'print', 'report', + ]; + // Additional constants can be added here as needed: // const MIME_TYPES = [...]; // const COUNTRY_CODES = [...]; diff --git a/app/RSpade/CodeQuality/Rules/Convention/FilenameRedundantPrefix_CodeQualityRule.php b/app/RSpade/CodeQuality/Rules/Convention/FilenameRedundantPrefix_CodeQualityRule.php index 726f9588e..de20010b1 100755 --- a/app/RSpade/CodeQuality/Rules/Convention/FilenameRedundantPrefix_CodeQualityRule.php +++ b/app/RSpade/CodeQuality/Rules/Convention/FilenameRedundantPrefix_CodeQualityRule.php @@ -2,6 +2,7 @@ namespace App\RSpade\CodeQuality\Rules\Convention; +use App\Constants; use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract; /** @@ -145,6 +146,12 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract */ private function check_additive_case(string $file, string $filename, string $identifier, string $extension, string $dir_path, bool $is_rspade): void { + // Skip additive suggestions for framework code - canonical single-word class names + // like Ajax, Manifest, Session, Task are intentional and shouldn't be renamed + if ($is_rspade) { + return; + } + $filename_base = $this->get_filename_base($filename, $extension); $identifier_parts = explode('_', $identifier); @@ -180,6 +187,13 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract return; // Can't suggest - already taken } + // CRITICAL: Validate against class/identifier name using MANIFEST-FILENAME-01 logic + // The suggested filename must still be valid for the class name + $suggested_base = $is_rspade ? $suggested_identifier : strtolower($suggested_identifier); + if (!$this->is_valid_short_filename($suggested_base, $identifier, $dir_path, $is_rspade)) { + return; // Suggested rename would violate MANIFEST-FILENAME-01 - skip suggestion + } + // Add violation $identifier_type = $this->get_identifier_type($extension); $this->add_violation( @@ -273,6 +287,12 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract $filename_base = $this->get_filename_base($filename, $extension); $relative_path = str_replace(base_path() . '/', '', $file_path); + // Before checking this file, verify that ALL files sharing the same prefix can be + // consistently renamed. If not, renaming just this file would break visual grouping. + if (!$this->can_all_siblings_with_prefix_be_renamed($file_path, $filename_base, $dir_path, $is_rspade)) { + return; // Skip - would break visual grouping with sibling files + } + // Create a single-file "group" and check it $file_info = [ 'file_path' => $file_path, @@ -284,6 +304,89 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract $this->check_prefix_group_all_extensions($filename_base, [$file_info], $dir_path, $is_rspade); } + /** + * Check if ALL files in the directory sharing the same prefix can be consistently renamed. + * This prevents breaking visual grouping when some files can be shortened and others cannot. + * + * Example: settings_general_action.js and settings_general_controller.php share prefix "settings_general_". + * If the action can remove "settings_" but the controller cannot (due to different class name), + * we should NOT suggest renaming either to maintain visual grouping. + */ + private function can_all_siblings_with_prefix_be_renamed(string $file_path, string $filename_base, string $dir_path, bool $is_rspade): bool + { + $directory = dirname($file_path); + + // Determine what prefix would be removed from this file + $filename_parts = explode('_', $filename_base); + $dir_parts = array_values(array_filter(explode('/', $dir_path))); + $parts_to_remove = $this->count_redundant_prefix_parts($filename_parts, $dir_parts); + + if ($parts_to_remove === 0) { + return true; // No prefix to remove, so no grouping concern + } + + // The prefix that would be removed (e.g., "settings_" from "settings_general_action") + $prefix_to_remove = implode('_', array_slice($filename_parts, 0, $parts_to_remove)) . '_'; + + // Find all files in directory that share this prefix + $all_extensions = ['php', 'js', 'jqhtml', 'blade.php', 'scss']; + $sibling_files = []; + + foreach ($all_extensions as $ext) { + $pattern = $directory . '/*.' . $ext; + $files = glob($pattern); + if ($files) { + foreach ($files as $sibling_path) { + $sibling_filename = basename($sibling_path); + $sibling_base = $this->get_filename_base($sibling_filename, $ext); + + // Check if sibling shares the same prefix + $sibling_base_normalized = $is_rspade ? $sibling_base : strtolower($sibling_base); + $prefix_normalized = $is_rspade ? rtrim($prefix_to_remove, '_') : strtolower(rtrim($prefix_to_remove, '_')); + + if (str_starts_with($sibling_base_normalized, $prefix_normalized . '_')) { + $sibling_files[] = [ + 'file_path' => $sibling_path, + 'filename_base' => $sibling_base, + 'extension' => $ext, + ]; + } + } + } + } + + // If no siblings share the prefix, no grouping concern + if (count($sibling_files) <= 1) { + return true; + } + + // Check if ALL siblings can have the same prefix removed + foreach ($sibling_files as $sibling) { + $sibling_parts = explode('_', $sibling['filename_base']); + $short_name_parts = array_slice($sibling_parts, $parts_to_remove); + $short_name = implode('_', $short_name_parts); + + // Skip if would become single-segment + if (count($short_name_parts) < 2) { + return false; // Can't rename consistently - would create single-segment filename + } + + // Get class name for this sibling + $relative_path = str_replace(base_path() . '/', '', $sibling['file_path']); + $file_metadata = \App\RSpade\Core\Manifest\Manifest::get_file($relative_path); + $class_name = $file_metadata['class'] ?? $file_metadata['id'] ?? null; + + if ($class_name !== null) { + $suggested_base = $is_rspade ? $short_name : strtolower($short_name); + if (!$this->is_valid_short_filename($suggested_base, $class_name, $dir_path, $is_rspade)) { + return false; // This sibling can't be renamed - would violate manifest rule + } + } + } + + return true; // All siblings can be consistently renamed + } + /** * Detect the extension from a filename (handles .blade.php) */ @@ -519,6 +622,15 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract $short_name_lower = strtolower($short_name); $immediate_dir_lower = strtolower($immediate_dir); + // Generic operation directories - renaming files in these creates proliferation + // of identical filenames across the codebase (e.g., many "edit_action.js" files) + if (in_array($immediate_dir_lower, Constants::GENERIC_OPERATIONS)) { + $first_segment = strtolower($short_name_parts[0] ?? ''); + if ($first_segment === $immediate_dir_lower) { + return false; // Would create generic operation filename proliferation - abort + } + } + if (!str_contains($short_name_lower, $immediate_dir_lower)) { return false; // Would lose essential directory context - abort group rename } @@ -534,6 +646,20 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract return false; // Conflict - can't rename group } + // CRITICAL: Validate against class name using MANIFEST-FILENAME-01 logic + // Get the class/identifier name from manifest + $relative_path = str_replace(base_path() . '/', '', $file_info['file_path']); + $file_metadata = \App\RSpade\Core\Manifest\Manifest::get_file($relative_path); + $class_name = $file_metadata['class'] ?? $file_metadata['id'] ?? null; + + if ($class_name !== null) { + // Check if the suggested short filename would be valid for this class + $suggested_base = $is_rspade ? $short_name : strtolower($short_name); + if (!$this->is_valid_short_filename($suggested_base, $class_name, $dir_path, $is_rspade)) { + return false; // Suggested rename would violate MANIFEST-FILENAME-01 - abort + } + } + $rename_suggestions[] = [ 'old' => $file_info['filename'], 'new' => $short_filename, @@ -584,6 +710,15 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract $short_name_lower = strtolower($short_name); $immediate_dir_lower = strtolower($immediate_dir); + // Generic operation directories - renaming files in these creates proliferation + // of identical filenames across the codebase (e.g., many "edit_action.js" files) + if (in_array($immediate_dir_lower, Constants::GENERIC_OPERATIONS)) { + $first_segment = strtolower($short_name_parts[0] ?? ''); + if ($first_segment === $immediate_dir_lower) { + return false; // Would create generic operation filename proliferation - abort + } + } + if (!str_contains($short_name_lower, $immediate_dir_lower)) { return false; // Would lose essential directory context - abort group rename } @@ -598,6 +733,20 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract return false; // Conflict - can't rename group } + // CRITICAL: Validate against class name using MANIFEST-FILENAME-01 logic + // Get the class/identifier name from manifest + $relative_path = str_replace(base_path() . '/', '', $file_info['file_path']); + $file_metadata = \App\RSpade\Core\Manifest\Manifest::get_file($relative_path); + $class_name = $file_metadata['class'] ?? $file_metadata['id'] ?? null; + + if ($class_name !== null) { + // Check if the suggested short filename would be valid for this class + $suggested_base = $is_rspade ? $short_name : strtolower($short_name); + if (!$this->is_valid_short_filename($suggested_base, $class_name, $dir_path, $is_rspade)) { + return false; // Suggested rename would violate MANIFEST-FILENAME-01 - abort + } + } + $rename_suggestions[] = [ 'old' => $file_info['filename'], 'new' => $short_filename, @@ -702,6 +851,94 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract return pathinfo($filename, PATHINFO_FILENAME); } + /** + * Extract valid short name from class/id by checking if directory + filename parts match full name + * This is the same logic as FilenameClassMatch_CodeQualityRule::extract_short_name() + * + * Returns the valid short name if one exists, or null if only full name is valid. + */ + private function extract_valid_short_name(string $full_name, string $dir_path): ?string + { + // Short names only allowed in ./rsx directory, not in framework code (/app/RSpade) + if (str_contains($dir_path, '/app/RSpade') || str_contains($dir_path, 'app/RSpade')) { + return null; + } + + // Split the full name by underscores + $name_parts = explode('_', $full_name); + $original_segment_count = count($name_parts); + + // If original name has exactly 2 segments, short name is NOT allowed + if ($original_segment_count === 2) { + return null; + } + + // If only 1 segment, no prefix to match + if ($original_segment_count === 1) { + return null; + } + + // Split directory path into parts and re-index + $dir_parts = array_values(array_filter(explode('/', $dir_path))); + + // Try all possible short name lengths (from longest to shortest, minimum 2 segments) + // For each potential short name, check if directory contains the complementary prefix parts + for ($short_len = $original_segment_count - 1; $short_len >= 2; $short_len--) { + $short_parts = array_slice($name_parts, $original_segment_count - $short_len); + $prefix_parts = array_slice($name_parts, 0, $original_segment_count - $short_len); + + // Check if prefix_parts exist as a contiguous sequence in directory + $prefix_len = count($prefix_parts); + for ($start_idx = 0; $start_idx <= count($dir_parts) - $prefix_len; $start_idx++) { + $all_match = true; + for ($i = 0; $i < $prefix_len; $i++) { + if (strtolower($dir_parts[$start_idx + $i]) !== strtolower($prefix_parts[$i])) { + $all_match = false; + break; + } + } + + if ($all_match) { + // Found valid prefix in directory, this short name is valid + return implode('_', $short_parts); + } + } + } + + return null; // No valid short name found + } + + /** + * Check if a suggested short filename would be valid for a given class name + * Uses the same validation logic as MANIFEST-FILENAME-01 + */ + private function is_valid_short_filename(string $suggested_filename_base, string $class_name, string $dir_path, bool $is_rspade): bool + { + // Full name always valid + $matches_full = $is_rspade + ? $suggested_filename_base === $class_name + : strtolower($suggested_filename_base) === strtolower($class_name); + + if ($matches_full) { + return true; + } + + // Check if short name is valid + $valid_short_name = $this->extract_valid_short_name($class_name, $dir_path); + + if ($valid_short_name !== null) { + $matches_short = $is_rspade + ? $suggested_filename_base === $valid_short_name + : strtolower($suggested_filename_base) === strtolower($valid_short_name); + + if ($matches_short) { + return true; + } + } + + return false; + } + /** * Get identifier type description for messages */