"""Syntax validator for gitignore patterns.""" import re from dataclasses import dataclass from typing import List, Optional @dataclass class ValidationIssue: """Represents a validation issue.""" line_number: int line: str issue_type: str message: str severity: str class Validator: """Validate gitignore syntax and patterns.""" TRAILING_SLASH_PATTERN = re.compile(r".*[^/]//$") NEGATION_PATTERN = re.compile(r"^\s*!") BACKSLASH_PATTERN = re.compile(r"\\[nrt]") DOUBLE_STAR_PATTERN = re.compile(r"\*\*") UNESCAPED_EXCLAMATION = re.compile(r"[^\\]!") def __init__(self): """Initialize validator.""" self.issues: List[ValidationIssue] = [] self.warnings: List[ValidationIssue] = [] def validate_content(self, content: str) -> List[ValidationIssue]: """Validate gitignore content.""" self.issues = [] self.warnings = [] lines = content.splitlines() for line_num, line in enumerate(lines, start=1): self._validate_line(line, line_num, lines) return self.issues + self.warnings def _validate_line(self, line: str, line_num: int, all_lines: List[str]) -> None: """Validate a single line.""" stripped = line.strip() if not stripped or stripped.startswith("#"): return self._check_trailing_slash(stripped, line_num) self._check_double_negation(stripped, line_num, all_lines) self._check_backslash_escapes(stripped, line_num) self._check_invalid_wildcards(stripped, line_num) self._check_duplicate_pattern(stripped, line_num, all_lines) def _check_trailing_slash(self, line: str, line_num: int) -> None: """Check for trailing slashes (not allowed for directories).""" if self.TRAILING_SLASH_PATTERN.match(line): self.warnings.append(ValidationIssue( line_number=line_num, line=line, issue_type="trailing_slash", message="Trailing slash on directory pattern - consider removing it", severity="warning" )) def _check_double_negation(self, line: str, line_num: int, all_lines: List[str]) -> None: """Check for double negation patterns.""" if line.startswith("!"): prev_lines = [prev_line.strip() for prev_line in all_lines[:line_num - 1] if prev_line.strip() and not prev_line.strip().startswith("#")] if prev_lines and prev_lines[-1].startswith("!"): self.warnings.append(ValidationIssue( line_number=line_num, line=line, issue_type="double_negation", message="Consecutive negation patterns - verify this is intentional", severity="warning" )) def _check_backslash_escapes(self, line: str, line_num: int) -> None: """Check for invalid backslash escapes.""" if self.BACKSLASH_PATTERN.search(line): self.issues.append(ValidationIssue( line_number=line_num, line=line, issue_type="invalid_escape", message="Backslash escape not recognized in .gitignore", severity="error" )) def _check_invalid_wildcards(self, line: str, line_num: int) -> None: """Check for potentially invalid wildcard usage.""" if "**" in line and not (line.startswith("**") or line.endswith("**")): self.warnings.append(ValidationIssue( line_number=line_num, line=line, issue_type="double_star", message="Double wildcard '**' should only be used at start or end", severity="warning" )) def _check_duplicate_pattern(self, line: str, line_num: int, all_lines: List[str]) -> None: """Check for duplicate patterns.""" stripped = line.strip() for prev_line_num, prev_line in enumerate(all_lines[:line_num - 1], start=1): prev_stripped = prev_line.strip() if prev_stripped == stripped: self.warnings.append(ValidationIssue( line_number=line_num, line=line, issue_type="duplicate", message=f"Duplicate of pattern on line {prev_line_num}", severity="warning" )) break def validate_pattern(self, pattern: str) -> Optional[ValidationIssue]: """Validate a single pattern.""" if pattern.startswith("#") or not pattern.strip(): return None if self.BACKSLASH_PATTERN.search(pattern): return ValidationIssue( line_number=0, line=pattern, issue_type="invalid_escape", message="Backslash escape not recognized in .gitignore", severity="error" ) return None def is_valid(self, content: str) -> bool: """Check if content is valid.""" self.validate_content(content) return not any(issue.severity == "error" for issue in self.issues + self.warnings) def get_summary(self, content: str) -> dict: """Get validation summary.""" self.validate_content(content) return { "valid": not any(issue.severity == "error" for issue in self.issues + self.warnings), "errors": len([i for i in self.issues if i.severity == "error"]), "warnings": len([i for i in self.warnings if i.severity == "warning"]), "issues": self.issues + self.warnings } validator = Validator()