// Exact match * // 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 patterns // Match Define tag with component name and class attribute on same line if (preg_match('/]*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[] = " "; $lines[] = ""; if (empty($cleaned)) { $lines[] = "CORRECTED:"; $lines[] = " "; } else { $lines[] = "CORRECTED:"; $lines[] = " "; } 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[] = " "; $lines[] = ""; $lines[] = "The rendered element becomes:"; $lines[] = "
"; $lines[] = ""; $lines[] = "TO FIX:"; $lines[] = " 1. Remove the redundant class from the Define tag:"; $lines[] = " "; $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); } }