138 lines
6.1 KiB
Python
138 lines
6.1 KiB
Python
import os
|
|
import re
|
|
import sys
|
|
|
|
from utils import resolve_search_path
|
|
|
|
|
|
def get_defined_classes(search_path: str) -> dict[str, str]:
|
|
defined: dict[str, list[str]] = {}
|
|
ignored_classes: dict[str, set[int]] = {}
|
|
|
|
for root, _, files in os.walk(search_path):
|
|
for file in files:
|
|
if file.endswith(".css") and not file.endswith(".module.css"):
|
|
filepath = os.path.join(root, file)
|
|
try:
|
|
with open(filepath, encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
def comment_replacer(match: re.Match[str]) -> str:
|
|
if "unused-css-check-ignore" in match.group(0):
|
|
return " /* unused-css-check-ignore */ "
|
|
return " " * len(match.group(0))
|
|
|
|
clean_content = re.sub(r"/\*.*?\*/", comment_replacer, content, flags=re.DOTALL)
|
|
|
|
ignore_next = False
|
|
for match in re.finditer(
|
|
r"(/[*]\s*unused-css-check-ignore\s*[*]/|\.meld-[a-zA-Z0-9_-]+|[{};])", clean_content
|
|
):
|
|
token = match.group(1)
|
|
if "unused-css-check-ignore" in token:
|
|
ignore_next = True
|
|
elif token in ("{", "}", ";"):
|
|
ignore_next = False
|
|
elif token.startswith(".meld-"):
|
|
class_name = token[1:]
|
|
if ignore_next:
|
|
ignored_classes.setdefault(class_name, set()).add(len(defined.get(class_name, [])))
|
|
# Do not reset ignore_next so grouped selectors all get ignored
|
|
defined.setdefault(class_name, []).append(filepath)
|
|
except OSError as e:
|
|
print(f"Error reading {filepath}: {e}")
|
|
sys.exit(1)
|
|
|
|
final_defined: dict[str, str] = {}
|
|
for c, occurrences in defined.items():
|
|
ignored_indices = ignored_classes.get(c, set())
|
|
unignored = [occ for i, occ in enumerate(occurrences) if i not in ignored_indices]
|
|
if unignored:
|
|
final_defined[c] = unignored[0]
|
|
|
|
return final_defined
|
|
|
|
|
|
def get_referenced_classes(search_path: str) -> set[str]:
|
|
references: set[str] = set()
|
|
styles_import_pattern = re.compile(r'import\s+(\w+)\s+from\s+["\'].*\.module\.css["\']')
|
|
|
|
for root, _, files in os.walk(search_path):
|
|
for file in files:
|
|
if file.endswith((".tsx", ".ts")):
|
|
filepath = os.path.join(root, file)
|
|
try:
|
|
with open(filepath, encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
# 1. Handle CSS Module imports
|
|
style_vars = [m.group(1) for m in styles_import_pattern.finditer(content)]
|
|
for style_var in style_vars:
|
|
style_usage_pattern = re.compile(r"\b" + re.escape(style_var) + r"\.([a-zA-Z0-9_-]+)")
|
|
for match in style_usage_pattern.finditer(content):
|
|
references.add(match.group(1))
|
|
|
|
# 2. Extract classes from static strings
|
|
static_class_pattern = re.compile(r'className\s*=\s*["\']([^"\']+)["\']')
|
|
for match in static_class_pattern.finditer(content):
|
|
for cls in match.group(1).split():
|
|
references.add(cls)
|
|
|
|
# 3. Extract classes from template literals
|
|
expr_class_pattern = re.compile(r"className\s*=\s*{(.*?)}", re.DOTALL)
|
|
for match in expr_class_pattern.finditer(content):
|
|
expr = match.group(1)
|
|
for str_match in re.finditer(r'(["\'`])(.*?)\1', expr, re.DOTALL):
|
|
val = str_match.group(2)
|
|
val_clean = re.sub(r"\$\{[^}]+\}", " ", val)
|
|
for cls in val_clean.split():
|
|
references.add(cls)
|
|
|
|
# 4. Targeted extraction: search inside className usages
|
|
# to properly handle class names inside complex template literals or conditionals
|
|
for attr_match in re.finditer(
|
|
r'className\s*=\s*({(?:[^{}]|{(?:[^{}]|{[^{}]*})*})*}|(["\'`])[\s\S]*?\2)', content
|
|
):
|
|
attr_val = attr_match.group(1)
|
|
for str_match in re.finditer(r'(["\'`])(.*?)\1', attr_val, flags=re.DOTALL):
|
|
val = str_match.group(2)
|
|
for cls in re.finditer(r"\b(meld-[a-zA-Z0-9_-]+)\b", val):
|
|
references.add(cls.group(1))
|
|
|
|
except OSError as e:
|
|
print(f"Error reading {filepath}: {e}")
|
|
sys.exit(1)
|
|
return references
|
|
|
|
|
|
def main() -> None:
|
|
base_dir, target_dir, search_path = resolve_search_path()
|
|
|
|
print(f"Checking for unused CSS classes in {target_dir}...")
|
|
|
|
defined_classes = get_defined_classes(search_path)
|
|
referenced_classes = get_referenced_classes(search_path)
|
|
|
|
unused = []
|
|
|
|
for cls, filepath in defined_classes.items():
|
|
if cls not in referenced_classes:
|
|
unused.append((cls, filepath))
|
|
|
|
if unused:
|
|
print("\n[!] Unused CSS classes found:")
|
|
for cls, filepath in sorted(unused, key=lambda x: x[1]):
|
|
rel_path = os.path.relpath(filepath, base_dir)
|
|
print(f" {rel_path}: .{cls}")
|
|
|
|
print("\nError: Unused CSS classes detected!")
|
|
print("Please remove these classes, use them in your files, or add /* unused-css-check-ignore */ before them.")
|
|
sys.exit(1)
|
|
else:
|
|
print("CSS unused classes check passed.")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|