Files
rspade_system/app/RSpade/CodeQuality/Rules/Common/AbstractClassNaming_CodeQualityRule.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

245 lines
8.4 KiB
PHP

<?php
namespace App\RSpade\CodeQuality\Rules\Common;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
class AbstractClassNaming_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'ABSTRACT-CLASS-01';
}
public function get_name(): string
{
return 'Abstract Class Naming Convention';
}
public function get_description(): string
{
return 'Ensures abstract classes follow RSX naming conventions with _Abstract suffix';
}
public function get_file_patterns(): array
{
return ['*.php', '*.js'];
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Skip -temp files
if (str_contains($file_path, '-temp.')) {
return;
}
// Only check files in ./rsx and ./app/RSpade
$is_rsx = str_contains($file_path, '/rsx/');
$is_rspade = str_contains($file_path, '/app/RSpade/');
if (!$is_rsx && !$is_rspade) {
return;
}
// If in app/RSpade, check if it's in an allowed subdirectory
if ($is_rspade && !$this->is_in_allowed_rspade_directory($file_path)) {
return;
}
$filename = basename($file_path);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
// Handle PHP files
if ($extension === 'php') {
$this->check_php_abstract_class($file_path, $contents, $metadata);
}
// Handle JavaScript files
elseif ($extension === 'js') {
$this->check_js_abstract_class($file_path, $contents, $metadata);
}
}
private function check_php_abstract_class(string $file_path, string $contents, array $metadata): void
{
// Check if this is an abstract class
if (!preg_match('/^\s*abstract\s+class\s+(\w+)/m', $contents, $matches)) {
return; // Not an abstract class
}
$class_name = $matches[1];
$filename = basename($file_path);
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
// Check class name ends with _Abstract
if (!str_ends_with($class_name, '_Abstract')) {
$this->add_violation(
$file_path,
0,
"Abstract class '$class_name' must end with '_Abstract'",
"abstract class $class_name",
$this->get_abstract_class_remediation($class_name, $filename, true),
'high'
);
return; // Don't check filename if class name is wrong
}
// Check filename ends with _abstract.php or Abstract.php
if (!str_ends_with($filename, '_abstract.php') && !str_ends_with($filename, 'Abstract.php')) {
$this->add_violation(
$file_path,
0,
"Abstract class file '$filename' must end with '_abstract.php' or 'Abstract.php'",
"File: $filename",
$this->get_abstract_filename_remediation($class_name, $filename),
'medium'
);
}
}
private function check_js_abstract_class(string $file_path, string $contents, array $metadata): void
{
// Check for classes ending with _Abstract
if (!isset($metadata['class'])) {
return;
}
$class_name = $metadata['class'];
// Only check classes that end with _Abstract
if (!str_ends_with($class_name, '_Abstract')) {
return;
}
$filename = basename($file_path);
$extension = pathinfo($filename, PATHINFO_EXTENSION);
// Check filename ends with _abstract.js or Abstract.js
$valid_endings = ["_abstract.$extension", "Abstract.$extension"];
$valid = false;
foreach ($valid_endings as $ending) {
if (str_ends_with($filename, $ending)) {
$valid = true;
break;
}
}
if (!$valid) {
$this->add_violation(
$file_path,
0,
"Abstract class file '$filename' must end with '_abstract.$extension' or 'Abstract.$extension'",
"File: $filename",
$this->get_abstract_filename_remediation($class_name, $filename),
'medium'
);
}
}
private function get_abstract_class_remediation(string $class_name, string $filename, bool $is_php): string
{
// Determine suggested class name based on current naming
$suggested_class = $this->suggest_abstract_class_name($class_name);
$is_rsx = str_contains($filename, '_');
return "ABSTRACT CLASS NAMING CONVENTION
Abstract classes must follow RSX naming patterns:
- Class name must end with '_Abstract'
- Filename must end with '_abstract.php' or 'Abstract.php'
CURRENT CLASS: $class_name
SUGGESTED CLASS: $suggested_class
SUGGESTED FIXES:
1. Rename class from '$class_name' to '$suggested_class'
2. Rename file to match:
- For /rsx: Use lowercase convention (e.g., " . strtolower(str_replace('_', '_', $suggested_class)) . ".php)
- For /app/RSpade: Use exact match (e.g., $suggested_class.php)
3. Update all references to this class
WHY THIS MATTERS:
- Makes abstract classes immediately identifiable
- Enables framework introspection and auto-discovery
- Maintains consistent naming patterns across the codebase";
}
private function get_abstract_filename_remediation(string $class_name, string $filename): string
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
$lowercase_suggestion = strtolower(str_replace('_', '_', $class_name)) . ".$extension";
$exact_suggestion = $class_name . ".$extension";
return "ABSTRACT CLASS FILENAME CONVENTION
Abstract class files must end with '_abstract.$extension' or 'Abstract.$extension'
CURRENT FILE: $filename
CLASS NAME: $class_name
VALID FILENAME PATTERNS:
- Lowercase with underscore: *_abstract.$extension
- Uppercase suffix: *Abstract.$extension
SUGGESTED FILENAMES:
- For /rsx: $lowercase_suggestion
- For /app/RSpade: $exact_suggestion
Note: Both patterns are valid in either directory.";
}
private function suggest_abstract_class_name(string $current_name): string
{
// If class name contains 'Abstract' but not at the end
if (stripos($current_name, 'Abstract') !== false && !str_ends_with($current_name, '_Abstract') && !str_ends_with($current_name, 'Abstract')) {
// Remove 'Abstract' from wherever it is
$without_abstract = preg_replace('/Abstract/i', '', $current_name);
$without_abstract = trim($without_abstract, '_');
// If the class has underscores, add _Abstract
if (str_contains($without_abstract, '_')) {
return $without_abstract . '_Abstract';
} else {
// For non-underscore classes, add Abstract at the end
return $without_abstract . 'Abstract';
}
}
// If class doesn't contain 'Abstract' at all
if (str_contains($current_name, '_')) {
return $current_name . '_Abstract';
} else {
return $current_name . 'Abstract';
}
}
/**
* 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;
}
}