Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| import argparse | |
| import csv | |
| import json | |
| import re | |
| import subprocess | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| TEMPLATE_ROOT = ROOT / "modules/chart_engine/template/d3-js" | |
| CHECKS = { | |
| "missing_chart_utils": re.compile(r"chartUtils\."), | |
| "missing_color_resolver": re.compile(r"chartUtils\.color\.resolver"), | |
| "missing_schema_helper": re.compile(r"chartUtils\.schema\.(?:channel|channels|channelByKey|columns)"), | |
| "missing_format_helper": re.compile(r"chartUtils\.format\."), | |
| "missing_text_helper": re.compile(r"chartUtils\.text\."), | |
| "missing_legend_helper": re.compile(r"chartUtils\.legend\."), | |
| } | |
| ISSUE_PATTERNS = { | |
| "direct_column_index": re.compile( | |
| r"(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)" | |
| r"\[[^\]]+\](?:\?\.|\.)" | |
| r"(?:name|role|unit|label|description|display_name|data_type|type)" | |
| r"|(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)" | |
| r"\.(?:find|filter)\(" | |
| r"|(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)" | |
| r"\.filter\([^)]*\.role\s*===\s*['\"][^'\"]+['\"][^)]*\)\[[0-9]+\]\.name" | |
| r"|(?:dataColumns|data\.data\.columns|jsonData\.data\.columns)" | |
| r"\.find\([^)]*\.role\s*===\s*['\"][^'\"]+['\"][^)]*\)" | |
| ), | |
| "local_format_value": re.compile(r"\b(?:const|let|var|function)\s+formatValue\b|formatValue\s*="), | |
| "local_text_measure": re.compile( | |
| r"\b(?:const|let|var|function)\s+(?:getTextWidth|getTextWidthCanvas|measureText)\b" | |
| r"|\.getBBox\(|getComputedTextLength\(|\.measureText\(" | |
| ), | |
| "local_parse_date": re.compile( | |
| r"\b(?:const|let|var|function)\s+parseDate\b|parseDate\s*=" | |
| r"|new Date\(\s*d\[[^\]]+\]\s*\)" | |
| ), | |
| "direct_color_access": re.compile( | |
| r"\bcolors(?:\.|\?\.)" | |
| r"(?:other|field|available_colors|text_color|background_color)\b" | |
| r"|\b(?:data|jsonData|dataJSON)(?:\.|\?\.)colors(?:\.|\?\.)" | |
| r"(?:other|field|available_colors|text_color|background_color)\b" | |
| ), | |
| "direct_color_variant": re.compile(r"\.(?:brighter|darker)\("), | |
| "legacy_global_helper_call": re.compile( | |
| r"(?<![.\w$])(?:layoutLegend|getTextWidth|formatValue|parseDate)\(" | |
| ), | |
| "debug_console_log": re.compile(r"console\.log\("), | |
| "math_random": re.compile(r"Math\.random\("), | |
| "schema_role_lookup": re.compile( | |
| r"\.(?:find|filter)\([^\n)]*\.role\s*={2,3}\s*['\"][^'\"]+['\"]" | |
| r"|(?:\b\w+|\])\.role\s*={2,3}\s*['\"][^'\"]+['\"]" | |
| ), | |
| "manual_legend": re.compile( | |
| r"\blegend(?:Group|G|Items?|Lines?|Container|Block|Offset|X|Y|Gap|Padding|Spacing|Width|Height)\b" | |
| r"|class[\"'],\s*[\"']legend" | |
| ), | |
| } | |
| def has_requirements(text: str) -> bool: | |
| return "REQUIREMENTS_BEGIN" in text and "REQUIREMENTS_END" in text | |
| def node_check(path: Path) -> str: | |
| proc = subprocess.run( | |
| ["node", "--check", str(path)], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| ) | |
| if proc.returncode == 0: | |
| return "" | |
| return proc.stderr.strip().splitlines()[0] if proc.stderr.strip() else "node --check failed" | |
| def audit_file(path: Path, run_node_check: bool) -> dict: | |
| text = path.read_text(encoding="utf-8") | |
| record = { | |
| "path": str(path.relative_to(ROOT)), | |
| "has_requirements": has_requirements(text), | |
| "syntax_error": node_check(path) if run_node_check else "", | |
| } | |
| for name, pattern in CHECKS.items(): | |
| if name.startswith("missing_"): | |
| record[name] = not bool(pattern.search(text)) | |
| for name, pattern in ISSUE_PATTERNS.items(): | |
| record[name] = bool(pattern.search(text)) | |
| record["manual_legend_without_chart_utils_legend"] = ( | |
| record["manual_legend"] and record["missing_legend_helper"] | |
| ) | |
| return record | |
| def summarize(records: list[dict]) -> dict: | |
| keys = [ | |
| "has_requirements", | |
| "syntax_error", | |
| *CHECKS.keys(), | |
| *ISSUE_PATTERNS.keys(), | |
| "manual_legend_without_chart_utils_legend", | |
| ] | |
| summary = {"total_js": len(records)} | |
| summary["registered_requirement_files"] = sum(1 for record in records if record["has_requirements"]) | |
| for key in keys: | |
| if key == "syntax_error": | |
| summary[key] = sum(1 for record in records if record[key]) | |
| elif key == "has_requirements": | |
| continue | |
| else: | |
| summary[key] = sum(1 for record in records if record.get(key)) | |
| return summary | |
| def write_csv(path: Path, records: list[dict]) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| fieldnames = list(records[0].keys()) if records else ["path"] | |
| with open(path, "w", newline="", encoding="utf-8") as f: | |
| writer = csv.DictWriter(f, fieldnames=fieldnames) | |
| writer.writeheader() | |
| writer.writerows(records) | |
| def print_summary(summary: dict, records: list[dict], max_examples: int) -> None: | |
| print(json.dumps(summary, indent=2, ensure_ascii=False)) | |
| for key, count in summary.items(): | |
| if key in {"total_js", "registered_requirement_files"} or not count: | |
| continue | |
| examples = [record["path"] for record in records if record.get(key)][:max_examples] | |
| if examples: | |
| print(f"\n{key} examples:") | |
| for item in examples: | |
| print(f" {item}") | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Audit D3 template usage of chartUtils standard helpers.") | |
| parser.add_argument("--template-root", default=str(TEMPLATE_ROOT)) | |
| parser.add_argument("--output-json", default=None) | |
| parser.add_argument("--output-csv", default=None) | |
| parser.add_argument("--examples", type=int, default=5) | |
| parser.add_argument("--node-check", action="store_true") | |
| return parser.parse_args() | |
| def main() -> None: | |
| args = parse_args() | |
| template_root = Path(args.template_root) | |
| records = [ | |
| audit_file(path, run_node_check=args.node_check) | |
| for path in sorted(template_root.rglob("*.js")) | |
| ] | |
| summary = summarize(records) | |
| if args.output_json: | |
| output_path = Path(args.output_json) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text( | |
| json.dumps({"summary": summary, "records": records}, indent=2, ensure_ascii=False), | |
| encoding="utf-8", | |
| ) | |
| if args.output_csv: | |
| write_csv(Path(args.output_csv), records) | |
| print_summary(summary, records, args.examples) | |
| if __name__ == "__main__": | |
| main() | |