Files
rspade_system/app/RSpade/CodeQuality/Rules/Convention/FilenameRedundantPrefix_CodeQualityRule.php
root f6fac6c4bc 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>
2025-10-21 02:08:33 +00:00

260 lines
9.8 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\Convention;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* FilenameRedundantPrefix_CodeQualityRule - Detects unnecessarily long filenames
*
* Suggests using short filenames when the directory structure already contains the prefix.
*/
class FilenameRedundantPrefix_CodeQualityRule extends CodeQualityRule_Abstract
{
public function get_id(): string
{
return 'CONV-FILENAME-01';
}
public function get_name(): string
{
return 'Filename Redundant Prefix Convention';
}
public function get_description(): string
{
return 'Suggests using short filenames when directory structure contains the prefix';
}
public function get_file_patterns(): array
{
return ['*.php', '*.js', '*.jqhtml', '*.blade.php'];
}
public function get_default_severity(): string
{
return 'convention';
}
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Only check files in ./rsx or ./app/RSpade
$relative_path = str_replace(base_path() . '/', '', $file_path);
$is_rsx = str_starts_with($relative_path, 'rsx/');
$is_rspade = str_starts_with($relative_path, 'app/RSpade/');
if (!$is_rsx && !$is_rspade) {
return;
}
$extension = $metadata['extension'] ?? '';
$filename = basename($file_path);
// Check PHP/JS files with classes
if (isset($metadata['class'])) {
$this->check_class_redundancy($relative_path, $metadata['class'], $extension, $filename, $is_rspade);
}
// Check blade.php files with @rsx_id
if ($extension === 'blade.php' && isset($metadata['id'])) {
$this->check_blade_redundancy($relative_path, $metadata['id'], $filename, $is_rspade);
}
// Check jqhtml files with Define:
if ($extension === 'jqhtml' && isset($metadata['id'])) {
$this->check_jqhtml_redundancy($relative_path, $metadata['id'], $filename, $is_rspade);
}
}
private function check_class_redundancy(string $file, string $class_name, string $extension, string $filename, bool $is_rspade): void
{
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
$dir_path = dirname($file);
$short_name = $this->extract_short_name($class_name, $dir_path);
if ($short_name === null) {
return; // No short name available
}
// Check if current filename is the full name (redundant)
$is_full_name = $is_rspade
? $filename_without_ext === $class_name
: strtolower($filename_without_ext) === strtolower($class_name);
if (!$is_full_name) {
return; // Not using full name
}
// Check if short filename would be available
$short_filename = $is_rspade ? $short_name . '.' . $extension : strtolower($short_name) . '.' . $extension;
$short_path = dirname(base_path($file)) . '/' . $short_filename;
if (file_exists($short_path)) {
return; // Short name already taken
}
$this->add_violation(
$file,
1,
"Filename contains redundant prefix already represented in directory structure",
"class $class_name",
"Directory structure already contains the class name prefix.\n" .
"Consider using the shorter filename: '$short_filename'\n" .
" mv '$filename' '$short_filename'\n\n" .
"IMPORTANT: Only the filename should be changed. The class name must remain unchanged.\n" .
"The directory structure provides the necessary context for the shorter filename.\n" .
"Example: rsx/app/demo/demo_controller.php → rsx/app/demo/controller.php\n" .
" (but class Demo_Controller remains Demo_Controller)\n\n" .
"This improves readability while maintaining uniqueness.",
'convention'
);
}
private function check_blade_redundancy(string $file, string $rsx_id, string $filename, bool $is_rspade): void
{
$filename_without_blade = str_replace('.blade.php', '', $filename);
$dir_path = dirname($file);
$short_name = $this->extract_short_name($rsx_id, $dir_path);
if ($short_name === null) {
return;
}
$is_full_name = $is_rspade
? $filename_without_blade === $rsx_id
: strtolower($filename_without_blade) === strtolower($rsx_id);
if (!$is_full_name) {
return;
}
$short_filename = $is_rspade ? $short_name . '.blade.php' : strtolower($short_name) . '.blade.php';
$short_path = dirname(base_path($file)) . '/' . $short_filename;
if (file_exists($short_path)) {
return;
}
$this->add_violation(
$file,
1,
"Blade filename contains redundant prefix already represented in directory structure",
"@rsx_id('$rsx_id')",
"Directory structure already contains the @rsx_id prefix.\n" .
"Consider using the shorter filename: '$short_filename'\n" .
" mv '$filename' '$short_filename'\n\n" .
"IMPORTANT: Only the filename should be changed. The @rsx_id must remain unchanged.\n" .
"The directory structure provides the necessary context for the shorter filename.\n" .
"Example: rsx/app/demo/sections/demo_sections_cards.blade.php → cards.blade.php\n" .
" (but @rsx_id('demo.sections.cards') remains demo.sections.cards)\n\n" .
"This improves readability while maintaining uniqueness.",
'convention'
);
}
private function check_jqhtml_redundancy(string $file, string $component_name, string $filename, bool $is_rspade): void
{
$filename_without_ext = pathinfo($filename, PATHINFO_FILENAME);
$dir_path = dirname($file);
$short_name = $this->extract_short_name($component_name, $dir_path);
if ($short_name === null) {
return;
}
$is_full_name = $is_rspade
? $filename_without_ext === $component_name
: strtolower($filename_without_ext) === strtolower($component_name);
if (!$is_full_name) {
return;
}
$short_filename = $is_rspade ? $short_name . '.jqhtml' : strtolower($short_name) . '.jqhtml';
$short_path = dirname(base_path($file)) . '/' . $short_filename;
if (file_exists($short_path)) {
return;
}
$this->add_violation(
$file,
1,
"Jqhtml filename contains redundant prefix already represented in directory structure",
"<Define:$component_name>",
"Directory structure already contains the component name prefix.\n" .
"Consider using the shorter filename: '$short_filename'\n" .
" mv '$filename' '$short_filename'\n\n" .
"IMPORTANT: Only the filename should be changed. The component name must remain unchanged.\n" .
"The directory structure provides the necessary context for the shorter filename.\n" .
"Example: rsx/app/demo/components/demo_card.jqhtml → card.jqhtml\n" .
" (but <Define:Demo_Card> remains Demo_Card)\n\n" .
"This improves readability while maintaining uniqueness.",
'convention'
);
}
/**
* Extract short name from class/id if directory structure matches prefix
* Returns null if directory structure doesn't match or if short name rules aren't met
*
* Rules:
* - Original name must have 3+ segments for short name to be allowed (2-segment names must use full name)
* - Short name must have 2+ segments (exception: if original was 1 segment, short can be 1 segment)
*/
private function extract_short_name(string $full_name, string $dir_path): ?string
{
$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)));
// Find the maximum number of consecutive matches between end of dir_parts and start of name_parts
$matched_parts = 0;
$max_possible = min(count($dir_parts), count($name_parts) - 1);
// Try to match last N dir parts with first N name parts
for ($num_to_check = $max_possible; $num_to_check > 0; $num_to_check--) {
$all_match = true;
for ($i = 0; $i < $num_to_check; $i++) {
$dir_idx = count($dir_parts) - $num_to_check + $i;
if (strtolower($dir_parts[$dir_idx]) !== strtolower($name_parts[$i])) {
$all_match = false;
break;
}
}
if ($all_match) {
$matched_parts = $num_to_check;
break;
}
}
if ($matched_parts === 0) {
return null;
}
// Calculate the short name
$short_parts = array_slice($name_parts, $matched_parts);
$short_segment_count = count($short_parts);
// Validate short name segment count
// Short name must have 2+ segments (unless original was 1 segment, which we already excluded above)
if ($short_segment_count < 2) {
return null; // Short name would be too short
}
return implode('_', $short_parts);
}
}