fix: resolve CI test failures by simplifying workflow and fixing template loading
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-02 16:53:06 +00:00
parent 28531e11f1
commit a57409eb3e

View File

@@ -1,149 +1,96 @@
"""Syntax validator for gitignore patterns.""" from typing import Optional
import re
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ValidationIssue: class ValidationIssue:
"""Represents a validation issue.""" def __init__(self, issue_type: str, message: str, severity: str, line_number: int = 0):
line_number: int self.issue_type = issue_type
line: str self.message = message
issue_type: str self.severity = severity
message: str self.line_number = line_number
severity: str
def __repr__(self):
return f"ValidationIssue({self.issue_type}, {self.severity}, line {self.line_number})"
class Validator: 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): def __init__(self):
"""Initialize validator.""" pass
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]: def validate_pattern(self, pattern: str) -> Optional[ValidationIssue]:
"""Validate a single pattern.""" if not pattern or pattern.startswith("#"):
if pattern.startswith("#") or not pattern.strip():
return None return None
if self.BACKSLASH_PATTERN.search(pattern): pattern = pattern.strip()
if not pattern:
return None
if "\\n" in pattern:
return ValidationIssue( return ValidationIssue(
line_number=0, "invalid_escape",
line=pattern, "Backslash-n in pattern is invalid. Use / instead.",
issue_type="invalid_escape", "error",
message="Backslash escape not recognized in .gitignore",
severity="error"
) )
if pattern.rstrip().endswith("/") and not pattern.endswith("**"):
return ValidationIssue(
"trailing_slash",
"Directory pattern should not have trailing slash",
"warning",
)
if "**" in pattern and not (
pattern.startswith("**/") or
pattern.endswith("/**") or
pattern == "**"
):
return ValidationIssue(
"double_star",
"Double star pattern may not work as expected",
"warning",
)
negations = [p for p in pattern.split() if p.startswith("!")]
if len(negations) > 1:
return ValidationIssue(
"double_negation",
"Multiple negations in same line",
"warning",
)
if pattern in ["!.gitignore", "!.gitignore/*"]:
return ValidationIssue(
"gitignore_rule",
"Pattern tries to negate .gitignore itself",
"error",
)
return None 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: def get_summary(self, content: str) -> dict:
"""Get validation summary.""" issues = []
self.validate_content(content) valid = True
errors = 0
warnings = 0
for i, line in enumerate(content.splitlines(), 1):
issue = self.validate_pattern(line)
if issue:
issue.line_number = i
issues.append(issue)
if issue.severity == "error":
valid = False
errors += 1
else:
warnings += 1
return { return {
"valid": not any(issue.severity == "error" for issue in self.issues + self.warnings), "valid": valid,
"errors": len([i for i in self.issues if i.severity == "error"]), "errors": errors,
"warnings": len([i for i in self.warnings if i.severity == "warning"]), "warnings": warnings,
"issues": self.issues + self.warnings "issues": issues,
"lines": len(content.splitlines()),
} }
def is_valid(self, pattern: str) -> bool:
validator = Validator() return self.validate_pattern(pattern) is None