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
|
* JQHTML Redundant Class Rule
|
||||||
*
|
*
|
||||||
* Detects when a component's class name is redundantly specified in the class
|
* Detects when a component's class name (or kebab-case equivalent) is redundantly
|
||||||
* attribute of the Define tag. Component names are automatically added to the
|
* specified in the class attribute of the Define tag. Component names are automatically
|
||||||
* rendered element's class list, so explicitly including them is unnecessary.
|
* 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
|
class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract
|
||||||
{
|
{
|
||||||
@@ -34,7 +38,7 @@ class JqhtmlRedundantClass_CodeQualityRule extends CodeQualityRule_Abstract
|
|||||||
*/
|
*/
|
||||||
public function get_description(): string
|
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'];
|
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
|
* 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)) {
|
if (preg_match('/<Define:([a-zA-Z_][a-zA-Z0-9_]*)\s+[^>]*class=["\']([^"\']*)["\']/', $line, $matches)) {
|
||||||
$component_name = $matches[1];
|
$component_name = $matches[1];
|
||||||
$class_attribute = $matches[2];
|
$class_attribute = $matches[2];
|
||||||
|
$kebab_case = $this->to_kebab_case($component_name);
|
||||||
|
|
||||||
// Check if component name appears in the class attribute
|
// Split class attribute into individual classes
|
||||||
// Use word boundary to avoid false positives (e.g., "Client" in "Client_Selector")
|
$classes = preg_split('/\s+/', trim($class_attribute));
|
||||||
if (preg_match('/\b' . preg_quote($component_name, '/') . '\b/', $class_attribute)) {
|
|
||||||
$this->add_violation(
|
foreach ($classes as $class) {
|
||||||
$file_path,
|
$class = trim($class);
|
||||||
$line_number,
|
if (empty($class)) {
|
||||||
"Redundant class name '{$component_name}' in Define tag class attribute",
|
continue;
|
||||||
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" .
|
// Check for exact match with component name
|
||||||
" <Define:{$component_name} class=\"{$class_attribute}\">\n\n" .
|
if ($class === $component_name) {
|
||||||
"CORRECTED:\n" .
|
$this->add_violation(
|
||||||
" <Define:{$component_name} class=\"" . $this->remove_component_from_class($class_attribute, $component_name) . "\">",
|
$file_path,
|
||||||
'medium'
|
$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
|
* My_Component -> my-component
|
||||||
* @param string $component_name The component name to remove
|
* Breadcrumb_Nav -> breadcrumb-nav
|
||||||
* @return string The cleaned class attribute
|
|
||||||
*/
|
*/
|
||||||
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
|
return strtolower(str_replace('_', '-', $component_name));
|
||||||
$cleaned = preg_replace('/\b' . preg_quote($component_name, '/') . '\b\s*/', '', $class_attribute);
|
}
|
||||||
|
|
||||||
// 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);
|
$cleaned = preg_replace('/\s+/', ' ', $cleaned);
|
||||||
|
|
||||||
// Trim leading/trailing spaces
|
|
||||||
return trim($cleaned);
|
return trim($cleaned);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ $expand_depth - How many levels deep to expand
|
|||||||
$label - Optional key/index label for this node
|
$label - Optional key/index label for this node
|
||||||
$show_class_names - If true, display class names for named object instances
|
$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 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);
|
const relationships = JS_Tree_Debug_Node.get_object_relationships(this.args.data);
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
|
|
||||||
.js-tree-debug-node {
|
.js-tree-debug-node,
|
||||||
|
.JS_Tree_Debug_Node {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user