Add kebab-case detection to JQHTML redundant class rule
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
* <Define:Breadcrumb_Nav class="Breadcrumb_Nav"> // Exact match
|
||||
* <Define:Breadcrumb_Nav class="breadcrumb-nav"> // 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('/<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);
|
||||
|
||||
// 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" .
|
||||
" <Define:{$component_name} class=\"{$class_attribute}\">\n\n" .
|
||||
"CORRECTED:\n" .
|
||||
" <Define:{$component_name} class=\"" . $this->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[] = " <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);
|
||||
|
||||
// Trim leading/trailing spaces
|
||||
return trim($cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
--%>
|
||||
<Define:JS_Tree_Debug_Node tag="div" class="js-tree-debug-node">
|
||||
<Define:JS_Tree_Debug_Node tag="div">
|
||||
<%
|
||||
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);
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
|
||||
.js-tree-debug-node {
|
||||
.js-tree-debug-node,
|
||||
.JS_Tree_Debug_Node {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user