#!/usr/bin/env python3 import argparse import os import re from pathlib import Path from typing import Dict, List, Set, Tuple # --- Regexy do zbierania klas z CSS --- # Łapie ".class-name" w selektorach (pomija np. ".5" i rzeczy z escapami – prosto, ale działa w praktyce) CSS_CLASS_RE = re.compile(r'(? re.Pattern: return re.compile(r'(? str: try: return path.read_text(encoding="utf-8", errors="ignore") except Exception: return "" def collect_css_classes(css_dir: Path) -> Tuple[Set[str], Dict[str, Set[Path]]]: classes: Set[str] = set() where: Dict[str, Set[Path]] = {} for p in css_dir.rglob("*.css"): txt = read_text(p) for m in CSS_CLASS_RE.finditer(txt): c = m.group(1) classes.add(c) where.setdefault(c, set()).add(p) return classes, where def collect_used_classes(code_dir: Path, candidates: Set[str]) -> Tuple[Set[str], Dict[str, Set[Path]]]: used: Set[str] = set() used_where: Dict[str, Set[Path]] = {c: set() for c in candidates} exts = {".astro", ".jsx", ".tsx", ".js", ".ts"} # prekompilacja patternów dla szybkości patterns = {c: token_pattern(c) for c in candidates} for p in code_dir.rglob("*"): if not p.is_file(): continue if p.suffix.lower() not in exts: continue txt = read_text(p) if not txt: continue # szybki filtr: jeśli żaden kandydat nie ma nawet prefiksu "f-" / "jmb-" itd, # to i tak skan tokenowy jest ok, ale tu robimy prosty scan wszystkich. for c, pat in patterns.items(): if pat.search(txt): used.add(c) used_where[c].add(p) # usuń puste wpisy used_where = {k: v for k, v in used_where.items() if v} return used, used_where def main(): ap = argparse.ArgumentParser(description="Znajdź potencjalnie nieużywane klasy CSS w projekcie Astro/JSX.") ap.add_argument("--css", required=True, help="Katalog z plikami CSS (np. src/styles)") ap.add_argument("--code", required=True, help="Katalog z kodem (np. src)") ap.add_argument("--min-len", type=int, default=3, help="Minimalna długość nazwy klasy (domyślnie 3)") ap.add_argument("--prefix", action="append", default=[], help="Filtruj klasy po prefiksie (np. --prefix f- --prefix jmb-)") ap.add_argument("--show-where", action="store_true", help="Pokaż gdzie zdefiniowano klasę w CSS") args = ap.parse_args() css_dir = Path(args.css).resolve() code_dir = Path(args.code).resolve() if not css_dir.exists(): raise SystemExit(f"Brak katalogu CSS: {css_dir}") if not code_dir.exists(): raise SystemExit(f"Brak katalogu code: {code_dir}") all_classes, defined_where = collect_css_classes(css_dir) # filtr długości + prefiksów classes = {c for c in all_classes if len(c) >= args.min_len} if args.prefix: classes = {c for c in classes if any(c.startswith(px) for px in args.prefix)} used, used_where = collect_used_classes(code_dir, classes) unused = sorted(classes - used) print(f"CSS katalog: {css_dir}") print(f"CODE katalog: {code_dir}") print(f"Klasy w CSS: {len(classes)} (po filtrach)") print(f"Użyte w kodzie:{len(used)}") print(f"NIEUŻYTE: {len(unused)}") print("-" * 60) for c in unused: print(c) if args.show_where: files = sorted(defined_where.get(c, [])) for f in files: rel = f.relative_to(css_dir.parent) if css_dir.parent in f.parents else f print(f" defined in: {rel}") print("-" * 60) # opcjonalnie: pokaż top kilka użyć # (jak chcesz, dopiszę flagę na raport "gdzie użyte") if __name__ == "__main__": main()