diff --git a/scripts/validate_architecture.py b/scripts/validate_architecture.py index e26db0a3..e3f0f3cd 100755 --- a/scripts/validate_architecture.py +++ b/scripts/validate_architecture.py @@ -1102,6 +1102,56 @@ class ArchitectureValidator: suggestion=f"Use '{{% block {invalid_blocks[block_name]} %}}' instead", ) + def _check_alpine_template_vars( + self, file_path: Path, content: str, lines: list[str], js_content: str + ): + """TPL-009: Check that Alpine variables used in templates are defined in JS""" + if "noqa: tpl-009" in content.lower(): + return + + import re + + # Common Alpine variable patterns that MUST be defined in the component + # These are variables that are directly referenced in templates + required_vars = set() + + # Check for error_state macro usage (requires 'error' var) + if "error_state(" in content and "error_var=" not in content: + required_vars.add("error") + + # Check for action_dropdown macro with custom vars + dropdown_pattern = re.compile( + r"action_dropdown\([^)]*open_var=['\"](\w+)['\"]" + ) + for match in dropdown_pattern.finditer(content): + required_vars.add(match.group(1)) + + dropdown_pattern2 = re.compile( + r"action_dropdown\([^)]*loading_var=['\"](\w+)['\"]" + ) + for match in dropdown_pattern2.finditer(content): + required_vars.add(match.group(1)) + + # Check if variables are defined in JS + for var in required_vars: + # Check if variable is defined in JS (look for "varname:" pattern) + var_pattern = re.compile(rf"\b{var}\s*:") + if not var_pattern.search(js_content): + # Find the line where it's used in template + for i, line in enumerate(lines, 1): + if var in line and ("error_state" in line or "action_dropdown" in line): + self._add_violation( + rule_id="TPL-009", + rule_name="Alpine variables must be defined in JS component", + severity=Severity.ERROR, + file_path=file_path, + line_number=i, + message=f"Template uses Alpine variable '{var}' but it's not defined in the JS component", + context=line.strip()[:80], + suggestion=f"Add '{var}: null,' or '{var}: false,' to the Alpine component's return object", + ) + break + def _check_pagination_macro_usage( self, file_path: Path, content: str, lines: list[str] ): @@ -2717,6 +2767,16 @@ class ArchitectureValidator: if not is_base_or_partial: self._check_valid_block_names(file_path, content, lines) + # TPL-009: Check Alpine variables are defined in JS + if not is_base_or_partial and not is_macro and not is_components_page: + # Try to find corresponding JS file + # Template: app/templates/admin/messages.html -> JS: static/admin/js/messages.js + template_name = file_path.stem # e.g., "messages" + js_file = target_path / f"static/admin/js/{template_name}.js" + if js_file.exists(): + js_content = js_file.read_text() + self._check_alpine_template_vars(file_path, content, lines, js_content) + # Skip base/partials for TPL-001 check if is_base_or_partial: continue