Files
fuz-site/find_unused_css_classes.py
2025-12-19 14:56:02 +01:00

121 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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'(?<![\\\w-])\.([a-zA-Z_-][\w-]*)')
# --- Regexy do zbierania stringów klas z Astro/JSX/TSX ---
# 1) class="..."
CLASS_ATTR_RE = re.compile(r'\bclass\s*=\s*("([^"]*)"|\'([^\']*)\')', re.IGNORECASE)
# 2) className="..."
CLASSNAME_ATTR_RE = re.compile(r'\bclassName\s*=\s*("([^"]*)"|\'([^\']*)\')', re.IGNORECASE)
# Astro: class:list={{ a: cond, "b c": cond2 }} / class:list={[...]} tu łapiemy stringi w środku
STRING_LIT_RE = re.compile(r'("([^"]+)"|\'([^\']+)\')')
# Dla wyszukiwania tokenów klas (żeby "f-card" nie matchowało jako fragment "f-card-x")
def token_pattern(cls: str) -> re.Pattern:
return re.compile(r'(?<![\w-])' + re.escape(cls) + r'(?![\w-])')
def read_text(path: Path) -> 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()