feat: add frontend architecture rules FE-001 to FE-004

Add rules to enforce consistent frontend component usage:

- FE-001 (warning): Use pagination macro instead of inline HTML
- FE-002 (warning): Use $icon() helper instead of inline SVGs
- FE-003 (info): Use table macros for consistent styling
- FE-004 (info): Use form macros for consistent styling

Update validate_architecture.py to check FE-001 and FE-002
anti-patterns in templates, with exceptions for macro definitions
and the components showcase page.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-06 18:35:17 +01:00
parent 64ab9031d2
commit 979ae93b17
2 changed files with 175 additions and 6 deletions

View File

@@ -298,6 +298,7 @@ class ArchitectureValidator:
self._check_no_http_exception_in_services(file_path, content, lines)
self._check_service_exceptions(file_path, content, lines)
self._check_db_session_parameter(file_path, content, lines)
self._check_no_commit_in_services(file_path, content, lines)
# Models
elif "/app/models/" in file_path_str or "\\app\\models\\" in file_path_str:
@@ -372,15 +373,35 @@ class ArchitectureValidator:
"""Validate a single HTML template file"""
print("📄 Validating template...")
# Skip base template and partials
if "base.html" in file_path.name or "partials" in str(file_path):
file_path_str = str(file_path)
# Skip base template and partials for extends check
is_base_or_partial = "base.html" in file_path.name or "partials" in file_path_str
# Skip macros directory for FE rules
is_macro = "shared/macros/" in file_path_str or "shared\\macros\\" in file_path_str
# Skip components showcase page
is_components_page = "components.html" in file_path.name
if is_base_or_partial:
print("⏭️ Skipping base/partial template")
elif is_macro:
print("⏭️ Skipping macro file")
else:
# FE-001: Check for inline pagination (should use macro)
if not is_components_page:
self._check_pagination_macro_usage(file_path, content, lines)
# FE-002: Check for inline SVGs (should use $icon())
if not is_components_page and not is_macro:
self._check_icon_helper_usage(file_path, content, lines)
# Only check admin templates for extends
if "/admin/" not in file_path_str and "\\admin\\" not in file_path_str:
return
# Only check admin templates
file_path_str = str(file_path)
if "/admin/" not in file_path_str and "\\admin\\" not in file_path_str:
print("⏭️ Not an admin template, skipping extends check")
if is_base_or_partial:
return
# Check for standalone marker in template (first 5 lines)
@@ -419,6 +440,67 @@ class ArchitectureValidator:
suggestion="Add {% extends 'admin/base.html' %} at the top, or add {# standalone #} if intentional",
)
def _check_pagination_macro_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-001: Check for inline pagination that should use macro"""
# Look for signs of inline pagination
pagination_indicators = [
('aria-label="Table navigation"', "Inline table navigation found"),
("previousPage()" , "Inline pagination controls found"),
("nextPage()" , "Inline pagination controls found"),
("goToPage(" , "Inline pagination controls found"),
]
# Check if already using the pagination macro
uses_macro = any("from 'shared/macros/pagination.html'" in line for line in lines)
if uses_macro:
return
for i, line in enumerate(lines, 1):
for pattern, message in pagination_indicators:
if pattern in line:
# Skip if it's in a comment
stripped = line.strip()
if stripped.startswith("{#") or stripped.startswith("<!--"):
continue
self._add_violation(
rule_id="FE-001",
rule_name="Use pagination macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message=message + " - use shared macro instead",
context=stripped[:60],
suggestion="{% from 'shared/macros/pagination.html' import pagination %}\n{{ pagination() }}",
)
return # Only report once per file
def _check_icon_helper_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-002: Check for inline SVGs that should use $icon() helper"""
# Pattern to find inline SVGs
svg_pattern = re.compile(r'<svg[^>]*viewBox[^>]*>.*?</svg>', re.DOTALL | re.IGNORECASE)
# Find all SVG occurrences
for match in svg_pattern.finditer(content):
# Find line number
line_num = content[:match.start()].count('\n') + 1
# Skip if this is likely in a code example (inside <pre> or <code>)
context_before = content[max(0, match.start()-200):match.start()]
if '<pre' in context_before or '<code' in context_before:
continue
self._add_violation(
rule_id="FE-002",
rule_name="Use $icon() helper",
severity=Severity.WARNING,
file_path=file_path,
line_number=line_num,
message="Inline SVG found - use $icon() helper for consistency",
context="<svg...>",
suggestion='<span x-html="$icon(\'icon-name\', \'w-4 h-4\')"></span>',
)
def _validate_api_endpoints(self, target_path: Path):
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
print("📡 Validating API endpoints...")
@@ -974,6 +1056,7 @@ class ArchitectureValidator:
"exception_rules",
"javascript_rules",
"template_rules",
"frontend_component_rules",
]:
rules = self.config.get(category, [])
for rule in rules: