- Extract shared directory resolution to check_scripts/utils.py - Refine check_unused_css regex to accurately match className attribute values without broad character windows - Deduplicate adjacent unused-css-check-ignore comments in base.css
163 lines
5.5 KiB
Python
163 lines
5.5 KiB
Python
import os
|
|
import re
|
|
import sys
|
|
|
|
from utils import resolve_search_path
|
|
|
|
CONFLICT_RULES = {
|
|
"overflow": {"overflow-x", "overflow-y"},
|
|
"margin": {"margin-top", "margin-right", "margin-bottom", "margin-left"},
|
|
"padding": {"padding-top", "padding-right", "padding-bottom", "padding-left"},
|
|
"border": {
|
|
"border-top",
|
|
"border-right",
|
|
"border-bottom",
|
|
"border-left",
|
|
"border-width",
|
|
"border-style",
|
|
"border-color",
|
|
},
|
|
"background": {"background-color", "background-image", "background-position", "background-size"},
|
|
"flex": {"flex-grow", "flex-shrink", "flex-basis"},
|
|
}
|
|
|
|
|
|
def parse_css_blocks(content: str) -> list[tuple[str, str, int]]:
|
|
stack = []
|
|
blocks = []
|
|
in_comment = False
|
|
in_string = False
|
|
str_char = ""
|
|
i = 0
|
|
n = len(content)
|
|
|
|
while i < n:
|
|
if in_comment:
|
|
if content[i : i + 2] == "*/":
|
|
in_comment = False
|
|
i += 2
|
|
else:
|
|
i += 1
|
|
continue
|
|
|
|
if not in_string and content[i : i + 2] == "/*":
|
|
in_comment = True
|
|
i += 2
|
|
continue
|
|
|
|
char = content[i]
|
|
if in_string:
|
|
if char == "\\":
|
|
i += 2
|
|
continue
|
|
if char == str_char:
|
|
in_string = False
|
|
elif char in ('"', "'"):
|
|
in_string = True
|
|
str_char = char
|
|
elif char == "{":
|
|
stack.append(i)
|
|
elif char == "}":
|
|
if stack:
|
|
start = stack.pop()
|
|
prev = start - 1
|
|
while prev >= 0 and content[prev] not in "{}":
|
|
prev -= 1
|
|
selector = content[prev + 1 : start].strip()
|
|
block_content = content[start + 1 : i]
|
|
blocks.append((selector, block_content, start))
|
|
i += 1
|
|
|
|
return blocks
|
|
|
|
|
|
def check_file(filepath: str) -> list:
|
|
errors = []
|
|
try:
|
|
with open(filepath, encoding="utf-8") as f:
|
|
original_content = f.read()
|
|
|
|
# Iterate through all matches in the original content to get accurate line numbers and check for ignore comments
|
|
for selector_raw, block_content_raw, start_pos in parse_css_blocks(original_content):
|
|
# Get line number from original content
|
|
line_num = original_content.count("\n", 0, start_pos) + 1
|
|
|
|
if (
|
|
"/* property-conflict-ignore */" in block_content_raw
|
|
or "/* property-conflict-ignore */" in selector_raw
|
|
):
|
|
continue
|
|
|
|
# Clear comments to isolate property names correctly
|
|
clean_block_content = re.sub(r"/\*.*?\*/", "", block_content_raw, flags=re.DOTALL)
|
|
|
|
properties = set()
|
|
declarations = clean_block_content.split(";")
|
|
for decl in declarations:
|
|
parts = decl.split(":", 1)
|
|
if len(parts) == 2:
|
|
prop_name = parts[0].strip()
|
|
if prop_name:
|
|
properties.add(prop_name)
|
|
|
|
for shorthand, longhands in CONFLICT_RULES.items():
|
|
if shorthand in properties:
|
|
conflicting = properties.intersection(longhands)
|
|
if conflicting:
|
|
errors.append(
|
|
{
|
|
"line": line_num,
|
|
"selector": selector_raw.strip().split("}")[-1].strip(),
|
|
"shorthand": shorthand,
|
|
"longhands": sorted(list(conflicting)),
|
|
}
|
|
)
|
|
except OSError as e:
|
|
print(f"Error reading {filepath}: {e}")
|
|
sys.exit(1)
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> None:
|
|
base_dir, target_dir, search_path = resolve_search_path()
|
|
|
|
print(f"Checking for CSS property conflicts in {target_dir}...")
|
|
|
|
has_errors = False
|
|
for root, _, files in os.walk(search_path):
|
|
for file in files:
|
|
if file.endswith(".css"):
|
|
filepath = os.path.join(root, file)
|
|
errors = check_file(filepath)
|
|
if errors:
|
|
has_errors = True
|
|
rel_path = os.path.relpath(filepath, base_dir)
|
|
print(f"\n[!] CSS property conflict found in {rel_path}:")
|
|
# Sort errors by line number
|
|
errors.sort(key=lambda x: x["line"])
|
|
for err in errors:
|
|
longhands_str = ", ".join(err["longhands"])
|
|
# If selector is empty (e.g., just spaces/newlines), it might be formatting.
|
|
# Use a fallback name.
|
|
sel_disp = err["selector"] if err["selector"] else "<unknown-selector>"
|
|
# Trim extremely long selectors if needed
|
|
if len(sel_disp) > 100:
|
|
sel_disp = sel_disp[:97] + "..."
|
|
print(f" Line {err['line']}: {sel_disp} {{ ... }}")
|
|
print(
|
|
f" -> Conflict: shorthand '{err['shorthand']}' used alongside longhand(s) '{longhands_str}'."
|
|
)
|
|
|
|
if has_errors:
|
|
print("\nError: Conflicting CSS properties detected!")
|
|
print("Please resolve these conflicts or add /* property-conflict-ignore */ in the block.")
|
|
sys.exit(1)
|
|
else:
|
|
print("CSS property conflicts check passed.")
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|