Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 = [...];
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user