feat: add FE-003 to FE-007 macro validation rules

New architecture validation rules for Jinja macros:

- FE-003: Inline loading/error states → use alerts.html macro
  Detects: x-show="loading" with py-12, bg-red-100 error boxes

- FE-004: Inline modals → use modals.html macro
  Detects: fixed inset-0 z-50 with role="dialog" or backdrop

- FE-005: Inline table wrappers → use tables.html macro
  Detects: overflow-hidden rounded-lg shadow-xs with <table>

- FE-006: Inline dropdowns → use dropdowns.html macro
  Detects: @click.outside with absolute positioning menu

- FE-007: Inline page headers → use headers.html macro
  Detects: flex justify-between with h2 text-2xl

All rules support noqa comments (e.g., {# noqa: FE-003 #})

Current violations found: 62 (all warnings)

🤖 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 20:24:51 +01:00
parent 74e8620fc7
commit 4c5c851e3f

View File

@@ -511,6 +511,173 @@ class ArchitectureValidator:
suggestion='<span x-html="$icon(\'icon-name\', \'w-4 h-4\')"></span>',
)
def _check_alerts_macro_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-003: Check for inline loading/error states that should use alerts macro"""
# Check if already using the alerts macro
uses_macro = any("from 'shared/macros/alerts.html'" in line for line in lines)
if uses_macro:
return
# Check for noqa comment
has_noqa = any("noqa: fe-003" in line.lower() for line in lines)
if has_noqa:
return
# Look for inline loading states
for i, line in enumerate(lines, 1):
# Loading state pattern: text-center py-12 with loading content
if 'x-show="loading"' in line or "x-show='loading'" in line:
# Check if next few lines have spinner pattern
context_lines = "\n".join(lines[i-1:i+3])
if "text-center" in context_lines and "py-12" in context_lines:
self._add_violation(
rule_id="FE-003",
rule_name="Use alerts macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Inline loading state found - use loading_state macro",
context=line.strip()[:60],
suggestion="{% from 'shared/macros/alerts.html' import loading_state %}\n{{ loading_state('Loading...') }}",
)
return # Only report once per file
# Error state pattern: bg-red-100 border-red-400
if "bg-red-100" in line and "border-red-400" in line:
self._add_violation(
rule_id="FE-003",
rule_name="Use alerts macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Inline error state found - use error_state macro",
context=line.strip()[:60],
suggestion="{% from 'shared/macros/alerts.html' import error_state %}\n{{ error_state('Error', 'error') }}",
)
return
def _check_modals_macro_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-004: Check for inline modals that should use modals macro"""
# Check if already using the modals macro
uses_macro = any("from 'shared/macros/modals.html'" in line for line in lines)
if uses_macro:
return
# Check for noqa comment
has_noqa = any("noqa: fe-004" in line.lower() for line in lines)
if has_noqa:
return
# Look for modal patterns: fixed inset-0 with role="dialog" or modal backdrop
for i, line in enumerate(lines, 1):
if "fixed inset-0" in line and ("z-50" in line or "z-30" in line or "z-40" in line):
# Check context for modal indicators
context_lines = "\n".join(lines[max(0, i-1):min(len(lines), i+5)])
if 'role="dialog"' in context_lines or "bg-opacity-50" in context_lines or "bg-black/50" in context_lines:
self._add_violation(
rule_id="FE-004",
rule_name="Use modals macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Inline modal found - use modal macro for consistency",
context=line.strip()[:60],
suggestion="{% from 'shared/macros/modals.html' import modal %}\n{% call modal('myModal', 'Title', 'isModalOpen') %}...{% endcall %}",
)
return
def _check_tables_macro_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-005: Check for inline table wrappers that should use tables macro"""
# Check if already using the tables macro
uses_macro = any("from 'shared/macros/tables.html'" in line for line in lines)
if uses_macro:
return
# Check for noqa comment
has_noqa = any("noqa: fe-005" in line.lower() for line in lines)
if has_noqa:
return
# Look for table wrapper pattern: overflow-hidden rounded-lg shadow-xs
for i, line in enumerate(lines, 1):
if "overflow-hidden" in line and "rounded-lg" in line and "shadow-xs" in line:
# Check if there's a table nearby
context_lines = "\n".join(lines[i-1:min(len(lines), i+10)])
if "<table" in context_lines:
self._add_violation(
rule_id="FE-005",
rule_name="Use tables macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Inline table wrapper found - use table_wrapper macro",
context=line.strip()[:60],
suggestion="{% from 'shared/macros/tables.html' import table_wrapper, table_header %}\n{% call table_wrapper() %}...{% endcall %}",
)
return
def _check_dropdowns_macro_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-006: Check for inline dropdowns that should use dropdowns macro"""
# Check if already using the dropdowns macro
uses_macro = any("from 'shared/macros/dropdowns.html'" in line for line in lines)
if uses_macro:
return
# Check for noqa comment
has_noqa = any("noqa: fe-006" in line.lower() for line in lines)
if has_noqa:
return
# Look for dropdown patterns: @click.outside or @click.away with menu positioning
for i, line in enumerate(lines, 1):
if ("@click.outside=" in line or "@click.away=" in line) and "false" in line:
# Check context for dropdown menu indicators
context_lines = "\n".join(lines[max(0, i-3):min(len(lines), i+10)])
# Look for dropdown menu styling
if "absolute" in context_lines and ("right-0" in context_lines or "left-0" in context_lines):
if "py-2" in context_lines or "py-1" in context_lines:
self._add_violation(
rule_id="FE-006",
rule_name="Use dropdowns macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Inline dropdown menu found - use dropdown macro",
context=line.strip()[:60],
suggestion="{% from 'shared/macros/dropdowns.html' import dropdown, dropdown_item %}\n{% call dropdown('Label', 'isOpen') %}...{% endcall %}",
)
return
def _check_headers_macro_usage(self, file_path: Path, content: str, lines: list[str]):
"""FE-007: Check for inline page headers that should use headers macro"""
# Check if already using the headers macro
uses_macro = any("from 'shared/macros/headers.html'" in line for line in lines)
if uses_macro:
return
# Check for noqa comment
has_noqa = any("noqa: fe-007" in line.lower() for line in lines)
if has_noqa:
return
# Look for page header pattern: flex with h2 text-2xl font-semibold
for i, line in enumerate(lines, 1):
if '<h2' in line and 'text-2xl' in line and 'font-semibold' in line:
# Check context for typical page header pattern
context_before = "\n".join(lines[max(0, i-3):i])
if "flex" in context_before and ("justify-between" in context_before or "items-center" in context_before):
self._add_violation(
rule_id="FE-007",
rule_name="Use headers macro",
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Inline page header found - use page_header macro",
context=line.strip()[:60],
suggestion="{% from 'shared/macros/headers.html' import page_header %}\n{{ page_header('Title', action_label='Create', action_url='/create') }}",
)
return
def _validate_api_endpoints(self, target_path: Path):
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
print("📡 Validating API endpoints...")
@@ -1038,6 +1205,26 @@ class ArchitectureValidator:
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_icon_helper_usage(file_path, content, lines)
# FE-003: Check for inline loading/error states (should use alerts macro)
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_alerts_macro_usage(file_path, content, lines)
# FE-004: Check for inline modals (should use modals macro)
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_modals_macro_usage(file_path, content, lines)
# FE-005: Check for inline table wrappers (should use tables macro)
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_tables_macro_usage(file_path, content, lines)
# FE-006: Check for inline dropdowns (should use dropdowns macro)
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_dropdowns_macro_usage(file_path, content, lines)
# FE-007: Check for inline page headers (should use headers macro)
if not is_base_or_partial and not is_macro and not is_components_page:
self._check_headers_macro_usage(file_path, content, lines)
# Skip base/partials for TPL-001 check
if is_base_or_partial:
continue