diff --git a/scripts/validate/manifest.py b/scripts/validate/manifest.py new file mode 100755 index 0000000..c7a4351 --- /dev/null +++ b/scripts/validate/manifest.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Validate Joomla manifest files. + +Copyright (C) 2025 Moko Consulting + +This file is part of a Moko Consulting project. + +SPDX-License-Identifier: GPL-3.0-or-later + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program (./LICENSE.md). + +FILE INFORMATION +DEFGROUP: Moko-Cassiopeia.Scripts +INGROUP: Scripts.Validate +REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +PATH: /scripts/validate/manifest.py +VERSION: 01.00.00 +BRIEF: Validate Joomla extension manifest files +""" + +import argparse +import sys +from pathlib import Path + +# Add lib directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) + +try: + import common + import joomla_manifest +except ImportError: + print("ERROR: Cannot import required libraries", file=sys.stderr) + sys.exit(1) + + +def validate_manifest_file(manifest_path: Path, verbose: bool = False) -> bool: + """ + Validate a single manifest file. + + Args: + manifest_path: Path to manifest file + verbose: Show detailed output + + Returns: + True if valid, False otherwise + """ + try: + info = joomla_manifest.parse_manifest(manifest_path) + is_valid, warnings = joomla_manifest.validate_manifest(manifest_path) + + if verbose: + common.log_section(f"Manifest: {manifest_path}") + common.log_kv("Type", info.extension_type) + common.log_kv("Name", info.name) + common.log_kv("Version", info.version) + + if warnings: + common.log_warn(f"Warnings ({len(warnings)}):") + for warning in warnings: + print(f" - {warning}") + + # Output JSON for machine consumption + result = { + "status": "ok" if is_valid else "error", + "manifest": str(manifest_path), + "ext_type": info.extension_type, + "name": info.name, + "version": info.version, + "warnings": warnings + } + + if not verbose: + common.json_output(result) + + if is_valid: + if not verbose: + print(f"manifest: ok ({manifest_path})") + else: + common.log_success("Manifest is valid") + return True + else: + common.log_error(f"Manifest validation failed: {manifest_path}") + return False + + except SystemExit: + common.log_error(f"Failed to parse manifest: {manifest_path}") + return False + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Validate Joomla extension manifest files", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + "-s", "--src-dir", + default="src", + help="Source directory to search for manifests (default: src)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Show detailed output" + ) + parser.add_argument( + "manifest", + nargs="?", + help="Specific manifest file to validate (optional)" + ) + + args = parser.parse_args() + + try: + if args.manifest: + # Validate specific manifest + manifest_path = Path(args.manifest) + if not manifest_path.is_file(): + common.die(f"Manifest file not found: {args.manifest}") + + success = validate_manifest_file(manifest_path, args.verbose) + return 0 if success else 1 + else: + # Find and validate all manifests in src directory + manifests = joomla_manifest.find_all_manifests(args.src_dir) + + if not manifests: + common.die(f"No manifest files found in {args.src_dir}") + + if args.verbose: + common.log_section("Validating Manifests") + common.log_info(f"Found {len(manifests)} manifest(s)") + print() + + all_valid = True + for manifest in manifests: + if not validate_manifest_file(manifest, args.verbose): + all_valid = False + + if args.verbose: + print() + if all_valid: + common.log_success(f"All {len(manifests)} manifest(s) are valid") + else: + common.log_error("Some manifests failed validation") + + return 0 if all_valid else 1 + + except Exception as e: + common.log_error(f"Validation failed: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/validate/php_syntax.py b/scripts/validate/php_syntax.py new file mode 100755 index 0000000..cd6251b --- /dev/null +++ b/scripts/validate/php_syntax.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Validate PHP syntax in files. + +Copyright (C) 2025 Moko Consulting + +This file is part of a Moko Consulting project. + +SPDX-License-Identifier: GPL-3.0-or-later + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program (./LICENSE.md). + +FILE INFORMATION +DEFGROUP: Moko-Cassiopeia.Scripts +INGROUP: Scripts.Validate +REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +PATH: /scripts/validate/php_syntax.py +VERSION: 01.00.00 +BRIEF: Validate PHP syntax in all PHP files +""" + +import argparse +import subprocess +import sys +from pathlib import Path +from typing import List, Tuple + +# Add lib directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) + +try: + import common +except ImportError: + print("ERROR: Cannot import required libraries", file=sys.stderr) + sys.exit(1) + + +def check_php_file(file_path: Path) -> Tuple[bool, str]: + """ + Check PHP syntax of a single file. + + Args: + file_path: Path to PHP file + + Returns: + Tuple of (is_valid, error_message) + """ + try: + result = subprocess.run( + ["php", "-l", str(file_path)], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + return (True, "") + else: + return (False, result.stderr or result.stdout) + + except subprocess.TimeoutExpired: + return (False, "Syntax check timed out") + except Exception as e: + return (False, str(e)) + + +def find_php_files(src_dir: str, exclude_dirs: List[str] = None) -> List[Path]: + """ + Find all PHP files in a directory. + + Args: + src_dir: Directory to search + exclude_dirs: Directories to exclude + + Returns: + List of PHP file paths + """ + if exclude_dirs is None: + exclude_dirs = ["vendor", "node_modules", ".git"] + + src_path = Path(src_dir) + if not src_path.is_dir(): + return [] + + php_files = [] + for php_file in src_path.rglob("*.php"): + # Check if file is in an excluded directory + if any(excluded in php_file.parts for excluded in exclude_dirs): + continue + php_files.append(php_file) + + return sorted(php_files) + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Validate PHP syntax in all PHP files", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + "-s", "--src-dir", + default="src", + help="Source directory to search for PHP files (default: src)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Show detailed output" + ) + parser.add_argument( + "--exclude", + action="append", + help="Directories to exclude (can be specified multiple times)" + ) + parser.add_argument( + "files", + nargs="*", + help="Specific files to check (optional)" + ) + + args = parser.parse_args() + + # Check if PHP is available + common.require_cmd("php") + + try: + # Determine which files to check + if args.files: + php_files = [Path(f) for f in args.files] + for f in php_files: + if not f.is_file(): + common.die(f"File not found: {f}") + else: + exclude_dirs = args.exclude or ["vendor", "node_modules", ".git"] + php_files = find_php_files(args.src_dir, exclude_dirs) + + if not php_files: + common.die(f"No PHP files found in {args.src_dir}") + + if args.verbose: + common.log_section("PHP Syntax Validation") + common.log_info(f"Checking {len(php_files)} PHP file(s)") + print() + + errors = [] + for php_file in php_files: + is_valid, error_msg = check_php_file(php_file) + + if is_valid: + if args.verbose: + common.log_success(f"OK: {php_file}") + else: + errors.append((php_file, error_msg)) + if args.verbose: + common.log_error(f"FAILED: {php_file}") + if error_msg: + print(f" {error_msg}") + + # Output results + if args.verbose: + print() + + if errors: + result = { + "status": "error", + "total": len(php_files), + "passed": len(php_files) - len(errors), + "failed": len(errors), + "errors": [{"file": str(f), "error": e} for f, e in errors] + } + + if not args.verbose: + common.json_output(result) + + common.log_error(f"PHP syntax check failed: {len(errors)} error(s)") + + if not args.verbose: + for file_path, error_msg in errors: + print(f"ERROR: {file_path}") + if error_msg: + print(f" {error_msg}") + + return 1 + else: + result = { + "status": "ok", + "total": len(php_files), + "passed": len(php_files) + } + + if not args.verbose: + common.json_output(result) + print(f"php_syntax: ok ({len(php_files)} file(s) checked)") + else: + common.log_success(f"All {len(php_files)} PHP file(s) are valid") + + return 0 + + except Exception as e: + common.log_error(f"Validation failed: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/validate/xml_wellformed.py b/scripts/validate/xml_wellformed.py new file mode 100755 index 0000000..48adbc1 --- /dev/null +++ b/scripts/validate/xml_wellformed.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Validate XML well-formedness. + +Copyright (C) 2025 Moko Consulting + +This file is part of a Moko Consulting project. + +SPDX-License-Identifier: GPL-3.0-or-later + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program (./LICENSE.md). + +FILE INFORMATION +DEFGROUP: Moko-Cassiopeia.Scripts +INGROUP: Scripts.Validate +REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +PATH: /scripts/validate/xml_wellformed.py +VERSION: 01.00.00 +BRIEF: Validate XML well-formedness in all XML files +""" + +import argparse +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import List, Tuple + +# Add lib directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) + +try: + import common +except ImportError: + print("ERROR: Cannot import required libraries", file=sys.stderr) + sys.exit(1) + + +def check_xml_file(file_path: Path) -> Tuple[bool, str]: + """ + Check if an XML file is well-formed. + + Args: + file_path: Path to XML file + + Returns: + Tuple of (is_valid, error_message) + """ + try: + ET.parse(file_path) + return (True, "") + except ET.ParseError as e: + return (False, str(e)) + except Exception as e: + return (False, str(e)) + + +def find_xml_files(src_dir: str, exclude_dirs: List[str] = None) -> List[Path]: + """ + Find all XML files in a directory. + + Args: + src_dir: Directory to search + exclude_dirs: Directories to exclude + + Returns: + List of XML file paths + """ + if exclude_dirs is None: + exclude_dirs = ["vendor", "node_modules", ".git"] + + src_path = Path(src_dir) + if not src_path.is_dir(): + return [] + + xml_files = [] + for xml_file in src_path.rglob("*.xml"): + # Check if file is in an excluded directory + if any(excluded in xml_file.parts for excluded in exclude_dirs): + continue + xml_files.append(xml_file) + + return sorted(xml_files) + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Validate XML well-formedness in all XML files", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + parser.add_argument( + "-s", "--src-dir", + default="src", + help="Source directory to search for XML files (default: src)" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Show detailed output" + ) + parser.add_argument( + "--exclude", + action="append", + help="Directories to exclude (can be specified multiple times)" + ) + parser.add_argument( + "files", + nargs="*", + help="Specific files to check (optional)" + ) + + args = parser.parse_args() + + try: + # Determine which files to check + if args.files: + xml_files = [Path(f) for f in args.files] + for f in xml_files: + if not f.is_file(): + common.die(f"File not found: {f}") + else: + exclude_dirs = args.exclude or ["vendor", "node_modules", ".git"] + xml_files = find_xml_files(args.src_dir, exclude_dirs) + + if not xml_files: + common.die(f"No XML files found in {args.src_dir}") + + if args.verbose: + common.log_section("XML Well-formedness Validation") + common.log_info(f"Checking {len(xml_files)} XML file(s)") + print() + + errors = [] + for xml_file in xml_files: + is_valid, error_msg = check_xml_file(xml_file) + + if is_valid: + if args.verbose: + common.log_success(f"OK: {xml_file}") + else: + errors.append((xml_file, error_msg)) + if args.verbose: + common.log_error(f"FAILED: {xml_file}") + if error_msg: + print(f" {error_msg}") + + # Output results + if args.verbose: + print() + + if errors: + result = { + "status": "error", + "src_dir": args.src_dir, + "xml_count": len(xml_files), + "passed": len(xml_files) - len(errors), + "failed": len(errors), + "errors": [{"file": str(f), "error": e} for f, e in errors] + } + + if not args.verbose: + common.json_output(result) + + common.log_error(f"XML validation failed: {len(errors)} error(s)") + + if not args.verbose: + for file_path, error_msg in errors: + print(f"ERROR: {file_path}") + if error_msg: + print(f" {error_msg}") + + return 1 + else: + result = { + "status": "ok", + "src_dir": args.src_dir, + "xml_count": len(xml_files) + } + + if not args.verbose: + common.json_output(result) + print(f"xml_wellformed: ok") + else: + common.log_success(f"All {len(xml_files)} XML file(s) are well-formed") + + return 0 + + except Exception as e: + common.log_error(f"Validation failed: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main())