Files
rspade_system/app/RSpade/CodeQuality/Rules/Jqhtml/JqhtmlRedundantClass_CodeQualityRule.php
2025-12-11 21:16:29 +00:00

248 lines
8.5 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\Jqhtml;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* JQHTML Redundant Class Rule
*
* Detects when a component's class name (or kebab-case equivalent) is redundantly
* specified in the class attribute of the Define tag. Component names are automatically
* added to the rendered element's class list, so explicitly including them is unnecessary.
*
* Detects both:
* <Define:Breadcrumb_Nav class="Breadcrumb_Nav"> // Exact match
* <Define:Breadcrumb_Nav class="breadcrumb-nav"> // Kebab-case equivalent
*/
class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique identifier for this rule
*/
public function get_id(): string
{
return 'JQHTML-CLASS-01';
}
/**
* Get the human-readable name of this rule
*/
public function get_name(): string
{
return 'JQHTML Redundant Component Class Name';
}
/**
* Get the description of what this rule checks
*/
public function get_description(): string
{
return 'Detects when component class name (or kebab-case equivalent) is redundantly specified in class attribute';
}
/**
* Get file patterns this rule should check
*/
public function get_file_patterns(): array
{
return ['*.jqhtml'];
}
/**
* This rule runs during manifest scan for immediate feedback
*/
public function is_called_during_manifest_scan(): bool
{
return true;
}
/**
* Get the default severity level
*/
public function get_default_severity(): string
{
return 'medium';
}
/**
* Check the file for violations
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
$lines = explode("\n", $contents);
$line_number = 0;
foreach ($lines as $line) {
$line_number++;
// Look for <Define:ComponentName ... class="..."> patterns
// Match Define tag with component name and class attribute on same line
if (preg_match('/<Define:([a-zA-Z_][a-zA-Z0-9_]*)\s+[^>]*class=["\']([^"\']*)["\']/', $line, $matches)) {
$component_name = $matches[1];
$class_attribute = $matches[2];
$kebab_case = $this->to_kebab_case($component_name);
// Split class attribute into individual classes
$classes = preg_split('/\s+/', trim($class_attribute));
foreach ($classes as $class) {
$class = trim($class);
if (empty($class)) {
continue;
}
// Check for exact match with component name
if ($class === $component_name) {
$this->add_violation(
$file_path,
$line_number,
"Redundant class=\"{$component_name}\" on component {$component_name}. " .
"The component automatically gets class=\"{$component_name}\" assigned to it.",
trim($line),
$this->build_exact_match_suggestion($component_name, $class_attribute),
'medium'
);
}
// Check for kebab-case equivalent (case-insensitive)
// Only flag if component name has underscores (multi-word), since single-word
// lowercase classes like "card" could be legitimate external CSS framework classes
// Also skip known CSS framework class prefixes (Bootstrap, etc.)
elseif (strpos($component_name, '_') !== false
&& strtolower($class) === $kebab_case
&& !$this->is_known_framework_class($class)) {
$this->add_violation(
$file_path,
$line_number,
"Unnecessary class=\"{$class}\" on component {$component_name}. " .
"The component automatically gets class=\"{$component_name}\" assigned to it. " .
"Use .{$component_name} in CSS/JS selectors instead of .{$class}",
trim($line),
$this->build_kebab_case_suggestion($component_name, $class),
'medium'
);
}
}
}
}
}
/**
* Convert PascalCase_With_Underscores to kebab-case
*
* My_Component -> my-component
* Breadcrumb_Nav -> breadcrumb-nav
*/
private function to_kebab_case(string $component_name): string
{
return strtolower(str_replace('_', '-', $component_name));
}
/**
* Check if a class name is a known CSS framework class
*
* These are legitimate uses of kebab-case classes from external frameworks
* like Bootstrap that happen to match component names.
*/
private function is_known_framework_class(string $class): bool
{
// Bootstrap class prefixes that are commonly used
$bootstrap_prefixes = [
'card-',
'btn-',
'nav-',
'navbar-',
'form-',
'input-',
'list-',
'table-',
'modal-',
'alert-',
'badge-',
'dropdown-',
'breadcrumb-',
'pagination-',
'progress-',
'spinner-',
'toast-',
'tooltip-',
'popover-',
'carousel-',
'accordion-',
'offcanvas-',
'placeholder-',
];
foreach ($bootstrap_prefixes as $prefix) {
if (strpos($class, $prefix) === 0) {
return true;
}
}
return false;
}
/**
* Build suggestion for exact component name match
*/
private function build_exact_match_suggestion(string $component_name, string $class_attribute): string
{
$cleaned = $this->remove_class_from_attribute($class_attribute, $component_name);
$lines = [];
$lines[] = "Remove '{$component_name}' from the class attribute. Component names are";
$lines[] = "automatically added to the rendered element's class list by the jqhtml framework.";
$lines[] = "";
$lines[] = "CURRENT:";
$lines[] = " <Define:{$component_name} class=\"{$class_attribute}\">";
$lines[] = "";
if (empty($cleaned)) {
$lines[] = "CORRECTED:";
$lines[] = " <Define:{$component_name}>";
} else {
$lines[] = "CORRECTED:";
$lines[] = " <Define:{$component_name} class=\"{$cleaned}\">";
}
return implode("\n", $lines);
}
/**
* Build suggestion for kebab-case equivalent
*/
private function build_kebab_case_suggestion(string $component_name, string $redundant_class): string
{
$lines = [];
$lines[] = "The class \"{$redundant_class}\" is redundant because jqhtml components";
$lines[] = "automatically receive their component name as a CSS class.";
$lines[] = "";
$lines[] = "When you write:";
$lines[] = " <Define:{$component_name} class=\"{$redundant_class}\">";
$lines[] = "";
$lines[] = "The rendered element becomes:";
$lines[] = " <div class=\"{$component_name} {$redundant_class}\">";
$lines[] = "";
$lines[] = "TO FIX:";
$lines[] = " 1. Remove the redundant class from the Define tag:";
$lines[] = " <Define:{$component_name}>";
$lines[] = "";
$lines[] = " 2. Update any CSS/SCSS selectors to use the component class:";
$lines[] = " .{$component_name} { ... } // Instead of .{$redundant_class}";
$lines[] = "";
$lines[] = " 3. Update any JavaScript selectors:";
$lines[] = " \$('.{$component_name}') // Instead of \$('.{$redundant_class}')";
return implode("\n", $lines);
}
/**
* Remove a class from the class attribute string
*/
private function remove_class_from_attribute(string $class_attribute, string $class_to_remove): string
{
$cleaned = preg_replace('/\b' . preg_quote($class_to_remove, '/') . '\b\s*/', '', $class_attribute);
$cleaned = preg_replace('/\s+/', ' ', $cleaned);
return trim($cleaned);
}
}