fix: resolve CI test failures by simplifying workflow and fixing template loading
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user