From 84136be7442c74e93597e0e58dfbb7b4d1327124 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 11 Dec 2025 21:16:29 +0000 Subject: [PATCH] Add kebab-case detection to JQHTML redundant class rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../JqhtmlRedundantClass_CodeQualityRule.php | 194 +++++++++++++++--- .../Core/Debug/JS_Tree_Debug_Node.jqhtml | 2 +- .../Core/Debug/js_tree_debug_component.scss | 3 +- 3 files changed, 168 insertions(+), 31 deletions(-) diff --git a/app/RSpade/CodeQuality/Rules/Jqhtml/JqhtmlRedundantClass_CodeQualityRule.php b/app/RSpade/CodeQuality/Rules/Jqhtml/JqhtmlRedundantClass_CodeQualityRule.php index 7b8ac3a33..65222eaf6 100755 --- a/app/RSpade/CodeQuality/Rules/Jqhtml/JqhtmlRedundantClass_CodeQualityRule.php +++ b/app/RSpade/CodeQuality/Rules/Jqhtml/JqhtmlRedundantClass_CodeQualityRule.php @@ -7,9 +7,13 @@ use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract; /** * JQHTML Redundant Class Rule * - * Detects when a component's class name 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 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: + * // Exact match + * // Kebab-case equivalent */ class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract { @@ -34,7 +38,7 @@ class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract */ public function get_description(): string { - return 'Detects when component class name is redundantly specified in class attribute'; + return 'Detects when component class name (or kebab-case equivalent) is redundantly specified in class attribute'; } /** @@ -45,6 +49,14 @@ class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract 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 */ @@ -69,43 +81,167 @@ class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract if (preg_match('/]*class=["\']([^"\']*)["\']/', $line, $matches)) { $component_name = $matches[1]; $class_attribute = $matches[2]; + $kebab_case = $this->to_kebab_case($component_name); - // Check if component name appears in the class attribute - // Use word boundary to avoid false positives (e.g., "Client" in "Client_Selector") - if (preg_match('/\b' . preg_quote($component_name, '/') . '\b/', $class_attribute)) { - $this->add_violation( - $file_path, - $line_number, - "Redundant class name '{$component_name}' in Define tag class attribute", - trim($line), - "Remove '{$component_name}' from the class attribute. Component names are automatically added to the rendered element's class list by the jqhtml framework. Explicitly including the component name in the class attribute is unnecessary.\n\n" . - "CURRENT:\n" . - " \n\n" . - "CORRECTED:\n" . - " remove_component_from_class($class_attribute, $component_name) . "\">", - 'medium' - ); + // 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' + ); + } } } } } /** - * Remove component name from class attribute string + * Convert PascalCase_With_Underscores to kebab-case * - * @param string $class_attribute The class attribute value - * @param string $component_name The component name to remove - * @return string The cleaned class attribute + * My_Component -> my-component + * Breadcrumb_Nav -> breadcrumb-nav */ - private function remove_component_from_class(string $class_attribute, string $component_name): string + private function to_kebab_case(string $component_name): string { - // Remove the component name, handling multiple spaces - $cleaned = preg_replace('/\b' . preg_quote($component_name, '/') . '\b\s*/', '', $class_attribute); + return strtolower(str_replace('_', '-', $component_name)); + } - // Clean up any double spaces + /** + * 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); - - // Trim leading/trailing spaces return trim($cleaned); } } diff --git a/app/RSpade/Core/Debug/JS_Tree_Debug_Node.jqhtml b/app/RSpade/Core/Debug/JS_Tree_Debug_Node.jqhtml index ff51fbcf2..afdc997e3 100755 --- a/app/RSpade/Core/Debug/JS_Tree_Debug_Node.jqhtml +++ b/app/RSpade/Core/Debug/JS_Tree_Debug_Node.jqhtml @@ -9,7 +9,7 @@ $expand_depth - How many levels deep to expand $label - Optional key/index label for this node $show_class_names - If true, display class names for named object instances --%> - + <% const class_name = this.args.show_class_names ? JS_Tree_Debug_Node.get_class_name(this.args.data) : null; const relationships = JS_Tree_Debug_Node.get_object_relationships(this.args.data); diff --git a/app/RSpade/Core/Debug/js_tree_debug_component.scss b/app/RSpade/Core/Debug/js_tree_debug_component.scss index b7895965a..f3266a21d 100755 --- a/app/RSpade/Core/Debug/js_tree_debug_component.scss +++ b/app/RSpade/Core/Debug/js_tree_debug_component.scss @@ -13,7 +13,8 @@ border-radius: 4px; background: #fafafa; - .js-tree-debug-node { + .js-tree-debug-node, + .JS_Tree_Debug_Node { margin-left: 0; }