playbook/antigravity-awesome-skills/skills/skill-sentinel/scripts/analyzers/dependencies.py

122 lines
4.5 KiB
Python

"""
Analyzer de dependencias.
Verifica: requirements.txt existe, versoes pinadas, dependencias nao usadas,
dependencias importadas mas nao listadas.
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple
def _extract_imports(source: str) -> Set[str]:
"""Extrai nomes de pacotes importados de um arquivo Python."""
imports = set()
for match in re.finditer(r'^(?:import|from)\s+(\w+)', source, re.MULTILINE):
pkg = match.group(1)
# Ignorar stdlib e imports relativos
if pkg not in {
"os", "sys", "re", "json", "ast", "pathlib", "datetime", "typing",
"uuid", "hashlib", "sqlite3", "argparse", "collections", "functools",
"time", "math", "io", "csv", "logging", "traceback", "textwrap",
"urllib", "http", "shutil", "subprocess", "tempfile", "threading",
"concurrent", "asyncio", "dataclasses", "enum", "abc", "copy",
"config", "db", "governance", "scanner", "analyzers", # modulos internos
}:
imports.add(pkg)
return imports
def _normalize_pkg_name(name: str) -> str:
"""Normaliza nome de pacote para comparacao (- e _ sao equivalentes)."""
return name.lower().replace("-", "_")
def analyze(skill_data: Dict[str, Any]) -> Tuple[float, List[Dict[str, Any]]]:
"""Analisa dependencias de uma skill. Retorna (score, findings)."""
score = 100.0
findings: List[Dict[str, Any]] = []
skill_name = skill_data["name"]
skill_path = Path(skill_data["path"])
requirements = skill_data.get("requirements", [])
reqs_path = skill_path / "scripts" / "requirements.txt"
# Sem requirements.txt
if not reqs_path.exists():
# Se nao tem Python files, nao precisa
if skill_data.get("file_count", 0) > 0:
findings.append({
"skill_name": skill_name,
"dimension": "dependencies",
"severity": "medium",
"category": "missing_requirements",
"title": "requirements.txt nao encontrado",
"recommendation": "Criar scripts/requirements.txt com todas as dependencias",
"effort": "low",
"impact": "medium",
})
score -= 15
return max(0.0, score), findings
# Verificar versoes pinadas
unpinned = [r for r in requirements if not r.get("pinned")]
if unpinned and len(requirements) > 1:
names = ", ".join(r["name"] for r in unpinned[:5])
findings.append({
"skill_name": skill_name,
"dimension": "dependencies",
"severity": "low",
"category": "unpinned_versions",
"title": f"{len(unpinned)} dependencia(s) sem versao pinada",
"description": f"Pacotes sem ==: {names}",
"recommendation": "Pinar versoes com == para reproducibilidade (ex: requests==2.31.0)",
"effort": "low",
"impact": "medium",
})
score -= min(10, len(unpinned) * 2)
# Verificar deps importadas vs listadas
all_imports: Set[str] = set()
for rel_path in skill_data.get("python_files", []):
filepath = skill_path / rel_path
if not filepath.exists():
continue
try:
source = filepath.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
all_imports.update(_extract_imports(source))
listed_names = {_normalize_pkg_name(r["name"]) for r in requirements}
# Importadas mas nao listadas (possivel dep faltando)
# Mapear nomes de import para nomes de pacote (alguns diferem)
import_to_pkg = {
"PIL": "pillow",
"cv2": "opencv_python",
"bs4": "beautifulsoup4",
"yaml": "pyyaml",
"dotenv": "python_dotenv",
"playwright": "playwright",
}
for imp in all_imports:
pkg_name = _normalize_pkg_name(import_to_pkg.get(imp, imp))
if pkg_name not in listed_names:
findings.append({
"skill_name": skill_name,
"dimension": "dependencies",
"severity": "low",
"category": "unlisted_dependency",
"title": f"Pacote '{imp}' importado mas nao em requirements.txt",
"recommendation": f"Adicionar {imp} ao requirements.txt",
"effort": "low",
"impact": "low",
})
score -= 2
return max(0.0, min(100.0, score)), findings