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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user