Framework updates

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-07 08:20:05 +00:00
parent 7eedf0fc63
commit c4a338fe7c
2 changed files with 304 additions and 0 deletions

View File

@@ -111,6 +111,73 @@ class Constants
'DATETIME' => 'M j, Y g:i A' '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: // Additional constants can be added here as needed:
// const MIME_TYPES = [...]; // const MIME_TYPES = [...];
// const COUNTRY_CODES = [...]; // const COUNTRY_CODES = [...];

View File

@@ -2,6 +2,7 @@
namespace App\RSpade\CodeQuality\Rules\Convention; namespace App\RSpade\CodeQuality\Rules\Convention;
use App\Constants;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract; 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 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); $filename_base = $this->get_filename_base($filename, $extension);
$identifier_parts = explode('_', $identifier); $identifier_parts = explode('_', $identifier);
@@ -180,6 +187,13 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract
return; // Can't suggest - already taken 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 // Add violation
$identifier_type = $this->get_identifier_type($extension); $identifier_type = $this->get_identifier_type($extension);
$this->add_violation( $this->add_violation(
@@ -273,6 +287,12 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract
$filename_base = $this->get_filename_base($filename, $extension); $filename_base = $this->get_filename_base($filename, $extension);
$relative_path = str_replace(base_path() . '/', '', $file_path); $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 // Create a single-file "group" and check it
$file_info = [ $file_info = [
'file_path' => $file_path, '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); $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) * 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); $short_name_lower = strtolower($short_name);
$immediate_dir_lower = strtolower($immediate_dir); $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)) { if (!str_contains($short_name_lower, $immediate_dir_lower)) {
return false; // Would lose essential directory context - abort group rename 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 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[] = [ $rename_suggestions[] = [
'old' => $file_info['filename'], 'old' => $file_info['filename'],
'new' => $short_filename, 'new' => $short_filename,
@@ -584,6 +710,15 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract
$short_name_lower = strtolower($short_name); $short_name_lower = strtolower($short_name);
$immediate_dir_lower = strtolower($immediate_dir); $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)) { if (!str_contains($short_name_lower, $immediate_dir_lower)) {
return false; // Would lose essential directory context - abort group rename 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 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[] = [ $rename_suggestions[] = [
'old' => $file_info['filename'], 'old' => $file_info['filename'],
'new' => $short_filename, 'new' => $short_filename,
@@ -702,6 +851,94 @@ class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract
return pathinfo($filename, PATHINFO_FILENAME); 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 * Get identifier type description for messages
*/ */